现在的位置: 首页 > 综合 > 正文

用IOCP实现个简易TCP并发服务器

2013年07月04日 ⁄ 综合 ⁄ 共 4855字 ⁄ 字号 评论关闭

我们前面接触过几个高效的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这里。

抱歉!评论已关闭.