博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
IOCP的一些思考(粘包,断包的处理)
阅读量:2437 次
发布时间:2019-05-10

本文共 7180 字,大约阅读时间需要 23 分钟。

最近接触了IOCP服务器的编写,对IOCP有了自己的一些认识,希望能对希望正在使用IOCP 的有些建议。我对IOCP了解不多,只是用到了,所以看了一下,还没怎么熟悉。

IOCP的一大优势是高并发率,同时连接1万个用户,CPU的使用率也不会很高,只是内存稍微增大一些了。而且对CPU的利用率很好,线程的量被固定了,所以线程可以更好的处理事情。

[cpp]
  1. CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);//创建完成端口  
[cpp]
  1. for(i=0;i<systeminfo.dwNumberOfProcessors; i++){  
  2.         CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &dwThreadId);//根据CPU的数量创建线程个数,最好的是2*CPU+2  
  3.     }  
[cpp]
  1. sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//监听端口  
[cpp]
  1. sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);  
  2.     //把新连入的Socket(也就是前面所谓的设备句柄),与目前的完成端口绑定在一起。  
  3.     CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)sClient, 0);  
  4.       
  5.     lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sizeof(PER_IO_OPERATION_DATA));  
  6.     lpPerIOData->Buffer.len = MAX_PACKBUFFER_SIZE;  
  7.     lpPerIOData->Buffer.buf = lpPerIOData->szMessage;  
  8.     lpPerIOData->OperationType = RECV_POSTED;  
  9.     WSARecv(sClient,&lpPerIOData->Buffer,1,&lpPerIOData->NumberOfBytesRecvd,&lpPerIOData->Flags, &lpPerIOData->overlap,NULL);  
//接收数据

[cpp]
  1. static DWORD WINAPI server::WorkerThread(LPVOID CompletionPortID)  
  2.     {  
  3.         HANDLE CompletionPort=(HANDLE)CompletionPortID;  
  4.         DWORD dwBytesTransferred;  
  5.         SOCKET sClient;  
  6.         LPPER_IO_OPERATION_DATA lpPerIOData = NULL;  
  7.         while (TRUE)  
  8.         {     
  9.   
  10.             GetQueuedCompletionStatus(CompletionPort,&dwBytesTransferred,(PULONG_PTR)&sClient,(LPOVERLAPPED *)&lpPerIOData,INFINITE);  
  11.             if (dwBytesTransferred == 0xFFFFFFFF)  
  12.             {  
  13.                 return 0;  
  14.              }  
  15.       
  16.             if (lpPerIOData->OperationType == RECV_POSTED)  
  17.             {  
  18.                   
  19.                   
  20.                 if (dwBytesTransferred == 0)  
  21.                 {  
  22.                 // Connection was closed by client,将在线状态值设为0  
  23.   
  24.                 }  
  25.                 else  
  26.                 {  
  27.                       
  28.                     //接收数据操作  
  29.                     LPNET_PACK pack;  
  30.                     pack=(LPNET_PACK)lpPerIOData->szMessage;  
  31.                       
  32.   
  33.                     //将数据重新装填  
  34.                     memset(lpPerIOData, 0, sizeof(PER_IO_OPERATION_DATA));  
  35.                     lpPerIOData->Buffer.len = MAX_PACKBUFFER_SIZE;  
  36.                     lpPerIOData->Buffer.buf = lpPerIOData->szMessage;  
  37.                     lpPerIOData->OperationType = RECV_POSTED;  
  38.                       
  39.                     WSARecv(sClient,&lpPerIOData->Buffer,1,&lpPerIOData->NumberOfBytesRecvd,&lpPerIOData->Flags,&lpPerIOData->overlap,NULL);  
  40.                 }  
  41.             }  
  42.         }  
  43.         return 0;  
  44.     }  

1、静态函数问题,这一块是我个人的认识。因为线程的创建,线程函数是静态的,这样导致所有你处理的数据必须都是静态的,因为静态函数是在编译的时候就初始化了,所以无法处理一些类的非静态函数。这里可以综合考虑多种方式进行处理,动态链表,动态指针,动态存储区,这些需要new,动态申请空间的语句。还可以通过map,list这类可以动态增长的类型。

