互动白板 Windows 端开发指南

网易云通信服务概要

网易云通信 SDK 提供完善的互动白板(实时会话)开发框架,用于在线白板教学和文件传输等实时场景。屏蔽其内部复杂细节,对外提供较为简洁的 API 接口,方便开发者快速集成互动白板功能。

注:SDK 兼容的系统有 Windows xp(sp2及以上)、Windows 7、Windows 8/8.1、Windows 10。SDK 从V3.2.5版本开始全面支持32位和64位程序接入。

开发准备

SDK内容

SDK不提供debug版本的动态链接库供开发者调试,如果遇到问题请联系技术支持或在线客服。

快速接入SDK

关于如何快速集成SDK到现有项目,初始化SDK登录退出,发送/接收第一条IM消息,发送/接收第一条聊天室消息,如何开始基于IM Demo源码(官网下载页面可以下载)开发的信息请前往Windows(PC) SDK Getting Started

SDK数据目录

当收到多媒体消息后,SDK 会负责下载这些多媒体文件,同时SDK 还要记录一些log,因此SDK 需要一个数据缓存目录。该目录由第三方App 通过nim_client_init 初始化接口传入,默认为存放到系统的AppData 目录下,App 传入一个目录名即可,SDK 会自动生成用户数据缓存目录。数据缓存目录默认为"{系统的AppData 目录}\{App 传入的目录名}\NIM\{某个用户对应的用户数据目录}”,还可以由App 完全自定义用户数据目录,需要传入完整的路径,并确保读写权限正确。如果第三方App 需要清除缓存功能,可扫描该目录下的文件,按照你们的规则清理即可。 具体某个用户对应的缓存目录下面包含如下子目录:

SDK 提供了接口nim_tool_get_user_specific_appdata_dir 获取某个用户对应的具体类型的App data 目录(如图片消息文件存放目录,语音消息文件存放目录等)(注意:需要调用nim_free_buf 接口释放其返回的内存)。

接口调用与约定

SDK 提供的所有API 都是 C接口 ,根据模块划分为不同的API 文件,根据业务需要可以显式或者隐式的调用(3.4.0开始)。显式调用下,开发者可以在程序进行到任何时间点按需加载SDK dll, 功能完成后如果不需要继续使用NIM可以卸载SDK dll, 更加灵活和节省内存, 该模式下开发者只需要将SDK包里nim_c_sdk\include 目录下的模块定义头文件(命名方式如nim_xxx_def.h)加入到工程项目中。隐式调用下,开发者除了链接lib文件外,需要将SDK包里nim_c_sdk 目录里的API 头文件(命名方式如nim_xxx.h)以及相关常量定义的头文件等其他头文件一并加入到工程项目中。一般来说,每个模块都有对应的API 头文件和相关常量的定义头文件,命名上一一对应。

