服务器-TCP 在 EPOLL 模型中的注意细节

 

前段时间在公司开发了基于udp的流媒体转发服务器,在公网udp转发ts,花屏比较严重。课下之余写了epoll-tcp模型的转发服务器作为测试,比较一下效果,其间遇到不少问题,在此做个笔记。代码最后附上


一、业务需求:终端录制视频(android编码h264)  客户端请求视频  服务器负责转发

因为是测试用没有考虑配置文件,负载均衡,安全认证等


二、协议指定


1、音视频 协议定义:总长度不大于1500 bytes, 终端启动后就进行发送数据

长度 short

终端id  int

类型 byte

包序号 short

帧类型 byte

数据

2 byte

4 byte

1 byte

2 byte

1 byte



类型标志:0xF0  视频

                    0xF1  音频

                    0x00  指令


2、指令协议  用于客户端请求视频

长度 short

终端id  int

类型 byte

指令 int

2 byte

4 byte

1 byte

4 byte


 指令:0x0010  请求报活

             0x0011   停止请求

  


三、数据结构关联


1、会话管理

struct Connection       //终端、客户端 会话管理
{

    int          term;          // 终端id, 用于客户端时表示请求目标的id
    int          sock;         //tcp
 

    time_t   tm;             //上次活动时间

    int         bufsize;       //用于epoll 接收
    int         wantsize;
    int         recvsize;
    char    *recvbuf;

    CBufQue bufque;   //循环队列,客户端用于缓存要发送的数据

}


typedef map MAPConnection;  // socket -Connection   //存储会话

typedef set SETSocket;                                    //socket

typedef map MAPTermClient;  // 一个终端可以转发到多个客户端,保存客户端的key


2、流程管理

     线程1-terminal:接收终端视频 : epoll  ET模式  非阻塞socket

     线程2-media:将接受的视频数据分发到对应的客户端发送缓冲队列

     线程3-client:接收客户端的请求, 分发视频数据,epoll LT模式 非阻塞 socket

     线程4-cmd:处理客户端的指令,请求停止等 


四、代码注意细节

1、socket 设置非堵塞

[cpp]  view plain copy
  1. bool NetCommon::SetSockBlock(const int &fd, bool block)  
  2. {  
  3.     if(block)  
  4.     {  
  5.         int flags = fcntl(fd, F_GETFL, 0);  
  6.         fcntl(fd, F_SETFL, flags&~O_NONBLOCK);  
  7.     }  
  8.     else  
  9.     {  
  10.         int flags = fcntl(fd, F_GETFL, 0);  
  11.         fcntl(fd, F_SETFL, flags|O_NONBLOCK);  
  12.     }  
  13.     return true;  
  14. }  

2、socket 设置SO_REUSEADDR
[cpp]  view plain copy
  1. bool NetCommon::SetReuseAddr(const int &fd, bool reuse)  
  2.  {  
  3.      int opt = 0;  
  4.      if(reuse)  
  5.      {  
  6.          opt = 1;  
  7.      }  
  8.      else  
  9.      {  
  10.          opt = 0;  
  11.      }  
  12.   
  13.      if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)  
  14.      {  
  15.          return false;  
  16.      }  
  17.      return true;  
  18.  }  

为什么要设置此项呢?参考   http://blog.csdn.net/liuhongxiangm/article/details/17301311


3、accept 

 LT模式比较清晰,就不说了     

 ET模式下accept存在的问题
 考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