2、数据包的顺序问题。这个很重要,算是IOCP的一个缺点吧。因为多个线程从完成端口中取数据处理,难免有些线程处理过慢,过快,然后导致包的顺序处理错位,这对于服务器负责转发的时候是一个致命的问题。比如我的服务器负责客户端之间的转发数据,比如聊天数据,这时候,如果包的转发顺序错乱,则会使数据接收有问题。转发文件更称问题。我的解决方案是,服务器设置一个缓冲区,接收整个文件,接收完成后,将数据进行发出去。还有一种方法,不过会降低效率,控制数据发送的速度,比如100毫秒发送一个包,这样可以强制服务器顺序接收,不过服务器的性能如果很不好的话,这个方法还是不行。所以还是缓冲区好一些。

3、粘包的问题。所谓的粘包,我个人理解是,不同的系统在做相关的TCP发送操作的时候,会根据Nagle算法将数据包进行合并操作,所以那个时候自己做的相关操作包就会被封装在一起,这往往是我们不想看到的。这个解决方法很简单,将socket的类型进行设置一下就可以,将socket的类型设置为禁用nagle算法,即设置TCP_NODELAY这个。这个也可以解决2的问题,不需要100毫秒进行发送了。具体解释在

4、继续IOCP中的粘包和断包处理。对于第三点,其实我当时理解不多,或者说理解不够深入。其实禁用掉Nagle算法会有些改观,但是因为禁用掉Nagle算法后,程序运行比之前好很多,我以为已经解决了问题。但是还是自己考虑不周全。如果在公网上测试,特别是一些3G的网络测试,便会发现,粘包断包的问题很严重,具体原因在

后半部分。

我这里贴上解决办法,我这里的解决办法,是我自己想到的,但是我觉得并不是特别好,希望有更好的解决方案的朋友指点一下啊,因为搜了一些这种文章,最后没给自己的应用有多大帮助,最后还是按自己的想法做了。

我自己的想法是这样的,定义一个包头,包头 = 标示符+校验位+包长度,最关键的就这几个信息。在对数据进行粘包处理的时候比较简单,直接按包的结构取数据就好了。有一个比较棘手的是断包(因为TCP是流式协议),所以,最重要的是还如何处理断包,以发送“&&&&abcdefg”为一个包为例,其中“&&&&”为包头。

出现的几种情况这里简单说一下

(1)、断包分为两部分,一部分包括包头,另一部分包括数据部分。比如“&&&&abc”+“defg”,这种是最常见的,比较好解决,首先对包头进行检测,发现该断包的时候,将该断包存储到缓冲区内,作为备份。然后遇到下一个非完整数据(这里可以认为是包尾)与缓冲区内的数据进行拼包,然后对拼起来的包进行CRC校验,因为之前包头内有校验位,所以这里很好解决。这里就可以判断出一个包是否可以拼接起来。

(2)、断包分为两部分,一部分包括包头的一部分,另一部分包括“包头另一份+数据部分”,比如,“&&”+“&&abcdefg”,这种情况下,则直接将包丢弃了,因为无法对包头进行解析,将收到“&&”时直接将该断头包丢弃,然后接下来收到&&abcdefg的时候,与缓冲区内的数据进行拼包,发现拼接不了,这个时候,则是对接受到的缓冲区根据标示符查找下一个包数据,其实这里的意思则是把&&abcdefg丢弃掉。

(3)、断包分为三部分(或者四部分等等),分为三部分的这种情况对我来说,目前都是无法解决的。因为不论怎样,中间都会存在一个断头断尾包,这个是最令人讨厌的,无法完成拼包。按照2的逻辑需要丢弃。

其实我这里和TCP的处理方式差不多,不过TCP做的更好,因为TCP至少保证了不会出现丢包(这个是一个假设,当然他是可能丢包的,那个时候估计TCP的连接也会断开),但是TCP可以保证流的顺序到达,所以他的数据是顺序的。对于服务器来说,为什么要处理断包是因为,服务器是一个多线程的。对IOCP尤其如此,因为每个线程都去取数据,虽然数据到达顺序一定,但是线程处理速度不一定,导致TCP堆栈内的数据进入应用层的时间不一样。这个可以去了解IOCP的流程,我记得有一个讲的非常通俗易懂,有时间转载过来。

还有一种处理方式,我大致说一下解决方案,另外一个是对包的定义变化,额外定义了一个包尾,也就是数据包 = “包头”+“数据”+“包尾”,然后这种方式的话,有一点好处就是拼包的时候会更快,包头和包尾有相关联的信息,比如sequence是一致的,则可以直接去匹配,而不用我上面的说的方式去计算校验值。但是这种方法最终也是要计算校验值的。我个人觉得第一张方案比较好,因为有了包尾的限制,其实对数据的处理往往复杂多变,包尾的标识符,可能也会因为数据中存在相同的数据存在而被误认为是包尾。

