多线程下的神奇的IOCP
https://blog.csdn.net/lijia626482312/article/details/40858061
一个人从接到项目到昨天终于完成,用了差不多4个月,其中各种心酸和眼泪。我的项目是通过网络从客户端上采集数据,通讯原则是客户端有数据要上传,如果网络允许就连接服务器,首先客户端发送一个消息判断服务器是不是处于忙碌和资源空闲状态,然后发送文件等等。可以说是一个基于C/S模式的多线程socket程序。
我刚开始那到这个项目,我们经理把项目给我一看,你一个人可以做的出来吗,我一看,额。。。。。很简单吗!must,领导要求,必须做出来啊,从此在这个项目陷进去了。。。。。。
由于以前没有做过网络方面的程序,我的开始是从传统模型开始的,就是accept一个连接就开启一个线程,这也是我那到这个项目,觉得简单的地方,很快我就实现了这个问题,我通过连接几台客户端,简单的测试一下,OK,可以正常的采集,基本的功能都可以实现。那好,我把项目一个技术经理,经理一看,小李,你效率挺快的吗,OK,你做个压力测试一下。然后我立马写了一个测试程序,写完后,我自信满满的拿给测试部门测试,我碰到了这个项目的第一个方向问题。测试部门的主管跟我说,客户端连接服务器,30、40台客户端,可以正常运行,客户端数量再往上的话,就比较容易出现乱码(也就是乱序),这个程序拿给用户不行,这是砸我们公司的招牌,我当时就懵了,怎么可能,代码没有问题啊!我回去一查,可能是粘包,我的处理方案是精简代码结构!写好后,我自己模拟用100台,测试了一晚上,没有问题,应该没有问题了吧,然后我又自信满满的拿到测试部门,我又懵了,(⊙o⊙)…,又不行,上面的问题,还是出现了,我想这是人品问题啊,怎么可能,然后在这个问题上,我纠结了差不多快两个月,代码再怎么优化都有问题,我都准备彻底放弃了!!!
一次偶然的机会在网上看到windows操作系统有六种模型:select模型,WSAAsyncSelect异步模型,WSAEventSelect事件模型,重叠I/O事件模型,重叠I/O完成例程,IOCP完成端口。我一个一个模型差不多都用了一遍,好像都不好用,前面4种,有windows平台数量限制(64台客户端),我一看这不行,那就用IOCP吧!
iocp步骤是:
1、创建工作在线程;
SYSTEM_INFO sysinfo;
GetSysteminfo(&sysinfo);
int threadcount = sysinfo.sysinfo.dwNumberOfProcessors*2;
for (DWORD i = 0;i<sysinfo.dwNumberOfProcessors*2;i++)
{
HANDLE m_pWorkThread;
m_pWorkThread = CreateThread(NULL,0,ServerWorkerThread,m_ComletionPort,0,NULL);
CloseHandle(m_pWorkThread);
}
2、创建socket,不同传统模型,m_listenSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
3、绑定socket,CreateIoCompletionPort((HANDLE)m_listenSock,m_ComletionPort,0,0);
4、传统模式的bind,listen;
5、accept(acceptex更高效),将iocp绑定客户端socket
SOCKADDR_IN saRemote;
int RemoteLen=sizeof(SOCKADDR_IN);
SOCKET Accept = INVALID_SOCKET;
Accept = WSAAccept(pDlg->m_listenSock,(SOCKADDR*)&saRemote, &RemoteLen,NULL,0));
PerHandleData[index].Socket = Accept;
memcpy(&PerHandleData[index].ClientAddr,&saRemote,RemoteLen);
HANDLE m_retcompletion = CreateIoCompletionPort((HANDLE)PerHandleData[index].Socket,m_ComletionPort,(DWORD)&PerHandleData[index],0);
6、绑定iocp成功后,投递接收请求WSARecv(PerHandleData[index].Socket, &(PerIoData[index].DataBuf), 1, &RecvBytes, &Flags,&(PerIoData[index].Overlapped), NULL);
以上就是iocp的大概步骤,我使用上面的模型创建好iocp模型后,发现确实好用不少,3Mb的短连接数据传输,接收速度比传统模型几乎快了十几秒。
然后我又进行了模拟100台客户端压力测试,发现运行了10几分钟系统出现崩溃,错误提示是:Debug Assertion Failed!File:Dbgheap.c Line:1011,然后我这个问题上又堵了好几天,这是什么情况呢,程序的逻辑是收完一段数据,给变一个标志位,然后通过检索这个标志位,在另外的线程处理,逻辑没有问题啊,原来windows多线程开发是线程不安全的,多线程中共享数据的访问,都要加锁。不然的话,出现内存溢出,数组越界等等奇怪的问题。
还有iocp如何区分每一个socket,我是采用检索全局socket,GetQueuedCompletionStatus被激活后,将接收到的数据放到各自的全局变量区域。全局变量怎么申请呢,如果连接数量够多,申请固定大小的全局内存空间,在程序启动的时候会出现内存不足,解决办法就是在堆中申请内存(也就是malloc,注意malloc之后别忘记free,如果频繁的申请释放内存空间,容易出现磁盘碎片,不鼓励使用这种办法)。
投递多个wsarecv有必要吗,投递多个能充分的使用cpu,这个不可否认,但是投递之后我们需要花更多的内存处理时间去组包和处理数据,如何数据还没有收完的话,还需要进一步的投递多个包,然后再组包和处理数据。。。。。。这样的效率更慢,我觉得一来一回(接收数据先判断自己的数据是否收满,没有收满再投递一个wsarecv)的方式已经可以满足自己的需要了,但是一来一回的处理方式,需要注意,GetQueuedCompletionStatus所在的工作着线程最好是不要做过多的数据处理工作,以免影响客户端发送的太快,服务器接收的太慢(如果出现这个问题,容易引起远程主机主动关闭socket错误)。
iocp中是如何进行超时判断的?服务器资源一定,客户端如果一直连接而不发送数据或者出现各种意外客户端断开连接服务器而没有及时发现,服务器会出现资源不够用的情况,这时就要超时判断。工作者线程中记录wsarecv和WSASend操作的时间,然后再另外开一个线程判断上一次的处理wsarecv和wsasend操作跟当前时间是否超时(可以根据自己需求设定,mfc中可以使用CTimespan::GetTotalSeconds()取得时间差),如果超时就关闭socket,释放相应的内存等等。
与IO打交道,估计是所有iocp的难题,据相关资料记载(我也不知道是在哪里看到的,IO外设的任何操作速度相对cpu处理速度来说,cpu开了1000多倍),而iocp工作者线程池设计目的为了充分发挥cpu的性能,所以工作者线程中最好不要处理IO(这就出现一个问题,空间换时间,内存不够使用)。
工作者不处理IO,那就带来了内存的使用问题,IO处理慢,未处理的IO比较多,未处理内存积压越来越多,内存不够用。32位操作系统,内存使用机制,内核默认2G内存,程序员自己可用2G。可以通过设置c盘的boot.ini隐藏文件设置3GB程序员可用内存(具体方法可自己上网搜索)。
昨天终于写好iocp服务器程序,在内存大小限制情况,短连接,每个连接都只上传6MB以内的数据,能跑500台端机,不过IO处理是关键,如果服务器忙碌的时候,在服务器上做其他操作如刷屏、打印、键盘输入数据等,会出现写文件很慢,内存很容易出现不够用的情况,不过,这已经能满足我的性能需求了。
终于能长长的松一口气。。。。。。好开心啊!