[cpp]  view plain copy
  1. int ctcpserver::acceptterminal(Connection *pConn)  
  2. {  
  3.     while(true)  
  4.     {  
  5.         int newsock = accept(pConn->sock,NULL, NULL);  
  6.   
  7.         if(newsock < 0)  
  8.         {  
  9.             if(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)  
  10.             {  
  11.                 break;  
  12.             }  
  13.             return -1;  
  14.         }  
  15.         if( newsock > 0)  
  16.         {  
  17.             NetCommon::SetSockBlock(newsock,false);  
  18.             NetCommon::SetReuseAddr(newsock, true);  
  19.   
  20.            if(m_mapConnTerminal.size() > 500)  
  21.            {  
  22.                 close(newsock);  
  23.                 return 0;  
  24.            }  
  25.   
  26.             Connection *newcon = new Connection;  
  27.             newcon->sock = newsock;  
  28.             newcon->wantsize = 2;  
  29.             newcon->recvsize = 0;  
  30.             time( &(newcon->tm));  
  31.   
  32.             if(m_epollTerminal.EpollAdd(newsock, EPOLLIN|EPOLLET, newcon) < 0)  
  33.             {  
  34.                 delete newcon;  
  35.                 newcon = NULL;  
  36.                 close(newsock);  
  37.                 return -1;  
  38.             }  
  39.             m_mapConnTerminal.insert(make_pair<int, Connection*>(newsock,newcon));  
  40.         }  
  41.     }  
  42.   
  43.     return 0;  
  44. }  



4、recv

 Epoll ET模式 非阻塞socket,必须独到没有数据可读,此处的设计是每个socket对应一个connection,connection中有一个数据包的长度wantsize,接收完一个数据包之后,会将此数据包放入队列等待处理,循环接收下一个数据包

[cpp]  view plain copy
  1. int ctcpserver::recvn(Connection *pConn)  
  2. {  
  3.     int iret = 0;  
  4.     //wantsize < 2048  
  5.     while(pConn->recvsize < pConn->wantsize)  
  6.     {  
  7.         iret = recv(pConn->sock, pConn->buf+pConn->recvsize,pConn->wantsize-pConn->recvsize, 0);  
  8.         if(iret == -1)  
  9.         {  
  10.             if(errno == EINTR)  
  11.             {  
  12.                  break;  
  13.             }  
  14.             else if(errno == EWOULDBLOCK || errno == EAGAIN)  
  15.             {  
  16.                 break;  
  17.             }  
  18.             else  
  19.             {  
  20.                 return -1;  
  21.             }  
  22.         }  
  23.         if(iret == 0)  
  24.         {  
  25.             return -1;  
  26.         }  
  27.   
  28.         pConn->recvsize += iret;  
  29.     }  
  30.   
  31.     time(&(pConn->tm));  
  32.   
  33.     return  pConn->recvsize;  
  34. }  


此函数是在epoll接到事件后调用,协议约定数据包前俩字节是长度
[cpp]  view plain copy
  1. int ctcpserver::recvtrminal(Connection *pConn)  
  2. {  
  3.     int iret = 0;  
  4.     while(true)  
  5.     {  
  6.         iret = recvn(pConn);  
  7.         if(iret < 0)  
  8.         {  
  9.             close(pConn->sock);  
  10.             m_epollTerminal.EpollDel(pConn->sock,pConn);  
  11.             m_mapConnTerminal.erase(pConn->sock);  
  12.             delete pConn;  
  13.             pConn = NULL;  
  14.   
  15.             return -1; //error  
  16.         }  
  17.         if(pConn->recvsize != pConn->wantsize) //no data recv  
  18.         {  
  19.             break;  
  20.         }  
  21.         else  
  22.         {  
  23.             if(pConn->wantsize == 2) //接收玩的是数据的长度信息,去设置数据的接收大小  
  24.             {  
  25.                 pConn->wantsize = *(short*)(pConn->buf);  
  26.                 if(pConn->wantsize > 2048 || pConn->wantsize < 2) //设计是不大于1500,此处为了兼容其他测试  
  27.                 {  
  28.                     close(pConn->sock);  
  29.                     m_epollTerminal.EpollDel(pConn->sock,pConn);  
  30.                     m_mapConnTerminal.erase(pConn->sock);  
  31.                     delete pConn;  
  32.                     pConn = NULL;  
  33.   
  34.                     return -1; //something error with data  
  35.                 }  
  36.             }  
  37.             else            //接收完一个数据包  
  38.             {  
  39.                 BufNode *pnode = m_bufQueMedia.AllocNode();  
  40.                 if(pnode != NULL )  
  41.                 {  
  42.                     memset( pnode->pBuf,0,pnode->nMaxLen);  
  43.                     pnode->nLen = pConn->recvsize;  
  44.                     memcpy(pnode->pBuf, pConn->buf,pConn->recvsize);  
  45.                     pnode->sock = pConn->sock;  
  46.                     m_bufQueMedia.PushNode(pnode);   //media 线程负责处理数据  
  47.                     m_semMediaTask.Post();  
  48.   
  49.                 }  
  50.                 pConn->recvsize = 0;  
  51.                 pConn->wantsize = 2;  
  52.             }  
  53.         }  
  54.     }  
  55.   
  56.     return 0;  
  57. }  