这两种方案目前自己都实现了,因为第二种是导师提出的,但是我个人还是倾向第一种,最终还是用了第二种,原因惟“导师”2字。

下面贴出来大致流程,我把代码简化了,可能用不了了。存储断头包的时候,我用的map和list来实现的。对于第二种有包尾的方案,我用的是双层map来实现的。

[cpp]
  1. //接收数据操作  
  2.     LPNET_PACK pack;  
  3.     pack=(LPNET_PACK)lpPerIOData->szMessage;  
  4.     //起始读取位置  
  5.     int nlen=0;  
  6.     //检查是否是包尾,进行拼包检查  
  7.     if(pack->m_nSeque == PACK_SEQUE){  
  8.         if(!pack->VerifyCRC()){  
  9.             //是一个残包(只有一个有包头的前半部分半包)  
  10.             // 或者断包(一个后半部分的半包+若干其他包)  
  11.               
  12.             if(dwBytesTransferred>=pack->m_nSize){  
  13.                 //判断是否是一个断包  
  14.                 goto pinbao;  
  15.             }  
  16.             else{  
  17.                 //收到的数据就是一个残包,直接进行断包处理,存储断包  
  18.             }  
  19.         }  
  20.         //运行到这里是一个正常包  
  21.       
  22.     }  
  23.     else{  
  24. ao:  
  25.         //进行拼包处理  
  26.         //取出来list  
  27.         list<OFFPACK> slist;  
  28.         map<SOCKET,list<OFFPACK>>::iterator listiter;  
  29.         listiter=socketlist.find(sClient);  
  30.         if(listiter!=socketlist.end()){  
  31.             slist = listiter->second;  
  32.         }else{  
  33.             //正常情况下不会执行,需要进行错误处理  
  34.         }  
  35.         list<OFFPACK>::iterator iter;  
  36.         iter=slist.begin();  
  37.         //申请一个缓冲区空间  
  38.         char * buffer =NULL;                          
  39.         for(int newi=0;newi<5;newi++){  
  40.             try{  
  41.                 buffer = new char[MAX_PACKBUFFER_SIZE];  
  42.                 if(buffer != NULL){  
  43.                     break;  
  44.                 }                     
  45.             }  
  46.             catch(...){  
  47.                 buffer = NULL;  
  48.                 printf("申请内存失败\n");  
  49.             }  
  50.             Sleep(20*(newi+1));  
  51.         }  
  52.           
  53.         while(buffer!=NULL&&iter!=slist.end()){  
  54.             //取出offpack的包  
  55.             OFFPACK offpack;  
  56.             offpack = *(iter);  
  57.             //添加  
  58.             pack = (LPNET_PACK)offpack.buff;  
  59.   
  60.             int bufferlength = pack->m_nSize;  
  61.             //将数据取出  
  62.             int packlength = (int)pack->m_nSize-offpack.length;  
  63.             if(packlength-1<0 //剩下的部分大于要拼的,也即是拼起来也不够  
  64.                 ||packlength>MAX_PACKBUFFER_SIZE){
    //拼起来长度过长 都略过  
  65.                 iter++;  
  66.                 continue;  
  67.             }  
  68.               
  69.             //对缓冲区清零  
  70.             memset(buffer,0,MAX_PACKBUFFER_SIZE);  
  71.             //拼接  
  72.             memcpy(buffer,pack,packlength);  
  73.             memcpy(buffer+packlength,lpPerIOData->szMessage,offpack.length);                           
  74.             pack=(LPNET_PACK)buffer;  
  75.             //计算CRC校验  
  76.             if(pack->VerifyCRC()){  
  77.                 //拼包成功  
  78.                 DealRecvPack();  
  79.                 //清除掉内存  
  80.                 delete offpack.buff;  
  81.                 //将读取指针移位  
  82.                 nlen=offpack.length;  
  83.                 //将断头包去掉  
  84.                 slist.erase(iter);  
  85.                 //退出循环  
  86.                 break;  
  87.             }else{  
  88.                 //拼包不成功,继续尝试下一个包  
  89.                 cout<<"出现问题"<<endl;  
  90.             }  
  91.             iter++;  
  92.         }  
  93.         //清空缓冲区  
  94.         if(buffer != NULL){  
  95.             delete buffer;  
  96.         }  
  97.           
  98.         //找下一个包头数据,找标识符,packseque校验  
  99.         while(nlen<dwBytesTransferred){  
  100.             pack = (LPNET_PACK)(lpPerIOData->szMessage+nlen);  
  101.             if(pack->m_nSeque == PACK_SEQUE){
    //&&  
  102.                 if(pack->VerifyCRC()){  
  103.                     //下一个正常报,跳出while循环  
  104.                     break;  
  105.                 }  
  106.                 else if(pack->m_nSize>(int)dwBytesTransferred-nlen){  
  107.                     //剩余一个只有包头的包  
  108.                     break;  
  109.                 }  
  110.                 else{  
  111.                     //出现断头断尾包,忽略  
  112.                     cout<<"出现忽略的包"<<endl;  
  113.                 }  
  114.                   
  115.             }  
  116.             nlen++;  
  117.             cout<<"忽略包"<<endl;  
  118.         }     
  119.     }  
  120.   
  121.     while(nlen<(int)dwBytesTransferred){  
  122.         //将数据取出来  
  123.         pack=(LPNET_PACK)(lpPerIOData->szMessage+nlen);  
  124.         //校验CRC,包错误,跳过这个包  
  125.         if(!pack->VerifyCRC()){  
  126.             if((int)dwBytesTransferred-nlen>=8){  
  127.                 nlen += pack->m_nSize;  
  128.                 //将地址指针存入map(map 存放list)  
  129.                 char * buffer = NULL;  
  130.                 for(int newi=0;newi<5;newi++){  
  131.                     try{  
  132.                         buffer = new char[pack->m_nSize];  
  133.                         if(buffer != NULL){  
  134.                             break;  
  135.                         }  
  136.                     }  
  137.                     catch(...){  
  138.                         buffer = NULL;  
  139.                         printf("申请内存失败\n");  
  140.                     }  
  141.                         Sleep(20*(newi+1));  
  142.                 }  
  143.                 if(buffer == NULL){  
  144.                     break;  
  145.                 }  
  146.                 memcpy(buffer,pack,pack->m_nSize-(nlen-(int)dwBytesTransferred));  
  147.                 //加入到断包  
  148.                 //取出来list  
  149.                 list<OFFPACK> slist;  
  150.               
  151.                 //添加入断头包list,呵呵  
  152.                 OFFPACK offpack;  
  153.                 offpack.buff=buffer;  
  154.                 offpack.length=nlen-(int)dwBytesTransferred;  
  155.                 slist.push_back(offpack);  
  156.   
  157.                 map<SOCKET,list<OFFPACK>>::iterator listiter;  
  158.                 listiter=socketlist.find(sClient);  
  159.                 if(listiter!=socketlist.end()){  
  160.                     listiter->second = slist;  
  161.                     //避免下一个包不会出现断头断尾包  
  162.                     Sleep(10);  
  163.                 }else{  
  164.                     //正常情况下不会执行  
  165.                 }  
  166.                 break;  
  167.             }  
  168.             else{
    //(nlen-(int)dwBytesTransferred>0)  
  169.                 //无头包  
  170.                 break;  
  171.             }  
  172.         }  
  173.         else{  
  174.             DealRecvPack(pack,pDbconn,sClient);  
  175.             nlen += pack->m_nSize;  
  176.         }  
  177.   
  178.     }  

