SOCKET编程登峰造极之完成端口
一、什么是完成端口?
完成端口---是一种WINDOWS内核对象。完成端口用于异步方式的重叠I/0情况下,当然重叠I/O不一定非使用完成端口不可,还有设备内核对象、事件对象、告警I/0等。但是完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。
二、完成端口的内部机制
1)创建完成端口
完成端口是一个内核对象,使用时他总是要和至少一个有效的设备句柄进行关联,完成端口是一个复杂的内核对象,创建它的函数是:
显示代码打印1 HANDLE CreateIoCompletionPort(
2 IN HANDLE FileHandle,
3 IN HANDLE ExistingCompletionPort,
4 IN ULONG_PTR CompletionKey,
5 IN DWORD NumberOfConcurrentThreads
6 );
通常创建工作分两步:
第一步,创建一个新的完成端口内核对象,可以使用下面的函数:
显示代码打印1 HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads)
2 {
3 return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads);
4 };
第二步,将刚创建的完成端口和一个有效的设备句柄关联起来,可以使用下面的函数:
显示代码打印1 bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey)
2 {
3 HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0);
4 return h==hCompPort;
5 };
说明
a)CreateIoCompletionPort函数也可以一次性的既创建完成端口对象,又关联到一个有效的设备句柄
b)CompletionKey是一个可以自己定义的参数,我们可以把一个结构的地址赋给它,然后在合适的时候取出来使用,最好要保证结构里面的内存不是分配在栈上,除非你有十分的把握内存会保留到你要使用的那一刻。
c)NumberOfConcurrentThreads通常用来指定要允许同时运行的的线程的最大个数。通常我们指定为0,这样系统会根据CPU的个数来自动确定。
创建和关联的动作完成后,系统会将完成端口关联的设备句柄、完成键作为一条纪录加入到这个完成端口的设备列表中。如果你有多个完成端口,就会有多个对应的设备列表。如果设备句柄被关闭,则表中自动删除该纪录。
2)完成端口线程的工作原理
完成端口可以帮助我们管理线程池,但是线程池中的线程需要我们使用_beginthreadex来创建,凭什么通知完成端口管理我们的新线程呢?答案在函数GetQueuedCompletionStatus。该函数原型:
显示代码打印1 BOOL GetQueuedCompletionStatus(
2 IN HANDLE CompletionPort,
3 OUT LPDWORD lpNumberOfBytesTransferred,
4 OUT PULONG_PTR lpCompletionKey,
5 OUT LPOVERLAPPED *lpOverlapped,
6 IN DWORD dwMilliseconds
7 );
这个函数试图从指定的完成端口的I/0完成队列中抽取纪录。只有当重叠I/O动作完成的时候,完成队列中才有纪录。凡是调用这个函数的线程将被放入到完成端口的等待线程队列中,因此完成端口就可以在自己的线程池中帮助我们维护这个线程。
完成端口的I/0完成队列中存放了当重叠I/0完成的结果---- 一条纪录,该纪录拥有四个字段,前三项就对应GetQueuedCompletionStatus函数的2、3、4参数,最后一个字段是错误信息dwError。我们也可以通过调用PostQueudCompletionStatus模拟完成了一个重叠I/0操作。
当I/0完成队列中出现了纪录,完成端口将会检查等待线程队列,该队列中的线程都是通过调用GetQueuedCompletionStatus函数使自己加入队列的。等待线程队列很简单,只是保存了这些线程的ID。完成端口会按照后进先出的原则将一个线程队列的ID放入到释放线程列表中,同时该线程将从等待GetQueuedCompletionStatus函数返回的睡眠状态中变为可调度状态等待CPU的调度。
基本上情况就是如此,所以我们的线程要想成为完成端口管理的线程,就必须要调用
GetQueuedCompletionStatus函数。出于性能的优化,实际上完成端口还维护了一个暂停线程列表,具体细节可以参考《Windows高级编程指南》,我们现在知道的知识,已经足够了。
写了一下午,终于写完了这个“完成端口”。
到今天为止,写完了Overlapped I\O Event、Overlapped I\O completion Routine和completion Port。一路写过来的确学到了不少东西,也清楚地看到到微软在遇到问题并解决问题的方法;不得不承认,微软~还是很强的。呵呵~
这也让我明白一件事:遇到困难,不要望而却步;只要你勇于探索,一切都将是那么简单。(听起来有点自恋的感觉^_^)
“完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I/O请求提供服务(Web服务器便是这方面的典型例子),那么I/O完成端口模型便是最佳选择!
我们基本上按下述步骤行事:
1) 创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作者线程。
2) 判断系统内到底安装了多少个处理器。
3) 创建工作者线程,根据步骤2)得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务。在这个简单的例子中,我们为每个处理器都只创建一个工作者线程。这是由于事先已预计到,到时不会有任何线程进入“挂起”状态,造成由于线程数量的不足,而使处理器空闲的局面(没有足够的线程可供执行)。调用CreateThread函数时,必须同时提供一个工作者例程,由线程在创建好执行。本节稍后还会详细讨论线程的职责。
4) 准备好一个监听套接字,在端口1234上监听进入的连接请求。
5) 使用accept函数,接受进入的连接请求。
6) 创建一个数据结构,同时在结构中存入接受的套接字句柄。
7) 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort。
8) 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,一个工作者线程会为I/O请求提供服务,同时继续处理未来的I/O请求,稍后便会在步骤3)指定的工作者例程
中,体验到这一点。
9) 重复步骤5) ~ 8),直至服务器中止。
源码---------------------------------------------------------------------------------------------
显示代码打印001 #pragma comment(lib,"ws2_32.lib")
002 #include <winsock2.h>
003 #include <stdio.h>
004 //////////////////////////////////////////////////////////////////////////
005 //仅供测试软件用
006 #include "Protocol.h"
007
008 #define DATA_BUFSIZE 1024 // 接收缓冲区大小
009 typedef enum{ IOSEND,IORECV,IOQUIT } IO_TYPE;
010 typedef struct _SOCKET_INFORMATION {
011 OVERLAPPED Overlapped;
012 SOCKET Socket;
013 IO_TYPE IoType;
014 char buffer[DATA_BUFSIZE];
015 WSABUF DataBuf;
016 DWORD BytesSEND;
017 DWORD BytesRECV;
018 } SOCKET_INFORMATION, * LPSOCKET_INFORMATION;
019 DWORD Flags = 0,
020 Bytes = 0;
021 DWORD WINAPI WorkThread(LPVOID CompletionPortID);
022 DWORD WINAPI AcceptThread(LPVOID lpParameter)
023 {
024 WSADATA wsaData;
025 HANDLE hCompPort;
026 DWORD ThreadID;
027 DWORD Ret;
028 if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
029 {
030 printf("WSAStartup failed with error %d\n", Ret);
031 return FALSE;
032 }
033 if ((hCompPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
034 {
035 printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError());
036 return FALSE;
037 }
038 //根据CPU个数来创建线程,以达到最佳性能
039 SYSTEM_INFO SystemInfo;
040 GetSystemInfo(&SystemInfo);
041 for(unsigned int i=0; i<SystemInfo.dwNumberOfProcessors*2; i++)
042 {
043 HANDLE ThreadHandle;
044 if ((ThreadHandle = CreateThread(NULL, 0, WorkThread, hCompPort, 0, &ThreadID)) == NULL)
045 {
046 printf("CreateThread() failed with error %d\n", GetLastError());
047 return FALSE;
048 }
049 CloseHandle(ThreadHandle);
050 }
051 SOCKET ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, NULL, WSA_FLAG_OVERLAPPED);
052 SOCKADDR_IN ServerAddr;
053 ServerAddr.sin_family = AF_INET;
054 ServerAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
055 ServerAddr.sin_port = htons(1234);
056 bind(ListenSocket,(LPSOCKADDR)&ServerAddr,sizeof(ServerAddr));
057 listen(ListenSocket,100);
058 printf("listenning...\n");
059 SOCKADDR_IN ClientAddr;
060 int addr_length=sizeof(ClientAddr);
061 while (TRUE)
062 {
063 LPSOCKET_INFORMATION SI = new SOCKET_INFORMATION;
064 if ((SI->Socket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length)) != INVALID_SOCKET)
065 {
066 printf("accept ip:%s port:%d\n",inet_ntoa(ClientAddr.sin_addr),ClientAddr.sin_port);
067 //相关参数初始化
068 memset(&SI->Overlapped,0,sizeof(WSAOVERLAPPED));
069 memset(SI->buffer, 0, DATA_BUFSIZE);
070 SI->DataBuf.buf = SI->buffer;
071 SI->DataBuf.len = DATA_BUFSIZE;
072 SI->BytesRECV = 0;
073 SI->BytesSEND = 0;
074 SI->IoType = IORECV;
075 //////////////////////////////////////////////////////////////////////////
076 //仅供测试软件用
077 HeaderMessage recvMsg;
078 if (recv(SI->Socket, (char*)&recvMsg, sizeof(recvMsg), 0) <= 0)
079 {
080 printf("初始参数交互失败");
081 }
082 if (CreateIoCompletionPort((HANDLE)SI->Socket, hCompPort, (DWORD)SI, 0) == NULL)
083 {
084 printf("CreateIoCompletionPort failed with error %d\n", GetLastError());
085 return FALSE;
086 }
087 //发出一个重叠I\O请求
088 if(WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
089 {
090 if(WSAGetLastError() != WSA_IO_PENDING)
091 {
092 printf("disconnect\n");
093 closesocket(SI->Socket);
094 delete SI;
095 continue;
096 }
097 }
098 }
099
100 }
101 return FALSE;
102 }
103 DWORD WINAPI WorkThread(LPVOID CompletionPortID)
104 {
105 HANDLE hCompPort = (HANDLE)CompletionPortID;
106 while (TRUE)
107 {
108 DWORD BytesTransferred = 0;
109 LPSOCKET_INFORMATION SI = NULL;
110 LPWSAOVERLAPPED Overlapped = NULL;
111 //线程进入线程池,等待被唤醒
112 if (GetQueuedCompletionStatus(hCompPort, &BytesTransferred, (LPDWORD)&SI, &Overlapped, INFINITE))
113 {
114 if (0 == BytesTransferred && IOQUIT != SI->IoType)
115 {
116 printf("disconnect\n");
117 closesocket(SI->Socket);
118 delete SI;
119 continue;
120 }
121 switch(SI->IoType)
122 {
123 case IORECV:
124 {
125 //目前的功能是将接收到的数据原封不动的返回
126 SI->DataBuf.len = BytesTransferred;
127 SI->BytesRECV = BytesTransferred;
128 SI->IoType = IOSEND;
129 if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
130 {
131 if(WSAGetLastError() != WSA_IO_PENDING)
132 {
133 printf("disconnect\n");
134 closesocket(SI->Socket);
135 delete SI;
136 continue;
137 }
138 }
139 break;
140 }
141 case IOSEND:
142 {
143 SI->BytesSEND += BytesTransferred;
144 //返回是否彻底,若未发完,接着发
145 if (SI->BytesSEND < SI->BytesRECV)
146 {
147 SI->DataBuf.buf += BytesTransferred;
148 SI->DataBuf.len -= BytesTransferred;
149 SI->IoType = IOSEND;
150 if (WSASend(SI->Socket, &SI->DataBuf, 1, &Bytes, Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
151 {
152 if(WSAGetLastError() != WSA_IO_PENDING)
153 {
154 printf("disconnect\n");
155 closesocket(SI->Socket);
156 delete SI;
157 continue;
158 }
159 }
160 }
161 else if (SI->BytesSEND > SI->BytesRECV)
162 {
163 printf("BytesSEND:%d > BytesRECV:%d\n",SI->BytesSEND,SI->BytesRECV);
164 memset(SI->buffer, 0, DATA_BUFSIZE);
165 SI->BytesRECV = 0;
166 SI->BytesSEND = 0;
167 SI->IoType = IORECV;
168 SI->DataBuf.len = DATA_BUFSIZE;
169 SI->DataBuf.buf = SI->buffer;
170 }
171 else
172 {
173 memset(SI->buffer, 0, DATA_BUFSIZE);
174 SI->BytesRECV = 0;
175 SI->BytesSEND = 0;
176 SI->IoType = IORECV;
177 SI->DataBuf.len = DATA_BUFSIZE;
178 SI->DataBuf.buf = SI->buffer;
179 if (WSARecv(SI->Socket, &SI->DataBuf, 1, &Bytes, &Flags, &SI->Overlapped, NULL) == SOCKET_ERROR)
180 {
181 if(WSAGetLastError() != WSA_IO_PENDING)
182 {
183 printf("disconnect\n");
184 closesocket(SI->Socket);
185 delete SI;
186 continue;
187 }
188 }
189 }
190 break;
191 }
192 case IOQUIT:
193 {
194 //让线程安全退出
195 return FALSE;
196 break;
197 }
198
199 default:
200 break;
201 }
202 }
203 }
204 return FALSE;
205 }
206 void main()
207 {
208 HANDLE hThreads = CreateThread(NULL, 0, AcceptThread, NULL, NULL, NULL);
209
210 WaitForSingleObject(hThreads,INFINITE);
211 printf("exit\n");
212 CloseHandle(hThreads);
213 }
文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/3_program/c++/cppjs/2007927/74932.html