epoll 循环,epoll添加socket时,epoll_eveny关联的是connection* ,在收到事件是,可以直接取来接收存放数据
[cpp]  view plain copy
  1. int ctcpserver::terminalloop()  
  2. {  
  3.     int event_count = 0;  
  4.     while(true)  
  5.     {  
  6.         event_count = m_epollTerminal.EpollWait();  
  7.         if(event_count < 0)  
  8.         {  
  9.             if(errno == EINTR)  
  10.             {  
  11.                 continue;  
  12.             }  
  13.             return 0;  
  14.         }  
  15.         else if(event_count > 0)  
  16.         {  
  17.             m_mutexTerminal.Lock();  
  18.             for(int index=0; index < event_count; index++)  
  19.             {  
  20.                 Connection *pConn  = (Connection*)(m_epollTerminal.GetEVPtr(index));  
  21.                 if(pConn->sock == m_terminalsock)  
  22.                 {  
  23.                     if(acceptterminal(pConn) < 0)  
  24.                     {  
  25.                         return 0;  
  26.                     }  
  27.                 }  
  28.                 else  
  29.                 {  
  30.                     if( m_epollTerminal.GetEVEvents(index)&EPOLLIN )  
  31.                     {  
  32.                         if(recvtrminal(pConn) < 0)  
  33.                         {  
  34.                             continue;  
  35.                         }  
  36.                     }  
  37.                     else if(m_epollTerminal.GetEVEvents(index)&EPOLLOUT)  
  38.                     {  
  39.   
  40.                     }  
  41.                 }  
  42.             }  
  43.             m_mutexTerminal.UnLock();  
  44.         }  
  45.     }  
  46.     return 0;  
  47. }  

5、send

这个问题比较谨慎处理,EPOLLOUT 到底在什么时候触发??

ET 模式下,转发一片文章 http://www.cnblogs.com/moodlxs/archive/2011/12/16/2290288.html

ET模式称为边缘触发模式,顾名思义,不到边缘情况,是死都不会触发的。

EPOLLOUT事件:
EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那你要先准备好下面条件:
1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。
2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。
简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!

其实,如果你真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。

EPOLLIN事件:
EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。
现在明白为什么说epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会在堵死在阻塞里。

LT模式 比较明了, 只要可写就一直触发 EPOLLOUT

在此测试项目,用了LT模式,但如何避免一直触发?

当有数据需要发送时,添加EPOLLOUT监听事件。当发送完成后移除此事件

