先从rtptransmitter.h和rtpudpv4transmitter.h开始讲起
1.关于setsockopt:以前只知道setsockopt很强大现在才知道原来socket的缓存区也可以设
size = params->GetRTPReceiveBuffer();//在438行
if (setsockopt(rtpsock,SOL_SOCKET,SO_RCVBUF,(const char*)&size,sizeof(int)) != 0)
{
CLOSESOCKETS;
MAINMUTEX_UNLOCK
return ERR_RTP_UDPV4TRANS_CANTSETRTPRECEIVEBUF;
}
...
剩下的buf操作类似。意思就是给rtpsock设置了接收和发送的缓冲区,后面还设置了rtcp的(如果没有端口复用的情况下)
这里提下setsockopt中的SO_RCVBUF和SO_SNDBUF用于存储发送缓冲区和接收缓冲区。通过setsockopt操作设置好rtpsock和rtcpsock等,就可以用poll对这些sock描述符进行管理了,poll所谓的管理本质上是监听这些sock文件描述符,当描述符缓冲区中有请求到来时poll会返回一个信号,之后我们轮询所有的描述符就可以找到发起请求的那个文件描述符了。
2.关于poll
Poll中主要调用了PollSocket方法,PollSocket中
int PollSocket(bool rtp)
{
RTPIOCTL(sock,FIONREAD,&len);//得到len值
int8_t isset = 0;
int status = RTPSelect(&sock, &isset, 1, RTPTime(0));//得到isset
//RTPSelect的第三个参数为监视的描述符的数量,此处监视一个sock文件描述符
//isset值为1表明有数据也就是dataavailable = true;
if(dataavailable)
{
recvlen = recvfrom(sock,packetbuffer,RTPUDPV4TRANS_MAXPACKSIZE,0,(struct sockaddr *)&srcaddr,&fromlen);
}
}
a.调用RTPIOCTL(sock,FIONREAD,&len)非阻塞的获取套接字可读数据的长度
b.调用RTPSELECT(&sock,&isset,1,RTPTime(0))//如果isset为真说明有数据到达,select中经常配合fd_isset用于检查某个描述符是否准备好。poll跟select差不多的只不过没有fdset这个结构体而是pollfd结构体,其中的event用来接收监听的事件,revents用来记录已发生的事件(通过revents可以获取返回值),poll是用链表的,所以我们需要一个vector来存储一系列pollfd,也就是代码中的vector
inline int RTPSelect(const SocketType *sockets, int8_t *readflags, size_t numsocks, RTPTime timeout)//60行
{
using namespace std;
//采用poll方法
vector fds(numsocks);
for (size_t i = 0 ; i < numsocks ; i++)
{
fds[i].fd = sockets[i];
fds[i].events = POLLIN;
fds[i].revents = 0;
readflags[i] = 0;
}
int status = poll(&(fds[0]), numsocks, timeoutmsec);//发现poll这里只检测了fds[0]的值,也就是说只在numsocks值为1时是成立的,然后刚好外部调用的RTPSelect(&sock,&isset,1,RTPTime(0))中值也是1。
if (status < 0)
{
// We're just going to ignore an EINTR
if (errno == EINTR)
return 0;
return ERR_RTP_SELECT_ERRORINPOLL;
}
if (status > 0)
{
for (size_t i = 0 ; i < numsocks ; i++)
{
if (fds[i].revents)
readflags[i] = 1;
}
}
return status;
}
...
//采用select方法
fd_set fdset;//RTPSelect.h中146行
FD_ZERO(&fdset);
for (size_t i = 0 ; i < numsocks ; i++)
{
FD_SET(sockets[i], &fdset);//向fdset中添加文件描述符
readflags[i] = 0;//记录文件描述符是否可读以便追踪相应的文件描述符
}
int status = select(FD_SETSIZE, &fdset, 0, 0, pTv);
//关于select也就不多说了,以下为select的原型
//int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
//所以此处我们需要监视fdset文件描述符集合的读变化,一旦有数据可读就返回一个大于0的值通知内核去读
//取数据,这个检测是采用轮询的方法,所有的描述符中只要有数据到来就返回大于0的值。
if (status < 0)
{
if (errno == EINTR)
return 0;
return ERR_RTP_SELECT_ERRORINSELECT;
}
if (status > 0) // some descriptors were set, check them
{
for (size_t i = 0 ; i < numsocks ; i++)
{
if (FD_ISSET(sockets[i], &fdset))//检测是哪一个文件描述符可读,可读则将readflags[i]置1
readflags[i] = 1;
}
}
return status;
c.调用recvfrom读取缓冲区的数据,并将数据拷贝至内存缓冲区并加上长度、源地址、时间等打包成一个RTPRawPacket型的pack包,加入至rawpacketlist(链表)中。关于Packet类之后再讲。
int WaitForIncomingData(const RTPTime &delay, bool *dataavailable)//834行
{
SocketType socks[3] = { rtpsock, rtcpsock, abortSocket };
int8_t readflags[3] = { 0, 0, 0 };
const int idxRTP = 0;
const int idxRTCP = 1;
const int idxAbort = 2;
RTPSelect(socks, readflags, 3, delay)
}
注意RTPSelect函数会传入readflags数组,定义了三种SocketType通道,跟以前的fd_set里的端口是一个道理。也就是开了3个端口,端口0接收RTP,端口1接收RTCP,端口3接收终止数据。超时时间为delay,delay时间过后没拿到数据返回过段时间再来。
if (readflags[idxAbort])
m_pAbortDesc->ReadSignallingByte();
if (dataavailable != 0)
{
if (readflags[idxRTP] || readflags[idxRTCP])
*dataavailable = true;
else
*dataavailable = false;
}
接下来的代码就好解释了,当readflags[idxAbort]值为1说明收到了终止数据,发送一个终止信号流关闭即可。当有数据来时不管是RTP还是RTCP都会改变变量dataavailable的值为true,这就是等待数据到来做的所有操作了。
PS:关于PollThread.h
有三个成员函数Start()、Stop()、Thread()
stop是一个全局变量,当接收到stop信号时这个transmitter如果处于Start()阶段需要终止,如果处于Thread()阶段也需要终止,这里的stop通过互斥锁进行保护,表明此时此刻只能有一个进程修改stop变量,stop变量可由stop()修改也可以由Thread()修改。Thread会在运行前后读取stop变量的值,如果为真说明transmitter被迫停止,Thread()也会终止进程。
else
{
rtpsession.OnPollThreadStep();
stopmutex.Lock();
stopthread = stop;
stopmutex.Unlock();
}
pollthread主要用在rtpsession中对poll过程的管理,它包含了transmitter头文件,在poll的过程中我们有可能发出终止信号停止这次RTP传输,此时就需要将trans关闭,其中的poll过程也需要关闭。如果poll一直没有接收到停止信号(读取全局变量stop值为false),则会接着完成rtpsession.OnPollThreadStep()操作接收数据。完成poll所要做的事情之后再次读取全局变量的值,判断是否有stop信号到来。stop信号发出后会等一段时间,使得Thread函数能够读取到这个stop变量。
stopmutex.Lock();
stop = true;
stopmutex.Unlock();
if (transmitter)
transmitter->AbortWait();
RTPTime thetime = RTPTime::CurrentTime();
bool done = false;
while (JThread::IsRunning() && !done)
{
// wait max 5 sec
RTPTime curtime = RTPTime::CurrentTime();
if ((curtime.GetDouble()-thetime.GetDouble()) > 5.0)
done = true;
RTPTime::Wait(RTPTime(0,10000));
}
if (JThread::IsRunning())
{
std::cerr << "RTPPollThread: Warning! Having to kill thread!" << std::endl;
JThread::Kill();
}
关于数据poll这一块现在还有很多不懂的地方,留坑,之后回来补,这里做的事情主要就是多播组的动态加入退出以及套接字buffer的处理,主要是rtpSocket、rtcpSocket、abortSocket。
3.关于组播,应该是指视频会议组
原本RTP协议就被设计为多播协议,后来被用在很多单播应用中。关于组播多播这一块,会有一个多播的哈希表,当获取到地止后如果申请加入多播则通过RTPHashTable.h中定义的。
RTPHashTable multicastgroups;
status = setsockopt(rtpsock,IPPROTO_IP,IP_MULTICAST_TTL,(const char *)&ttl2,sizeof(int));
//1438行,这里设置了rtpsock端口支持组播
const RTPIPv4Address &address = (const RTPIPv4Address &)addr;
uint32_t mcastIP = address.GetIP();
...
status = multicastgroups.AddElement(mcastIP);
//通过在KeyHashTable中定义的AddElement方法中加入这个发起RTP请求的地址,即可将其加入组播中。
在这里设置好组播之后,最终在RTPSession中我们会通过创建transmitter实例来调用这些函数。
rtptrans = 0;//RTPSession.cpp中135行
switch(protocol)
{
case RTPTransmitter::IPv4UDPProto:
rtptrans = RTPNew(GetMemoryManager(),RTPMEM_TYPE_CLASS_RTPTRANSMITTER) RTPUDPv4Transmitter(GetMemoryManager());
break;
...
}
...
int RTPSession::JoinMulticastGroup(const RTPAddress &addr)//575行
{
if (!created)
return ERR_RTP_SESSION_NOTCREATED;
return rtptrans->JoinMulticastGroup(addr);
}
int RTPSession::LeaveMulticastGroup(const RTPAddress &addr)
{
if (!created)
return ERR_RTP_SESSION_NOTCREATED;
return rtptrans->LeaveMulticastGroup(addr);
}
以及还有一个哈希结构的值destinations,用于发送rtp报文和rtcp报文
RTPHashTable destinations;
destinations.GotoFirstElement();
while (destinations.HasCurrentElement())
{
sendto(rtpsock,(const char *)data,len,0,(const struct sockaddr *)destinations.GetCurrentElement().GetRTPSockAddr(),sizeof(struct sockaddr_in));
destinations.GotoNextElement();
}