用libnice库做tcp连接(voip应用)

为什么

在开源licode中用到libnice作为udp建连和传输, 同webrtc通信, 而且网上有较多libnice udp连接的资源, 却很少有libnice tcp连接的资料, 而我需要用libnice做两路传输连接, 一路做udp传输通道(实时), 一路做tcp传输通道(可靠), 因此调研并实测了libnice tcp的连接方式, 以下作为记录。

传输组件

传输模块

服务端用的开源mediasoup, 在Transport模块同时开启udp, tcp两个服务监听, 无论tcp还是udp数据流都流向同一个transport。
在客户端, 一次创建两路NetworkAgent, 而NetworkAgent的实现采用的libnice, 一个启用libnice udp(reliable = false, ice-udp=true), 一个启用libnice tcp(reliable = true, ice-tcp=true)。

libnice tcp建连过程

建连过程

注意, libnice中有一个bug, 会导致tcp建连大概率失败, 如图所示, 可在connect和stun send之间加一个短暂的时延来解决。

libnice tcp配置和测试

  • libnice debug模式开启
  1. 编译libnice.a 时在CFlag中增加 NDBUG宏定义
  2. 开启详细日志:
nice_debug_enable(true);
setenv("NICE_DEBUG", "all", true);
setenv("G_MESSAGES_DEBUG", "all", true);
  • libnice tcp初始化
    //开启libnice debug, 可以看到详细的建连日志
    nice_debug_enable(true);
    setenv("NICE_DEBUG", "all", true);
    setenv("G_MESSAGES_DEBUG", "all", true);
    
    //创建NiceAgent
    _context = g_main_context_new();
    g_networking_init();
    _agent = (NiceAgent*) g_object_new(NICE_TYPE_AGENT,
                                       "compatibility", NICE_COMPATIBILITY_RFC5245,
                                       "main-context", _context,
                                       "reliable", _config.reliable ? TRUE : FALSE,  // 选择tcp 则reliable = true
                                       "full-mode", !_config.ice_lite,
                                       "ice-tcp", _config.reliable ? TRUE : FALSE, // tcp
                                       "ice-udp", _config.reliable ? FALSE : TRUE, // udp
                                       NULL);
    //启动nice main thread
    _loop = g_main_loop_new(_context, FALSE);
    g_main_loop_run(_loop);
    
    //这里mediaserver是lite模式,因此客户端固定做control
    //set controlling mode
    GValue controllingMode = { 0 };
    g_value_init(&controllingMode, G_TYPE_BOOLEAN);
    g_value_set_boolean(&controllingMode, _config.ice_lite);
    g_object_set_property(G_OBJECT(_agent), "controlling-mode", &controllingMode);
    
    //设置连通性检查的最次数
    //set max checks
    GValue checks = { 0 };
    g_value_init(&checks, G_TYPE_UINT);
    g_value_set_uint(&checks, 100);
    g_object_set_property(G_OBJECT(_agent), "max-connectivity-checks", &checks);
    _candidates.clear();
    
    // 配置stun server
    if (!_config.stun_server.empty() && _config.stun_port != 0) {
        GValue val = { 0 }, val2 = { 0 };
        g_value_init(&val, G_TYPE_STRING);
        g_value_set_string(&val, _config.stun_server.c_str());
        g_object_set_property(G_OBJECT(_agent), "stun-server", &val);
        g_value_init(&val2, G_TYPE_UINT);
        g_value_set_uint(&val2, _config.stun_port);
        g_object_set_property(G_OBJECT(_agent), "stun-server-port", &val2);
    }

    // 一些回调
    g_signal_connect(G_OBJECT(_agent), "candidate-gathering-done", G_CALLBACK(cb_candidate_gathering_done), this);
    g_signal_connect(G_OBJECT(_agent), "component-state-changed", G_CALLBACK(cb_component_state_changed), this);
    g_signal_connect(G_OBJECT(_agent), "new-selected-pair", G_CALLBACK(cb_new_selected_pair), this);
    g_signal_connect(G_OBJECT(_agent), "new-candidate", G_CALLBACK(cb_new_candidate), this);

  //创建一个stream, 且这个stream里只有一个conponent, 如果需要将rtp, rtcp分开发送,可以用两个conponent。
    _streamId = nice_agent_add_stream(_agent, 1);
    nice_agent_set_stream_name(_agent, _streamId, "video");
    
