1. 程式人生 > >用IOCP實現個簡易TCP併發伺服器

用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這裡。