用IOCP實現個簡易TCP併發伺服器
我們前面接觸過幾個高效的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這裡。