4 基于jrtplib的NAT穿透
4.1 NAT穿透的基础只是
有关于NAT穿透的基础知识
4.2 rtp/rtcp传输涉及到的NAT穿透
rtp/rtcp传输数据的时候,需要两个端口支持。即rtp端口用于传输rtp数据,即传输的多媒体数据;rtcp端口用于传输rtcp控制协议信息。rtp/rtcp协议默认的端口是rtcp port = rtp port + 1 。详细的说,比如A终端和B终端之间通过rtp/rtcp进行通信,
如上图,
本地IP:PORT NAT映射后IP:PORT
UACA RTP的发送和接收IP:PORT : 192.168.1.100:8000 61.144.174.230:1597
UACA RTCP的发送和接收IP:PORT:192.168.1.100:8001 61.144.174.230:1602
UACB RTP的发送和接收IP:PORT : 192.168.1.10:8000 61.144.174.12:8357
UACB RTCP的发送和接收IP:PORT:192.168.1.10:8001 61.144.174.12:8420
上图可以得到一下一些信息:
(一) 本地端口 RTCP PORT = RTP PORT + 1;但是经过NAT映射之后也有可能满足这个等式,但是并不一定有这个关系。
(二)在NAT设备后面的终端的本地IP:PORT并不被NAT外的设置可知,也就无法通过终端的本地IP:PORT与之通信。而必须通过NAT映射之后的公网IP:PORT作为目的地址进行通信。
如上图的终端A如果要发送RTP数据到终端B,UACA发送的目的地址只能是:61.144.174.12:8357。同理,UACB发送RTP数据给UACA,目的地址只能是: 61.144.174.230:1597 。
(三)也许看到这里,如何得到自己的外网IP:PORT呢?如何得到对方的外网IP:PORT呢?这就是NAT IP:PORT转换和穿孔(puncture),下回分解。
4.3 NAT 地址转换
如上所述,终端需要知道自己的外网IP:port,可以通过STUN、STUNT、TURN、Full Proxy等方式。这里介绍通过STUN方式实现NAT穿透。
STUN: Simple Traversal of UDP Through NAT。即通过UDP对NAT进行穿透。
STUNT:Simple Traversal of UDP Through NATs and TCP too.可以通过TCP对NAT进行穿透。
STUN是一个标准协议,具体的协议内容网络上很多。在此不累述了。
为了通过STUN实现NAT穿透,得到自己的公网IP:PORT,必须有一个公网STUN服务器,以及我们的客户端必须支持STUN Client功能。STUN Client 通过UDP发送一个request到STUN服务器,该请求通过NAT设备的时候会把数据报头中的本地IP:PORT换成该本地IP:PORT对应的公网IP:PORT,STUN服务器接收到该数据包后就可以把该公网IP:PORT 发送给STUN Client。这样我们就得到了自己的公网IP:PORT。这样别的终端就可以把该公网IP:PORT最为发送UDP数据的目的地址发送UDP数据。
推荐一款STUN client/server 程序代码,http://sourceforge.net/projects/stun/files/
这是一款开源软件。在客户端中的主要函数是下面这个:
NatType
stunNatType( StunAddress4& dest, //in 公网STUN服务器地址,如stun.xten.net
bool verbose, //in 调试时是否输出调试信息
bool* preservePort=0, //out if set, is return for if NAT preservers ports or not
bool* hairpin=0 , //out if set, is the return for if NAT will hairpin packetsNAT设备是否支持回环
int port=0, // in 本地测试端口port to use for the test, 0 to choose random port
StunAddress4* sAddr=0 // out NIC to use ,返回STUN返回的本地地址的公网IP:PORT
);
输入StunAddress和测试端口port,得到本地IP:PORT对应的公网IP:PORT.
4.4 对jrtplib 的改造
jrtplib中对rtp rtcp端口的处理关系是:rtcp port = rtp port + 1 。这就有问题,本地端口可以按照这个等式来设置端口,但是经过NAT映射之后的公网端口是随机的,有可能并不满足上述等式。
int portbase = 6000; //设置本地rtp端口为6000
transparams.SetPortbase(portbase);//默认的本地rtcp端口为6001.因为这里是本地设置,所一这样设置OK
status = sess.Create(sessparams,&transparams);
checkerror(status);
RTPIPv4Address addr(destip,destport); //设置目的地的rtp接收IP:PORT,公网传输的话就要设置为对方的rtp公网IP:PORT
// AddDestination()的内部处理是把addr.ip和addr.port+1赋给rtcp。这样如果对方在公网上,就有问题了。因为对方的rtcp port 可能不等于rtp port +1;这就导致对方收不到rtcp数据包。
status = sess.AddDestination(addr);
通过跟踪AddDestination()函数的实现,发现在class RTPIPv4Destination的构造函数中是这样构造一个发送目的地址的:
RTPIPv4Destination(uint32_t ip,uint16_t rtpportbase)
{
memset(&rtpaddr,0,sizeof(struct sockaddr_in));
memset(&rtcpaddr,0,sizeof(struct sockaddr_in));
rtpaddr.sin_family = AF_INET;
rtpaddr.sin_port = htons(rtpportbase);
rtpaddr.sin_addr.s_addr = htonl(ip);
rtcpaddr.sin_family = AF_INET;
rtcpaddr.sin_port = htons(rtpportbase+1);//默认把rtp的端口+1赋给目的rtcp端口。
rtcpaddr.sin_addr.s_addr = htonl(ip);
RTPIPv4Destination::ip = ip;
}
为了实现:可以自定义目的IP地址和目的rtp port和rtcp port。为了实现这么目标,自己动手改造下面几个函数:构造函数RTPIPv4Destination() 、RTPSession::AddDestination(),思路是在目的地址设置相关函数中增加一个rtcp ip 和port参数。
RTPIPv4Destination(uint32_t ip,uint16_t rtpportbase,uint32_t rtcpip,uint16_t rtcpport)
{
memset(&rtpaddr,0,sizeof(struct sockaddr_in));
memset(&rtcpaddr,0,sizeof(struct sockaddr_in));
rtpaddr.sin_family = AF_INET;
rtpaddr.sin_port = htons(rtpportbase);
rtpaddr.sin_addr.s_addr = htonl(ip);
/**If rtcport has not been set separately, use the default rtcpport*/
if ( 0 == rtcpport )
{
rtcpaddr.sin_family = AF_INET;
rtcpaddr.sin_port = htons(rtpportbase+1);
rtcpaddr.sin_addr.s_addr = htonl(ip);
}else
{
rtcpaddr.sin_family = AF_INET;
rtcpaddr.sin_port = htons(rtcpport);
rtcpaddr.sin_addr.s_addr = htonl(ip);
}
RTPIPv4Destination::ip = ip;
}
int RTPSession::AddDestination(const RTPAddress &addr,const RTPIPv4Address &rtcpaddr)
{
if (!created)
return ERR_RTP_SESSION_NOTCREATED;
return rtptrans->AddDestination(addr,rtcpaddr);
}
在调用RTPSession::AddDestination、定义RTPIPv4Destination的时候实参也相应增加目的rtcp参数。
这样改造之后就可以自定义独立的设置目的地址rtp ,rtcp端口了