SDK 提供了3种类型的接口。

  1. 全局广播类通知的注册接口。

    常见的几类消息或通知是通过该注册的回调函数中通知开发者的,比如收到的消息,会话数据更新,用户相关数据的变更等,开发者需要在登录前提前注册好这些接口,例如:

    C++

     //注册数据同步结果的回调
     nim::DataSync::RegCompleteCb(nbase::Bind(&nim_comp::DataSyncCallback::SyncCallback, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    
     /* 以下注册的回调函数,都是在收到服务器推送的消息或事件时执行的。因此需要在程序开始时就注册好。 */
     //注册重连、被踢、掉线、多点登录、把移动端踢下线的回调
     nim::Client::RegReloginCb(&nim_comp::LoginCallback::OnReLoginCallback);
     nim::Client::RegKickoutCb(&nim_comp::LoginCallback::OnKickoutCallback);
     nim::Client::RegDisconnectCb(&nim_comp::LoginCallback::OnDisconnectCallback);
     nim::Client::RegMultispotLoginCb(&nim_comp::LoginCallback::OnMultispotLoginCallback);
     nim::Client::RegKickOtherClientCb(&nim_comp::LoginCallback::OnKickoutOtherClientCallback);
     nim::Client::RegSyncMultiportPushConfigCb(&nim_comp::MultiportPushCallback::OnMultiportPushConfigChange);
    
     //注册返回发送消息结果的回调,和收到消息的回调。
     nim::Talk::RegSendMsgCb(nbase::Bind(&nim_comp::TalkCallback::OnSendMsgCallback, std::placeholders::_1));
     nim::Talk::RegReceiveCb(nbase::Bind(&nim_comp::TalkCallback::OnReceiveMsgCallback, std::placeholders::_1));
     nim::Talk::RegRecallMsgsCallback(nbase::Bind(&nim_comp::TalkCallback::OnReceiveRecallMsgCallback, std::placeholders::_1, std::placeholders::_2));
     nim::Talk::RegReceiveMessagesCb(nbase::Bind(&nim_comp::TalkCallback::OnReceiveMsgsCallback, std::placeholders::_1));
     nim::MsgLog::RegMessageStatusChangedCb(nbase::Bind(&nim_comp::TalkCallback::OnMsgStatusChangedCallback, std::placeholders::_1));

    C#

    //注册数据同步结果的回调
    NIM.DataSync.RegCompleteCb(DataSyncDelegate cb);
    //登录结果通知,开发者可以通过注册该事件获取登录结果,或者在调用登录接口NIM.ClientAPI.Login 时填充回调函数
    NIM.ClientAPI.LoginResultHandler
    //注册客户端掉线回调
    NIM.ClientAPI.RegDisconnectedCb(Action handler);
    //注册客户端多点登录通知回调
    NIM.ClientAPI.RegMultiSpotLoginNotifyCb(MultiSpotLoginNotifyResultHandler handler);
    //注册将登录当前账号的其他端踢下线的结果回调
    NIM.ClientAPI.RegKickOtherClientCb(KickOtherClientResultHandler handler);
    //注册当前客户端被踢下线通知回调
    NIM.ClientAPI.RegKickoutCb(KickoutResultHandler handler)
    //注册自动重连结果回调
    NIM.ClientAPI.RegAutoReloginCb(LoginResultDelegate handler, string jsonExtension = null)

     //接收消息
    NIM.TalkAPI.OnReceiveMessageHandler += ReceiveMessageHandler;
    //发送消息结果
    NIM.TalkAPI.OnSendMessageCompleted += SendMessageResultHandler;
    //注册消息撤回结果回调
    NIM.TalkAPI.RegRecallMessageCallback(RecallMessageDelegate cb);

C

    // 数据同步结果通知(nim_data_sync)
    void nim_data_sync_reg_complete_cb(nim_data_sync_cb_func cb, const void *user_data);        
    // 接收会话消息通知(nim_talk)
    void nim_talk_reg_receive_cb(const char *json_extension, nim_talk_receive_cb_func cb, const void *user_data);        
    // 接收系统消息通知(nim_sysmsg)
    void nim_sysmsg_reg_sysmsg_cb(const char *json_extension, nim_sysmsg_receive_cb_func cb, const void *user_data);    
    // 发送消息结果通知(nim_talk)
    void nim_talk_reg_arc_cb(const char *json_extension, nim_talk_arc_cb_func cb, const void *user_data);
    // 发送自定义系统通知的结果通知(nim_talk)
    void nim_talk_reg_custom_sysmsg_arc_cb(const char *json_extension, nim_custom_sysmsg_arc_cb_func cb, const void *user_data);
    // 群组事件通知(nim_team)
    void nim_team_reg_team_event_cb(const char *json_extension, nim_team_event_cb_func cb, const void *user_data);    
    // 帐号被踢通知(nim_client)
    void nim_client_reg_kickout_cb(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data);        
    // 网络连接断开通知(nim_client)
    void nim_client_reg_disconnect_cb(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data);    
    // 将本帐号的其他端踢下线的结果通知(nim_client)
    void nim_client_reg_kickout_other_client_cb(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data); 
    // 好友通知 
    void nim_friend_reg_changed_cb(const char *json_extension, nim_friend_change_cb_func cb, const void *user_data);    
    // 用户特殊关系通知
    void nim_user_reg_special_relationship_changed_cb(const char *json_extension, nim_user_special_relationship_change_cb_func cb, const void *user_data);    
    ...
  1. 异步接口。

    回调函数作为参数,传入执行接口,然后执行接口时,会触发传入的回调函数。注意,回调函数中如果涉及到跨线程资源的需要开发者切换线程操作,即使不涉及到资源问题,也要保证回调函数中不会处理耗时任务,以免堵塞SDK底层线程。

  2. 同步接口。

    为方便开发者使用,我们提供了一些同步接口,调用接口获取的内容同步返回,以block命名的都是同步接口,该类会堵塞SDK线程,谨慎使用。

从版本2.7.0开始,服务器和客户端上线了频控策略,与服务器有交互的接口(接口命名结尾为_online的查询接口以及命令类接口,如同意群邀请等)增加频控控制,开发者关注错误码416。

C++ SDK

为了方便桌面应用开发者更快的接入网易云通信SDK,PC SDK下载包中还提供了官方的C++ 封装层 项目文件及源码,接入和使用方法请看Windows(PC) SDK开发手册(C++ 封装层),目前IM Demo(C++)源码就是通过接入和调用该SDK完成IM功能的。

C# SDK

网易云通信SDK还提供了C# 程序集,方便.net 开发人员接入,PC SDK下载包中包括官方的C# 封装层 项目文件及源码,接入和使用方法请看Windows(PC) SDK开发手册(C# 封装层),目前IM Demo(C#)源码就是通过接入和调用该SDK完成IM功能的。

如果开发者在调用C接口过程中或者解析接口返回结果过程中出现疑问,可以参考和借鉴C++封装层。

初始化SDK

准备工作:将SDK 相关的dll 文件(nim.dll,nim_audio.dll, nim_tools_http.dll, nrtc.dll)放到App 的运行目录下,并将SDK 的配置文件目录nim_conf(目录里包含一个ver_ctrl.dll文件)放到App 的运行目录下。SDK 基于vs2010 开发,如果App 没有对应的运行时库文件(msvcp100.dll和msvcr100.dll),请将其放到App 的运行目录下。

准备工作完成后,在程序启动时,如果直接使用C接口,需要调用LoadLibrary 函数动态加载nim.dll,然后调用GetProcAddress获取API 接口:nim_client_init,调用此接口初始化NIM SDK。同时,SDK 能力的一些参数以及如果SDK 需要连接独立部署的服务器的地址等配置也是在初始化SDK 时传入。例如:

C++

nim::SDKConfig config;

//组装SDK能力参数(必填)
config.database_encrypt_key_ = "Netease";     //string(db key必填,目前只支持最多32个字符的加密密钥!建议使用32个字符)
config.sdk_log_level_ = ;                     //bool 选填,是否需要预下载附件(图片和语音),SDK默认预下载,如果有对带宽流量有较高要求的请关闭该选项,改为上层开发者按需下载附件文件
config.preload_image_quality_ = ;             //int 预下载图片质量,选填,范围0-100
config.preload_image_resize_ = ;             //string 预下载图片基于长宽做内缩略,选填,比如宽100高50,则赋值为100x50,中间为字母小写x
config.sync_session_ack_ = true;             //bool 选填,消息已读未读是否多端同步,默认true
config.login_max_retry_times_ = ;             //int 登录重试最大次数,如需设置建议设置大于3次,默认填0,SDK默认设置次数

//组装SDK独立部署的服务器配置(选填)
config.use_private_server_ = true;
config.rsa_public_key_module_ = "http://xxx";
config.default_nos_download_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");
config.lbs_address_ = "http://xxx";
config.nos_lbs_address_ = "http://xxx";
config.default_link_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");
config.default_nos_upload_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");
config.default_nos_access_address_.push_back("http://xxx.xxx.xxx.xxx:xxxx");

bool ret = nim::Client::Init("45c6af3c98409b18a84451215d0bdd6e", "Netease", "", config); // 载入网易云通信sdk,初始化安装目录和用户目录,第一个参数是appkey(此处填写的是测试示例)

C#

C# 程序在运行时需要使用到 C++ SDK 的dll,必须将SDK相关的dll 文件(nim.dll,nim\_audio.dll, nim\_tools\_http.dll, nrtc.dll)放到最终的生成文件目录,如果缺少运行时库文件(msvcp100.dll和msvcr100.dll)也需要拷贝到生成目录中,否则可能会出现找不到dll的异常。

C# 提供了参数配置类 NimUtility.NimConfig,通过创建该类对象完成参数配置。
var config = new NimUtility.NimConfig();
config.CommonSetting = new SdkCommonSetting();

//组装SDK能力参数
config.CommonSetting.DataBaseEncryptKey = "Netease"; //string(db key必填,目前只支持最多32个字符的加密密钥!建议使用32个字符)
config.CommonSetting.LogLevel = SdkLogLevel.Pro;  //SDK 日志记录级别 
config.CommonSetting.PredownloadAttachmentThumbnail = True;   //bool 选填,是否需要预下载附件(图片和语音),SDK默认预下载,如果有对带宽流量有较高要求的请关闭该选项,改为上层开发者按需下载附件文件
config.CommonSetting.UsePriviteServer = False;    //是否使用自定义服务器,默认False,如果使用自定义服务器,需设置config.PrivateServerSetting 对象

//组装SDK独立部署的服务器配置
config.PrivateServerSetting = new SdkPrivateServerSetting();
config.PrivateServerSetting.RSAPublicKey = "http://xxx";
config.PrivateServerSetting.DownloadServerList = new List<string>({"",""});
config.PrivateServerSetting.LbsAddress = "http://xxx";
config.PrivateServerSetting.NOSLbsAddress = "http://xxx";
config.PrivateServerSetting.LinkServerList = new List<string>({"",""});
config.PrivateServerSetting.UploadServerList = new List<string>({"",""});
config.PrivateServerSetting.AccessServerList = new List<string>({"",""});

bool ret = NIM.ClientAPI.Init("45c6af3c98409b18a84451215d0bdd6e", "NIMCSharpDemo", null, config); // 载入网易云通信sdk,初始化安装目录和用户目录,第一个参数是appkey(此处填写的是测试示例)

C

typedef bool(*nim_client_init)(const char *app_data_dir, const char *app_install_dir, const char *json_extension);

void foo()
{
    //获取SDK初始化接口
    HINSTANCE hInst = LoadLibraryW(L"nim.dll");
    nim_client_init func = (nim_client_init) GetProcAddress(hInst, "nim_client_init");

    //组装SDK初始化参数
    Json::Value config_root;

    //组装SDK能力参数(必填)
    Json::Value config_values;
    config_values[kNIMAppKey] = "xxxx"; //用户的APP key
    config_values[kNIMDataBaseEncryptKey] = ""; //string(db key必填,目前只支持最多32个字符的加密密钥!建议使用32个字符)
    config_values[kNIMPreloadAttach] = ;        //bool 选填,是否需要预下载附件(图片和语音),SDK默认预下载,如果有对带宽流量有较高要求的请关闭该选项,改为上层开发者按需下载附件文件
    config_values[kNIMPreloadImageQuality] = ;  //int 预下载图片质量,选填,范围0-100
    config_values[kNIMPreloadImageResize] = ;   //string 预下载图片基于长宽做内缩略,选填,比如宽100高50,则赋值为100x50,中间为字母小写x
    config_values[nim::kNIMSyncSessionAck] = ;            //bool 选填,消息已读未读是否多端同步,默认true
    config_values[nim::kNIMLoginRetryMaxTimes] = ;        //int 登录重试最大次数,如需设置建议设置大于3次,默认填0,SDK默认设置次数
    config_root[kNIMGlobalConfig] = config_values;

    //组装SDK独立部署的服务器配置(选填)
    Json::Value server_values;
    server_values[kNIMLbsAddress] = "http://xxx";             //lbs地址
    server_values[kNIMNosLbsAddress] = "http://xxx";        //nos lbs地址
    server_values[kNIMDefaultLinkAddress].append("xxx.xxx.xxx.xxx:xxxx");                //默认的link地址
    server_values[kNIMDefaultNosUploadAddress].append("http://xxx.xxx.xxx.xxx:xxxx");     //默认的nos上传地址
    server_values[kNIMDefaultNosDownloadAddress].append("http://xxx.xxx.xxx.xxx:xxxx");    //默认的nos下载地址
    server_values[kNIMRsaPublicKeyModule] = "";                //密钥
    server_values[kNIMRsaVersion] = 0;                        //密钥版本号      
    config_root[kNIMPrivateServerSetting] = server_values;
    config_root[kNIMAppKey] = "45c6af3c98409b18a84451215d0bdd6e"; //必填appkey(此处填写的是测试示例)

    //初始化SDK
    func("appdata path", "app installation path", config_root.toStyledString().c_str());
}

在程序退出前,调用接口nim_client_cleanup 进行NIM SDK 的清理工作,然后调用FreeLibrary 函数释放nim.dll,对于清理工作的注意事项请查看后续的"登出/退出和清理SDK"章节。

C++

nim::Client::Cleanup();

C#

NIM.ClientAPI.Cleanup();

C

typedef    void(*nim_client_cleanup)(const char *json_extension);

void foo()
{
    nim_client_cleanup func = (nim_client_cleanup) GetProcAddress(hInst, "nim_client_cleanup");
    func("");
    FreeLibrary(hInst);
}

登录与登出

登录集成必读

客户端代理

SDK目前支持配置代理,支持Socket4/5,暂不支持Http代理,音视频模块只支持Socket5代理。

C++

void foo()
{
    nim::Global::SetProxy(nim::kNIMProxySocks5, "123.123.123.123", "400", "test", "test");
}

C#

void foo()
{
    NIM.GlobalAPI.SetProxy(NIM.NIMProxyType.kNIMProxySocks5, "123.123.123.123", "400", "test", "test");
}

C

typedef void(*nim_global_set_proxy)(NIMProxyType, const char*, int, const char*, const char*);

void foo()
{
    nim_global_set_proxy func = (nim_global_set_proxy) GetProcAddress(hInst, "nim_global_set_proxy");
    func(kNIMProxySocks5, "123.123.123.123", "400", "test", "test");
}

首次登录

SDK 登录后会同步群信息,离线消息,漫游消息,系统通知等数据,所以首次登录前需要 提前注册所需的全局广播类通知的回调 (具体说明请参阅API 文档),以下以注册最近会话变更通知回调为例:

C++

void OnSessionChangeCallback(nim::NIMResCode rescode, const nim::SessionData& data, int total_unread_counts)
{
    if (rescode != nim::kNIMResSuccess)
    {
        return;
    }

    switch (data.command_)
    {
    case nim::kNIMSessionCommandAdd:
    case nim::kNIMSessionCommandUpdate:
    case nim::kNIMSessionCommandMsgDeleted:
    break;
    ···
    }
}

void foo()
{
    //注册全局会话列表变更通知函数
    nim::Session::RegChangeCb(&OnSessionChangeCallback);
}

C#

//C#通过订阅会话列表变更事件来获取更新通知:
NIM.Session.SessionAPI.RecentSessionChangedHandler += OnSessionChanged;

private void OnSessionChanged(object sender, SessionChangedEventArgs e)
{
    //根据会话列表变更类型进行相应的操作
    if (e.Info.Command == NIMSessionCommand.kNIMSessionCommandAdd ||
        e.Info.Command == NIMSessionCommand.kNIMSessionCommandUpdate)
    {
        //TODO:会话列表新增、更新

    }
    else if (e.Info.Command == NIMSessionCommand.kNIMSessionCommandRemove)
    {
        //TODO:删除会话项
    }
}

C

//全局会话列表变更通知函数
void CallbackSessionChange(int rescode, const char *result, int total_unread_counts, const char *json_extension, const void *user_data)
{
    if (rescode == kNIMResSuccess)
    {
        Json::Value value;
        Json::Reader reader;
        if (reader.parse(result, value))
        { 
            nim::NIMSessionCommand command = nim::NIMSessionCommand(value[nim::kNIMSessionCommand].asInt());
            switch (command)
            {
                case kNIMSessionCommandAdd:
                    ...
                case kNIMSessionCommandUpdate:    
                    ...
                ...                
            }

        }
    }
    else
    {
        ...
    }
}

typedef void(*nim_session_reg_change_cb)(const char *json_extension, nim_session_change_cb_func cb, const void *user_data);

void foo()
{
    //注册全局会话列表变更通知函数
    nim_session_reg_change_cb func = (nim_session_reg_change_cb)GetProcAddress(hInst, "nim_session_reg_change_cb");
    func(nullptr, &CallbackSessionChange, nullptr);    // 会话列表变更通知(nim_session)
}

登录

C++

void OnLoginCallback(const nim::LoginRes& login_res, const void* user_data)
{
    if (login_res.res_code_ == nim::kNIMResSuccess)
    {
        if (login_res.login_step_ == nim::kNIMLoginStepLogin)
        {
            ···
        }
    }
    else
    {
        ···
    }
}

void foo()
{
    nim::Client::Login(app_key, "app acount", "token", &OnLoginCallback);
}

C#

private void HandleLoginResult(NIM.NIMLoginResult result)
{
    //在UI线程进行处理登录结果
    Action action = () =>
    {
        if (result.LoginStep == NIM.NIMLoginStep.kNIMLoginStepLogin)
        {
            if (result.Code == NIM.ResponseCode.kNIMResSuccess)
            {
                //TODO:登录成功
            }
            else
            {
                //TODO:登录失败
            }
        }
    };
    this.Invoke(action);
}

private void foo()
{
    NIM.ClientAPI.Login(appkey, "app acount", "token", HandleLoginResult);
}

C

void CallbackLogin(const char* res, const void *user_data)
{
    Json::Value values;
    Json::Reader reader;
    if (reader.parse(res, values) && values.isObject())
    {
        if (values[kNIMLoginStep].asInt() == kNIMLoginStepLogin && values[kNIMErrorCode].asInt() == 200)
        {
            ...
        }
        ...
    }
}

typedef    void(*nim_client_login)(const char *app_key, const char *account, const char *password, const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_login func = (nim_client_login) GetProcAddress(hInst, "nim_client_login");
    func("app key", "app account", "token", nullptr, &CallbackLogin, nullptr);
    //app key: 应用标识,不同应用之间的数据(用户,消息,群组等)是完全隔离的。开发自己的应用时,需要替换成自己申请来的app key
    //注意:替换成客户自己的app key之后,请不要对登录密码进行md5加密。
}

自动重新登录

SDK 在网络连接断开后,会监听网络状况,如果网络状况好转,那么SDK 会进行重新登录,登录结果可以在注册的重新登录回调函数中处理。

注意 自动重新登录的机制可能会失效:

如果重新登录回调函数返回的的错误号既不是kNIMResSuccess,也不是网络错误相关错误号(kNIMResTimeoutError或者kNIMResConnectionError),那么说明自动重新登录的机制已经失效,需要开发者退回到登录界面进行手动重新登录。(详见nim_client_reg_auto_relogin_cb接口说明)

手动重新登录

SDK 在重新登录失败后,可以由用户手动调用重新登录接口nim_client_relogin。

c++

nim::Client::Relogin();

c#

NIM.ClientAPI.Relogin();

c

typedef void(*nim_client_relogin)(const char *json_extension);
void foo()
{
    nim_client_relogin func = (nim_client_relogin) GetProcAddress(hInst, "nim_client_relogin");
    func(nullptr);
}

登出/退出和清理SDK

执行接口nim_client_logout 进行登出或退出,登出或退出的过程因为涉及到和服务器的交互以及需要将缓存中的数据持久化到本地,因此根据实际情况回调通知会有1s到20s左右的延迟。清理SDK必须在完全退出(收到退出回调)后执行。

Trick:因为退出和其他接口一样,总是在某些情况下会出现未及时返回需要上层界面等待的情况(比如网络原因导致服务器返回慢甚至超时,缓存数据大持久化到本地慢等原因),因此开发者可以通过暂时隐藏UI,让进程等待退出完成的回调后再执行清理SDK,退出程序的工作。

C++

void LoginCallback::OnLogoutCallback(nim::NIMResCode res_code)
{
    //如果是退出程序
    nim::Client::Cleanup();
    ...

    //如果是登出到登录界面,这里不能执行清理SDK工作,否则会报错
    ...
}

void foo()
{
    //登出到登录界面(nim::kNIMLogoutChangeAccout) 或者 退出程序(nim::kNIMLogoutAppExit)
    nim::Client::Logout(nim::kNIMLogoutAppExit, &OnLogoutCallback);
}

C#

private void foo()
{
    System.Threading.Semaphore s = new System.Threading.Semaphore(0, 1);
    NIM.ClientAPI.Logout(NIM.NIMLogoutType.kNIMLogoutAppExit, (r) =>
    {
        s.Release();
    });
    //需要logout执行完才能退出程序
    s.WaitOne(TimeSpan.FromSeconds(10));
    NIM.ClientAPI.Cleanup();
}

C

void CallbackLogout(const char* res, const void *user_data)
{
    ...
}

typedef    void(*nim_client_logout)(nim::NIMLogoutType logout_type, const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_logout func = (nim_client_logout) GetProcAddress(hInst, "nim_client_logout");
    func(kNIMLogoutAppExit, nullptr, &CallbackLogout, nullptr);
}

断网重连通知

通过接口nim_client_reg_disconnect_cb 来注册连接断开回调函数,断开连接后根据实际需要做后续处理(比如提示用户)。

C++

void OnDisconnectCallback()
{
    ···
}

void foo()
{
    nim::Client::RegDisconnectCb(&OnDisconnectCallback);
}

C#

private foo()
{
    NIM.ClientAPI.RegDisconnectedCb(() =>
    {
         MessageBox.Show("网络连接断开,网络恢复后后会自动重连");
    });
}

C

void CallbackDisconnect(const char* res, const void* user_data)
{
    //解析res
}

typedef void(*nim_client_reg_disconnect_cb)(const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_reg_disconnect_cb func = (nim_client_reg_disconnect_cb) GetProcAddress(hInst, "nim_client_reg_disconnect_cb");
    func(nullptr, &CallbackDisconnect, nullptr);
}

被踢出通知

通过接口nim_client_reg_kickout_cb 来注册被踢出回调函数,当收到此通知时,一般是退出程序然后回到登录窗口,回调函数参数中详细说明了被踢出的原因。

C++

void OnKickoutCallback(const nim::KickoutRes& res)
{
    ···
}

void foo()
{
    nim::Client::RegKickoutCb(&OnKickoutCallback);
}

C#

private foo()
{
    NIM.ClientAPI.RegKickoutCb((r) =>
    {
        MessageBox.Show(r.Dump(), "被踢下线,请注意账号安全");
    });
}

C

void CallbackKickout(const char* res, const void* user_data)
{
    //解析res
}

typedef void(*nim_client_reg_kickout_cb)(const char *json_extension, nim_json_transport_cb_func cb, const void* user_data);

void foo()
{
    nim_client_reg_kickout_cb func = (nim_client_reg_kickout_cb) GetProcAddress(hInst, "nim_client_reg_kickout_cb");
    func(nullptr, &CallbackKickout, nullptr);
}

多端登录

通过调用nim_client_reg_multispot_login_notify_cb 来注册多端登录通知的回调函数,当用户在某个客户端登录时,其他没有被踢掉的端会触发这个回调函数,并携带当前时间登录的设备列表的数据。

网易云通信内置踢人策略为:移动端(Android,iOS)互踢,桌面端(PC,Web)互踢,移动端和桌面端共存。

如果当前的互踢策略无法满足业务需求的话,可以联系我们取消内置互踢,根据多端登录的回调和当前的设备列表,判断本设备是否需要被踢出。如果需要踢出,直接调用登出接口并在界面上给出相关提示即可。

c++

void LoginCallback::OnMultispotLoginCallback(const nim::MultiSpotLoginRes& res)
{
    ...
}
nim::Client::RegMultispotLoginCb(&nim_comp::LoginCallback::OnMultispotLoginCallback);

c#

//注册多端登录通知回调
NIM.ClientAPI.RegMultiSpotLoginNotifyCb(OnMultiSpotLogin);

//result 包含多端登录的其他端信息
private static void OnMultiSpotLogin(NIMMultiSpotLoginNotifyResult result)
{

}

c

static void CallbackMutliSpotLogin(const char* json_res, const void* user_data)
{
    Json::Reader reader;
    Json::Value values;
    if (json_res != nullptr && reader.parse((std::string)json_res, values) && values.isObject())
    {
        ...
    }
}

typedef void(*nim_client_reg_multispot_login_notify_cb)(const char *json_extension, nim_json_transport_cb_func cb, const void *user_data);
void foo()
{
    nim_client_reg_multispot_login_notify_cb func = (nim_client_reg_multispot_login_notify_cb) GetProcAddress(hInst, "nim_client_reg_multispot_login_notify_cb");
    func(nullptr, &CallbackMutliSpotLogin, nullptr);
}

网易云通信 ID

网易云通信的每个用户由网易云通信 ID 唯一标识。网易云通信 ID是由用户帐号和token成功同步到网易云通信服务器后自动分配的,可以通过 NIMMessage 的 from 字段获取消息发送方的网易云通信 ID。

互动白板

说明

点对点实时会话提供了一个音视频和一个tcp的通道,允许用户同时发起这2个通道,实现数据和音视频的会话功能,详见nim_rts.h和nim_rts_def.h。多人实时会话只发起一个多人的tcp通道,多人音视频通道需要用户使用多人音视频接口来创建。 其中需要注意的是,如果用户需要使用音视频通道,请使用音视频网络通话中的初始化和清理接口(nim_vchat.h),并在会话中使用nim_device.h中的设备相关接口,设置启动设备。并且,音视频通话具有唯一性,和音视频网络通话互斥,一个客户端只允许同时存在一个音视频连接。

服务器tcp数据录制说明

服务器会将用户发送的数据,每个成员录制到一个文件中。网易云通信sdk3.2版本之前,录制数据是纯裸数据录制;sdk3.2版本开始,针对用户发的每条数据前追加8字节数据,包括uint32的长度(4+4+数据实际长度)和uint32的时间戳(多人会话开始计算的毫秒级时间)。

发起会话

发起者通过nim_rts_start将创建一个rts会话。其中可以通过传入json_extension,来修改通话的设置,参数使用见nim_rts_def.h。发起会话的结果将由nim_rts_start_cb_func,如果成功将得到一个唯一的session_id来标识此次会话。

收到会话邀请

被邀请者,通过nim_rts_set_start_notify_cb_func接口来设置回调实现,由nim_rts_start_notify_cb_func通知。

回复收到的邀请

被邀请者回复邀请接口为nim_rts_ack,可以通过传入json_extension,来修改通话的设置,参数使用见nim_rts_def.h。

发起者接收对方的响应

发起者通过设置nim_rts_set_ack_notify_cb_func接口,从回调函数nim_rts_ack_notify_cb_func得到被邀请者的回复。

被邀请者的多端同步

被邀请通过设置nim_rts_set_sync_ack_notify_cb_func接口,可以知道自己其他端在收到邀请后的回复,同时sdk将会清理本次邀请。

创建多人tcp通道(多人白板)

创建者通过nim_rts_create_conf接口向服务器创建一个多人白板会话。如果长时间无人进入或所有人离开后此会话失效。

加入多人tcp通道(多人白板)

创建者或参与者通过nim_rts_join_conf接口加入多人白板通道。

监听通道连接状态

当被邀请者同意邀请后,会话双方将连接服务器进入数据通道。监听通道状态,通过nim_rts_set_connect_notify_cb_func接口实现。如果会话中有多个通道,则各个通道的状态将分别通过nim_rts_connect_notify_cb_func通知。其中只有返回回调中code=200时,标识通道连接成功,其他均为失败,并且sdk会清理失败的通道,但不会主动结束会话。同时如果有音视频通道,则可以在通道连接成功的时候,启动相关音视频设备。

监听通道内成员变化

在通道连接成功后,通过由nim_rts_set_member_change_cb_func设置提供的nim_rts_member_change_cb_func回调,将通知通道内其他成员的状态。当通道内有其他成员时,数据通道才允许发送数据,否则发送数据将失败。

会话控制通知

通过nim_rts_control接口,用户可以在会话过程中,向对方发送一些自定义的通知。接收通知由nim_rts_set_control_notify_cb_func接口中的nim_rts_control_notify_cb_func回调返回。

音视频通话模式切换

用户可以通过nim_rts_set_vchat_mode接口切换音频或视频模式。模式切换的作用是,在非视频模式时用户的视频数据将不会发送给对方。注意,音频数据如果在设备打开的时候会通过音视频通道发送给对方,如果用户需要静音,则需要关闭麦克风设备,或者在发起会话时使用custom_audio模式,自主控制音频数据。

主动结束会话

通过nim_rts_hangup接口,用户可以主动结束会话。并通过nim_rts_set_hangup_notify_cb_func接口的nim_rts_hangup_notify_cb_func回调,监听对方的结束通知。注意,对于一个成功发起的会话,需要主动hangup结束或者收到对方结束通知,sdk才会结束清理本次会话。多人模式下此接口只做本地挂断、清理,其他参与人不会受到挂断通知。

数据发送与接收

发送数据由nim_rts_send_data实现,接收数据由nim_rts_set_rec_data_cb_func的nim_rts_rec_data_cb_func回调监听。发送数据接口需要在通道成功建立并有2个或以上成员(包括自己)时才能发送数据。

rts简单示例

下面是一个tcp通道的简单示例,所有回调接口建议post到UI线程处理

static std::string session_id_;//会话id
//会话发起结果回调
void RtsStartCb(int code, const char *session_id, int channel_type, const char* uid, const char *json_extension, const void *user_data)
{
    if (code == 200)
    {
        session_id_ = session_id;
    }
}
//一个会话邀请通知
void RtsStartNotifyCb(const char *session_id, int channel_type, const char* uid, const char *json_extension, const void *user_data)
{
    if (session_id_.empty())
    {
        session_id_ = session_id;
        nim_rts_ack(session_id, channel_type, true, "", nullptr, nullptr);
    }
    else
    {
        nim_rts_ack(session_id, channel_type, false, "", nullptr, nullptr);
    }
}
//收到被邀请方回复的通知回调
void RtsAckNotifyCb(const char *session_id, int channel_type, bool accept, const char *uid, const char *json_extension, const void *user_data)
{
    if (session_id == session_id_)
    {
        if (accept)
        {
            //对方同意,sdk底层开始连接
        }
        else
        {
            session_id_.clear();
        }
    }
}
//连接状态的回调
void RtsConnectNotifyCb(const char *session_id, int channel_type, int code, const char *json_extension, const void *user_data)
{
    if (session_id == session_id_)
    {
        if (code != 200)//连接异常,挂断
        {
            nim_rts_hangup(session_id_.c_str(), "", nullptr, nullptr);
            session_id_.clear();
        }
    }
}
//成员变化回调
void RtsMemberChangeCb(const char *session_id, int channel_type, int type, const char *uid, const char *json_extension, const void *user_data)
{
    if (session_id == session_id_)
    {
        if (type == kNIMRtsMemberStatusJoined)
        {
            //成员进入,此时可以在tcp通道发送数据
        }
    }
}
void RtsHangupNotifyCb(const char *session_id, const char *uid, const char *json_extension, const void *user_data)
{
    if (session_id == session_id_)
    {
        session_id_.clear();
    }
}
//收到会话中的数据
void RtsAppDataCb(const char *session_id, int channel_type, const char* uid, const char* data, unsigned int size, const char *json_extension, const void *user_data)
{
}
//初始化
void init()
{
    nim_rts_set_start_notify_cb_func(RtsStartNotifyCb, nullptr);
    nim_rts_set_ack_notify_cb_func(RtsAckNotifyCb, nullptr);
    nim_rts_set_connect_notify_cb_func(RtsConnectNotifyCb, nullptr);
    nim_rts_set_member_change_cb_func(RtsMemberChangeCb, nullptr);
    nim_rts_hangup(RtsHangupNotifyCb, nullptr);
    nim_rts_set_rec_data_cb_func(RtsAppDataCb, nullptr);
}
//发起一个会话
nim_rts_start(kNIMRtsChannelTypeTcp, "account", "", RtsStartCb, nullptr);
//发送数据
std::string data = "test 123";
nim_rts_send_data(session_id_.c_str(), kNIMRtsChannelTypeTcp, data.c_str(), data.size(), "");
//挂断
nim_rts_hangup(session_id_.c_str(), "", nullptr, nullptr);
session_id_.clear();