// 配置turn服务, 这里不需要
    if (!_config.ice_lite) {
        nice_agent_set_relay_info(_agent, _streamId, _config.ice_components, _config.turn_server.c_str(), _config.turn_port,
                                  _config.turn_username.c_str(), _config.turn_pass.c_str(), NICE_RELAY_TYPE_TURN_UDP);
    }

    // 设置端口号范围
    if (_config.min_port != 0 && _config.max_port != 0) {
             _config.min_port, _config.max_port);
        nice_agent_set_port_range(_agent, _streamId, _componentId, (guint) _config.min_port,
                                  (guint) _config.max_port);
    }
    
    // 产生本地的ice name, passwd
    gchar* ufrag = nullptr, *upass = nullptr;
    nice_agent_get_local_credentials(_agent, _streamId, &ufrag, &upass);
    _ufrag = std::string(ufrag);
    g_free(ufrag);
    _upass = std::string(upass);
    g_free(upass);

    // 绑定接收数据的回调
    auto casted_function = reinterpret_cast<NiceAgentRecvFunc>(cb_nice_recv);
    nice_agent_attach_recv(_agent, _streamId, _componentId, _context, casted_function, this);

   
    // 开始收集本地的local candidates
    if (nice_agent_gather_candidates(_agent, _streamId) != TRUE) {
        return false;
    }

  • libnice tcp 连接

/*
    m=video 64499 ICE/SDP                                   
    c=IN IP4 0.0.0.0                                                                        
    a=ice-ufrag:tr06wupyrgrvzmsr                                                            
    a=ice-pwd:q8x7lzyngtwwkzl4z5i82kjw9ath9gex                                              
    a=candidate:0 1 udp 0 10.224.17.136 15444 typ host                                      
    a=candidate:1 1 tcp 0 10.224.17.136 15939 typ host 
 */
    std::string sdp = remoteSDP;

    if (_config.reliable) {
        // 可靠传输, 支持tcp-pass, tcp-act两种transport type, tcp-pass即tcp passive模式, 一般在服务端使用,监听tcp连接, tcp-act即tcp active模式,一般客户端使用,主动发起连接。
        std::string str1 = "tcp";
        std::string str2 = "tcp-pass";

        if (sdp.find(str1) != string::npos) {
            sdp = sdp.replace(sdp.find(str1), str1.length(), str2);
        }

        // 去掉udp candidate
        std::string eraseUdp = "a=candidate:0 1 udp";

        if (sdp.find(eraseUdp) != string::npos) {
            auto pos1 = sdp.find(eraseUdp);
            auto pos2 = sdp.find("typ host", pos1);
            sdp = sdp.replace(pos1, pos2 - pos1 + 8, "");
        }
    } else {
       // 去掉 tcp candidate
        std::string eraseTcp = "a=candidate:1 1 tcp";

        if (sdp.find(eraseTcp) != string::npos) {
            auto pos1 = sdp.find(eraseTcp);
            auto pos2 = sdp.find("typ host", pos1);
            sdp = sdp.replace(pos1, pos2 - pos1 + 8, "");
        }
    }

    // Because we send & recv audio/video packet on a single port,
    // So we only reserve "m=audio" or "m=video" in the sdp
    size_t audioline = sdp.find("m=audio");
    size_t videoline = sdp.find("m=video");

    if (audioline != string::npos && videoline != string::npos) {
        if (videoline < audioline) {
            sdp.erase(audioline, sdp.length() - audioline);
        } else {
            sdp.erase(audioline, videoline - audioline);
        }
    }

    // 有了远端的candidates, 而本地的candidates在上一步已经拿到, 可以进行连通性检查了。
    if (nice_agent_parse_remote_sdp(_agent, sdp.c_str()) <= 0) {
        return false;
    }


  • mediasoup服务
  1. 在transport同时enableUdp, enableTcp
  2. IceServer中合存reliableTransportTuple和transportTuple
  3. 在transport中, 不同media kind的数据,走不同的transportTuple.

参考

https://tools.ietf.org/html/rfc5245 ice建连过程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容