[cpp]  view plain copy
  1. int ctcpserver::sendclient(Connection *pConn)  
  2. {  
  3.    //  
  4.     BufNode *pnode = pConn->bufque.FrontNode(); //  
  5.     if(pnode != NULL)  
  6.     {  
  7.       int  iret =   send(pConn->sock, pnode->pBuf,pnode->nLen,0);  
  8.       if(iret <= 0)  
  9.        {  
  10.           if(errno != EAGAIN || errno == EWOULDBLOCK || errno == EAGAIN)  
  11.           {  
  12.                  return 0;  
  13.           }  
  14.           else  
  15.           {  
  16.               close(pConn->sock);  
  17.               m_epollClient.EpollDel(pConn->sock,pConn);  
  18.               m_mapConClient.erase(pConn->sock);  
  19.               delete pConn;  
  20.               pConn = NULL;  
  21.   
  22.         //      cout << "m_mapConClient count:" << m_mapConClient.size() << endl;  
  23.               return -1;  
  24.           }  
  25.   
  26.        }  
  27.       else  
  28.       {  
  29.           pConn->bufque.PopNode();  
  30.           pConn->bufque.FreeNode(pnode);  
  31.       }  
  32.   
  33.     }  
  34.     if(pConn->bufque.GetUsedNodeCount() == 0)  
  35.     {  
  36.        "color:#990000;"> m_epollClient.EpollMod(pConn->sock, EPOLLIN, pConn);"color: rgb(153, 0, 0); font-family: Georgia, Times, 'Times New Roman', serif;">//移除EPOLLOUT 监听  
  37.     }  
  38.   
  39.     return 0;  
  40. }  


[cpp]  view plain copy
  1. int ctcpserver::mediataskloop()  
  2. {  
  3.     while (true)  
  4.     {  
  5.         m_semMediaTask.Wait();  
  6.         BufNode *pnode = m_bufQueMedia.PopNode();  
  7.         if(pnode == NULL)  
  8.         {  
  9.             continue;  
  10.         }  
  11.         //id-client  
  12.         int id = *(int*)(pnode->pBuf+2);  
  13.   
  14.         m_mutexTermClient.Lock();  
  15.         MAPTermClientIt itTermClient = m_mapTermClient.find(id);  
  16.         if(itTermClient == m_mapTermClient.end())  
  17.         {  
  18.             SETSocket *pSetSock = new  SETSocket;  
  19.             pSetSock->clear();  
  20.             m_mapTermClient.insert(make_pair<int, SETSocket*>(id,pSetSock));  
  21.         }  
  22.         else  
  23.         {  
  24.             SETSocket *pSetSock= itTermClient->second;  
  25.             if(!pSetSock->empty())  
  26.             {  
  27.                 SETSocketIt itSock;  
  28.                 m_mutexClient.Lock();  
  29.                 for(itSock=pSetSock->begin(); itSock != pSetSock->end(); ++ itSock)  
  30.                 {  
  31.                     int sock = *itSock;  
  32.                     MAPConnectionIt itConnClient = m_mapConClient.find(sock);  
  33.                     if(itConnClient != m_mapConClient.end())  
  34.                     {  
  35.                         Connection *pConn = itConnClient->second;  
  36.                         BufNode *pnodeClient = pConn->bufque.AllocNode();  
  37.                         if(pnodeClient == NULL)  
  38.                         {  
  39.                             continue;  
  40.                         }  
  41.                         memcpy(pnodeClient->pBuf, pnode->pBuf, pnode->nLen );  
  42.                         pnodeClient->nLen = pnode->nLen;  
  43.                         pConn->bufque.PushNode(pnodeClient);  
  44.                       "color:#990000;">  m_epollClient.EpollMod(pConn->sock, EPOLLIN|EPOLLOUT, pConn); //添加EPOLLOUT 监听  
  45.                     }  
  46.                     else  
  47.                     {  
  48.                         pSetSock->erase(itSock);  
  49.                     }  
  50.                 }  
  51.                 m_mutexClient.UnLock();  
  52.             }  
  53.         }  
  54.         m_mutexTermClient.UnLock();  
  55.   
  56.         m_bufQueMedia.FreeNode(pnode);  
  57.     }  
  58.   
  59.     return 0;  
  60. }  

附上代码: http://download.csdn.net/detail/liuhongxiangm/6709063



                   

版权声明:本文为博主原创文章,未经博主允许不得转载。

你可能感兴趣的:(linux,网络通信技术,linux,Epoll,TCP)