这段代码需要放在上一段的31-32行之间。需要的时候进行补充。感觉还是需要根据自己的项目进行自我定制。

你可能感兴趣的文章
[组图]基于Nokia S60的游戏开发之四(转)
查看>>
屡次选错终致项目暴毙(转)
查看>>
WinXP优化 全面消除操作系统的复制乱码(转)
查看>>
symbian 60 开发的一个问题-让左右键都能弹出菜单(转)
查看>>
SQL Server静态页面导出技术(转)
查看>>
黑客软件最常用的连接端口一览(转)
查看>>
解决无盘多机启动慢的方法(转)
查看>>
检查字符串strSource是否为big或big5码(转)
查看>>
EXCEL读取与写入数据的最佳方案(转)
查看>>
windows运行命令详解(转)
查看>>
sql语句插入的数据中含有单引号怎么办(转)
查看>>
RJ45接头接法(转)
查看>>
将数据库的内容放到下拉列表中(转)
查看>>
突破网吧及机房管理限制的方法(转)
查看>>
WAP 2.0--XHTML mobile profile(转)
查看>>
Platform Builder之旅(二)(转)
查看>>
GFP:新一代多业务传输技术(转)
查看>>
安全至上:7月11日值得注意病毒列表(转)
查看>>
How to Use DBMS_SUPPORT Package(转)
查看>>
在Win2003中配置SNMP服务的网络安全(转)
查看>>