我们前面接触过几个高效的unix/linux的异步IO模型:select,poll,epoll,kqueue,其实windows也有它的异步模型,比如windows版本的select,当然最高效的还属IOCP吧。我也没有做过多少windows的网络编程,但是看到网上不少人拿IOCP与epoll模型做对比,觉得必定不简单,忍不住试试。IOCP的原理大家多百度百度吧,我也还没弄清楚,呵呵。
大致核心原理是:我们不停地发出异步的WSASend/WSARecv IO操作,具体的IO处理过程由Windows系统完成,Windows系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断取出IO操作结果,然后根据需要再发出WSASend/WSARecv IO操作。IO操作都放心地交给操作系统,我们只需要关注业务逻辑代码与监听完成端口的代码。下面是个简单的并发TCP服务器程序示例(程序下载地址:点击打开链接):
服务器端代码
#include<WinSock2.h> #include<stdio.h> #include<Windows.h> #pragma comment(lib,"ws2_32.lib") #define PORT 1987 //监听端口 #define BUFSIZE 1024 //数据缓冲区大小 typedef struct { SOCKET s; //套接字句柄 sockaddr_in addr; //对方的地址 } PER_HANDLE_DATA, *PPER_HANDLE_DATA; typedef struct { OVERLAPPED ol; //重叠结构 char buf[BUFSIZE]; //数据缓冲区 int nOperationType; //操作类型 } PER_IO_DATA, *PPER_IO_DATA; //自定义关注事件 #define OP_READ 100 #define OP_WRITE 200 DWORD WINAPI WorkerThread(LPVOID ComPortID) { HANDLE cp = (HANDLE)ComPortID; DWORD btf; PPER_IO_DATA pid; PPER_HANDLE_DATA phd; DWORD SBytes, RBytes; DWORD Flags; while(true) { //关联到完成端口的所有套接口等待IO准备好 if(GetQueuedCompletionStatus(cp, &btf, (LPDWORD)&phd, (LPOVERLAPPED *)&pid, WSA_INFINITE) == 0){ return 0; } //当客户端关闭时会触发 if(0 == btf && (pid->nOperationType == OP_READ || pid->nOperationType == OP_WRITE)) { closesocket(phd->s); GlobalFree(pid); GlobalFree(phd); printf("client closed\n"); continue; } WSABUF buf; //判断IO端口的当前触发事件(读入or写出) switch(pid->nOperationType){ case OP_READ: pid->buf[btf] = '\0'; printf("Recv: %s\n", pid->buf); char sendbuf[BUFSIZE]; sprintf(sendbuf,"Server Got \"%s\" from you",pid->buf); //继续投递写出的操作 buf.buf = sendbuf; buf.len = strlen(sendbuf)+1; pid->nOperationType = OP_WRITE; SBytes = 0; //让操作系统异步输出吧 WSASend(phd->s, &buf, 1, &SBytes, 0, &pid->ol, NULL); break; case OP_WRITE: pid->buf[btf] = '\0'; printf("Send: Server Got \"%s\"\n\n", pid->buf); //继续投递读入的操作 buf.buf = pid->buf; buf.len = BUFSIZE; pid->nOperationType = OP_READ; RBytes = 0; Flags = 0; //让底层线程池异步读入吧 WSARecv(phd->s, &buf, 1, &RBytes, &Flags, &pid->ol, NULL); break; } } return 0; } void main() { WSADATA wsaData; /* * 加载指定版本的socket库文件 */ WSAStartup( MAKEWORD( 2, 2 ), &wsaData ); printf("server start running\n"); //创建一个IO完成端口 HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); //创建一个工作线程,传递完成端口 CreateThread(NULL, 0, WorkerThread, completionPort, 0, 0); /* * 初始化网络套接口 */ SOCKET sockSrv=socket(AF_INET,SOCK_STREAM,0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(PORT); bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR)); listen(sockSrv,5); /* * 等待通信 */ while (1) { SOCKADDR_IN addrClient; int len=sizeof(SOCKADDR); SOCKET sockConn=accept(sockSrv,(SOCKADDR*)&addrClient,&len); //为新连接创建一个handle,关联到完成端口对象 PPER_HANDLE_DATA phd = (PPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); phd->s = sockConn; memcpy(&phd->addr, &addrClient, len); CreateIoCompletionPort((HANDLE)phd->s, completionPort,(DWORD)phd, 0); //分配读写 PPER_IO_DATA pid = (PPER_IO_DATA)GlobalAlloc(GPTR, sizeof(PER_IO_DATA)); ZeroMemory(&(pid->ol), sizeof(OVERLAPPED)); //初次投递读入的操作,让操作系统的线程池去关注IO端口的数据接收吧 pid->nOperationType = OP_READ; WSABUF buf; buf.buf = pid->buf; buf.len = BUFSIZE; DWORD dRecv = 0; DWORD dFlag = 0; //一般服务器都是被动接受客户端连接,所以只需要异步Recv即可 WSARecv(phd->s, &buf, 1, &dRecv, &dFlag, &pid->ol, NULL); } }
客户端代码
#include<WinSock2.h> #include<stdio.h> #pragma comment(lib,"ws2_32.lib") void main() { WORD wVersionRequested; WSADATA wsaData; int err; /* * 加载指定版本的socket库文件 */ wVersionRequested = MAKEWORD( 2, 2 ); err = WSAStartup( wVersionRequested, &wsaData ); if ( err != 0 ) { return; } /* * 初始化网络套接口 */ SOCKET sockClient=socket(AF_INET,SOCK_STREAM,0); SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1"); addrSrv.sin_family=AF_INET; addrSrv.sin_port=htons(1987); connect(sockClient,(SOCKADDR *)&addrSrv,sizeof(SOCKADDR)); /* * 通信 */ char recvBuffer[100]; //发送第一段消息 printf("Send: %s\n","This is Kary"); send(sockClient,"This is Kary",strlen("This is Kary")+1,0); recv(sockClient,recvBuffer,100,0); printf("Get: %s\n",recvBuffer); printf("\n"); //发送第二段消息 printf("Send: %s\n","Nice to meet you"); send(sockClient,"Nice to meet you",strlen("Nice to meet you")+1,0); recv(sockClient,recvBuffer,100,0); printf("Get: %s\n",recvBuffer); closesocket(sockClient); WSACleanup(); system("pause"); }
分别运行服务器端代码与客户端代码可以看到如下结果:
客户端命令行
Send: This is Kary Get: Server Got "This is Kary" from you Send: Nice to meet you Get: Server Got "Nice to meet you" from you 请按任意键继续. . .服务器端命令行
server start running Recv: This is Kary Send: Server Got "This is Kary" Recv: Nice to meet you Send: Server Got "Nice to meet you" client closed从上面繁杂的代码还是能感觉到windows网络编程是件多么苦逼的事情啊,IOCP虽然用到了windows底层的线程池专门去做异步IO,但是感觉远没有epoll编程直观,编码难度提升不少。不止是C语言,另外C#等基于.net框架的语言也有IOCP的接口支持,我没有尝试过。
IOCP的编码流程大致如下:
1,创建一个完成端口
2,创建一个工作线程
3,工作线程循环调用GetQueuedCompletionStatus()函数得到IO操作结果,这个函数是阻塞函数。
4,工作线程循环调用accept函数等待客户端连接
5,工作线程获取到新连接的套接字句柄用CreateIoCompletionPort函数关联到第一步创建的完成端口,然后发出一个异步的WSASend或者WSARecv调用,因为是异步调用,会立马返回,实际的发送接收数据操作由Windows系统去做。
6,工作线程继续下一次循环,阻塞accept等待客户端连接
7,Windows系统完成WSASend或者WSARecv的操作,把结果发到完成端口。
8,主线程的GetQueuedCompletionStatus马上返回,并从完成端口取得刚刚完成的WSASend或WSARecv的结果。
9,在主线程里对这些数据进行处理。(如果处理很耗时,需要新开线程处理),然后接着发WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus这里。