VOIP使用单端口替换动态端口池进行UDP通信

2023-05-02 15:03:05 浏览数 (2)

做过VOIP的同学都知道,基于UDP实现RTP包收发时需要进行SDP协商或者ICE协商,通常服务器都是用一个端口池来和客户端进行RTP包的转发,而当前的网络环境下,开放端口池给运维带来了维护的风险,也给部分代理场景下带来了实现的复杂度,所以如果使用一个端口用来做媒体数据包的转发,那带来了极大的便利;

以WebRTC的服务器Janus为例,主要需要修改libnice返回的端口配置;以RtpProxy的实现为例,修改SIP协商时,始终返回固定端口给对方,注意需要关闭O_NONBLOCK属性:

1、rtpp_create_listener方法中,原来是通过在端口池中随机选择一个可用的端口,现在只需要返回固定端口就可以了:

代码语言:javascript复制
#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、设置端口复用属性:

代码语言:javascript复制
  //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:

代码语言:javascript复制
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] = {'',};
    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] = {'',};
       char taddr[MAX_ADDR_STRLEN] = {'',};
       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

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

0 人点赞