做过VOIP的同学都知道,基于UDP实现RTP包收发时需要进行SDP协商或者ICE协商,通常服务器都是用一个端口池来和客户端进行RTP包的转发,而当前的网络环境下,开放端口池给运维带来了维护的风险,也给部分代理场景下带来了实现的复杂度,所以如果使用一个端口用来做媒体数据包的转发,那带来了极大的便利;
以WebRTC的服务器Janus为例,主要需要修改libnice返回的端口配置;以RtpProxy的实现为例,修改SIP协商时,始终返回固定端口给对方,注意需要关闭O_NONBLOCK属性:
1、rtpp_create_listener方法中,原来是通过在端口池中随机选择一个可用的端口,现在只需要返回固定端口就可以了:
#ifdef USE_SINGLE_PORT return create_twinlistener(5600, &cta);//modify 2020-03-12 #else return (CALL_METHOD(rpp, get_port, create_twinlistener, &cta)); #endif
2、设置端口复用属性:
//add for set reuse. int reuse = 1; setsockopt(pvt->fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); //add end.
3、收到第一个RTP包时,调用accept方法,在内核中生成对方IP/端口和fd句柄之间的映射关系,注意,调用accept方法后,不能再使用recvfrom 或者 sendto 方法发送数据包,替换为recv/send方法,如使用这两个接口,则目的地址只能为NULL:
struct rtpp_socket_priv { struct rtpp_socket pub; int fd; struct sockaddr_storage raddr; int raddr_len ; }; 调用例子,在收到第一个UDP包的时候,得到对方的ip地址和端口,然后使用connect方法连接到对方 struct rtp_packet *packet; packet = rtp_packet_alloc(); if (packet == NULL) { return NULL; } pvt = PUB2PVT(self); packet->rlen = sizeof(packet->raddr); packet->size = recvfrom(pvt->fd, packet->data.buf, sizeof(packet->data.buf), 0, sstosa(&packet->raddr), &packet->rlen); if (packet->size > 0 && 0 != check_update_source_addr(pvt, packet)) { rtp_packet_free(packet); return (NULL); } // 检查和连接函数 static int check_update_source_addr(struct rtpp_socket_priv *pvt, struct rtp_packet *packet){ if (pvt == NULL || packet == NULL){ return -1; } if (pvt->raddr_len == 0){ //主要逻辑,就是收到第一个UDP包的时候(判断是否有存储对方的地址,没有则是第一次接收到包),得到对方的ip地址和端口,然后使用connect方法连接到对方 char saddr[MAX_ADDR_STRLEN] = {'\0',}; sstosa(&packet->raddr)->sa_family = AF_INET; addr2char_r(sstosa(&packet->raddr), saddr, sizeof(saddr)); pvt->raddr_len = packet->rlen; //memcpy(&pvt->raddr, &packet->raddr, packet->rlen); sstosa(&pvt->raddr)->sa_family = AF_INET; satosin(&pvt->raddr)->sin_addr.s_addr = satosin(&packet->raddr)->sin_addr.s_addr; satosin(&pvt->raddr)->sin_port = satosin(&packet->raddr)->sin_port; struct sockaddr_in serv_addr; serv_addr.sin_family=AF_INET; serv_addr.sin_port= satosin(&packet->raddr)->sin_port; serv_addr.sin_addr.s_addr = satosin(&packet->raddr)->sin_addr.s_addr; bzero(&(serv_addr.sin_zero),8); int ret = connect(pvt->fd, &serv_addr, sizeof(serv_addr)); if (0 == ret){ printf ("ret:%d, pvt %p: connect %s, port: %d success.\n", ret, pvt, saddr, ntohs(satosin(&packet->raddr)->sin_port)); }else{ printf ("ret ret:%d, errno:%d, pvt %p: connect %s, port: %d failed.\n", ret, errno , pvt, saddr, ret, ntohs(satosin(&packet->raddr)->sin_port)); return -1; } }else{ int rval = 0; if (packet->rlen == pvt->raddr_len){ rval = memcmp(&pvt->raddr, &packet->raddr, packet->rlen); }else{ rval = -1; } if (rval != 0) { char saddr[MAX_ADDR_STRLEN] = {'\0',}; char taddr[MAX_ADDR_STRLEN] = {'\0',}; sstosa(&packet->raddr)->sa_family = AF_INET; sstosa(&pvt->raddr)->sa_family = AF_INET; addr2char_r(sstosa(&pvt->raddr), saddr, sizeof(saddr)); addr2char_r(sstosa(&packet->raddr), taddr, sizeof(taddr)); printf("error, not the same address, desire ip:%s, port:%d, but receive ip:%s, port:%d.\n", saddr, ntohs(satosin(&pvt->raddr)->sin_port), taddr, ntohs(satosin(&packet->raddr)->sin_port)); return -1; } } return 0; }
经过验证,在高内核版本上,单端口复用会出现ICE连接失败的现象,只能依靠一个端口监听,然后通过ICE连接标识做多用户分发!
--
补充更新-2021-04-30
这种单端口的实现受限于操作系统内核句柄和客户端的分发实现,可能存在数据混乱的情况,只能作为一个思路而已,更好的单端口实现还是需要在协议报文中识别是不同的用户,然后分发给不同的组或者目标;
-------------------广告线---------------
项目、合作,欢迎勾搭,邮箱:promall@qq.com
本文为呱牛笔记原创文章,转载无需和我联系,但请注明来自呱牛笔记 ,it3q.com