做过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
这种单端口的实现受限于操作系统内核句柄和客户端的分发实现,可能存在数据混乱的情况,只能作为一个思路而已,更好的单端口实现还是需要在协议报文中识别是不同的用户,然后分发给不同的组或者目标;