socket通信之七:Overlapped I/O 完成例程模型实现的客户/服务器模型

前一篇介绍了重叠IO的一种实现方式即基于事件通知的方式,这一篇介绍另外一种方式,即使用完成例程的方式实现重叠IO,首先声明这种方式比事件通知的方式简单多了。


用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中,主线程只用不停的接受连接即可;辅助线程判断有没有新的客户端连接被建立,如果有,就为那个客户端套接字激活一个异步的WSARecv操作,然后调用SleepEx使线程处于一种可警告的等待状态,(使辅助线程主动的放弃一段时间的CPU使用权,以防止一直处于阻塞状态)以使得I/O完成后CompletionROUTINE可以被内核调用。如果辅助线程不调用SleepEx,则内核在完成一次I/O操作后,无法调用完成例程(因为完成例程的运行应该和当初激活WSARecv异步操作的代码在同一个线程之内)。当重叠IO操作完成,SleepEx会被完成的消息唤醒,从而使得辅助线程中的完成例程得到执行。

下面是SleepEx函数的说明:

The SleepEx function causes the current thread to enter a wait state until one of the following occurs: 

1)An I/O completion callback function is called 

2)An asynchronous procedure call (APC) is queued to the thread. 

3)The time-out interval elapses


完成例程内的实现代码比较简单,它取出接收到的数据,然后将数据原封不动的发送给客户端,最后重新激活另一个WSARecv异步操作。注意,在这里用到了“尾随数据”。我们在调用WSARecv的时候,参数lpOverlapped实际上指向一个比它大得多的结构PerSocketData,这个结构除了WSAOVERLAPPED以外,还被我们附加了缓冲区的结构信息,另外还包括客户端套接字等重要的信息。这样,在完成例程中通过参数lpOverlapped拿到的不仅仅是WSAOVERLAPPED结构,还有后边尾随的包含客户端套接字和接收数据缓冲区等重要信息。


下面是服务器端的代码,依然延续前面的编程风格。服务端的代码只在第6步进行了更改,并且在主线程中启动了一个新的辅助线程workThread,这个辅助线程用来监视客户端的连接,并且为每一个客户端的连接激活WSARecv异步调用,并提供一个完成例程函数completionRoutine,这个完成例程函数completionRoutine会在WSARecv返回时被调用。客户端的代码和前面一样,没发生变化。


#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <iostream>

#pragma comment(lib, "ws2_32.lib")


using namespace std;

#define  PORT 6000
//#define  IP_ADDRESS "10.11.163.113"  //表示服务器端的地址
#define  IP_ADDRESS "127.0.0.1"  //直接使用本机地址

#define MSGSIZE 1024

void CALLBACK completionRoutine(DWORD dwError,DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped,DWORD dwFlags);
DWORD WINAPI workThread(LPVOID lpParam);

SOCKET newClientConn;
bool bNewClientArrive=false;

//与重叠IO结构相关的一些信息,把它们封装在一个结构体中方便管理
class PerSocketData
{
public:
	WSAOVERLAPPED overlap;//每一个socket连接需要关联一个WSAOVERLAPPED对象
	WSABUF buffer;//与WSAOVERLAPPED对象绑定的缓冲区
	char          szMessage[MSGSIZE];//初始化buffer的缓冲区
	DWORD          NumberOfBytesRecvd;//指定接收到的字符的数目
	DWORD          flags;
	SOCKET socket;
};

//使用这个工作线程来通过重叠IO的方式与客户端通信
DWORD WINAPI workThread(LPVOID lpParam)
{
		PerSocketData * lpIOdata=NULL;

		while(true)
		{
			//判断有没有新的客户端连接建立,如果有,就需要为这个客户端建立
			//一个异步的WSARecv操作
			if (bNewClientArrive)
			{
				//在新的连接中建立一个异步的操作
				lpIOdata=(PerSocketData*)HeapAlloc(
					GetProcessHeap(),
					HEAP_ZERO_MEMORY,
					sizeof(PerSocketData));
				lpIOdata->buffer.len=MSGSIZE;
				lpIOdata->buffer.buf=lpIOdata->szMessage;
				lpIOdata->socket=newClientConn;

				WSARecv(lpIOdata->socket,
					&lpIOdata->buffer,
					1,
					&lpIOdata->NumberOfBytesRecvd,
					&lpIOdata->flags,
					&lpIOdata->overlap,
					completionRoutine);//这里向系统登记一个回调函数,接收数据完成时会被调用

				bNewClientArrive=false;//处理完这个连接后将当前连接重置为false,等待下一个连接的到来

			}

			SleepEx(1000,true);//这一行代码是必须的,使上面的completionRoutine能在当前的上下文中调用
		}

		return 0;
}

//可以在msdn中查看WSARecv中最后一个函数也就是完成例程completionRoutine的原型
void CALLBACK completionRoutine(DWORD dwError,DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped,DWORD dwFlags)
{
	//注意这里将WSAOVERLAPPED类型的指针转换成了PerSocketData类型的指针
	PerSocketData * lpIOdata=(PerSocketData*)(lpOverlapped);

	//下面表示客户端断开了连接
	if (dwError!=0||cbTransferred==0)
	{
		cout<<"客户端断开连接"<<endl;
		closesocket(lpIOdata->socket);
		HeapFree(GetProcessHeap(),0,lpIOdata);
	}
	else
	{
		cout<<lpIOdata->szMessage<<endl;

		send(lpIOdata->socket,lpIOdata->szMessage,cbTransferred+1,0);//多发送一个字符,将字符串结束符也发送过去

		memset(&lpIOdata->overlap,0,sizeof(WSAOVERLAPPED));
		lpIOdata->buffer.len=MSGSIZE;
		lpIOdata->buffer.buf=lpIOdata->szMessage;

		WSARecv(lpIOdata->socket,
			&lpIOdata->buffer,
			1,
			&lpIOdata->NumberOfBytesRecvd,
			&lpIOdata->flags,
			&lpIOdata->overlap,
			completionRoutine);
	}
}

void main()
{
	
	WSADATA wsaData;
	int err;

	//1.加载套接字库
	err=WSAStartup(MAKEWORD(1,1),&wsaData);
	if (err!=0)
	{
		cout<<"Init Windows Socket Failed::"<<GetLastError()<<endl;
		return ;
	}

	//2.创建socket
	//套接字描述符,SOCKET实际上是unsigned int
	SOCKET serverSocket;
	serverSocket=socket(AF_INET,SOCK_STREAM,0);
	if (serverSocket==INVALID_SOCKET)
	{
		cout<<"Create Socket Failed::"<<GetLastError()<<endl;
		return ;
	}


	//服务器端的地址和端口号
	struct sockaddr_in serverAddr,clientAdd;
	serverAddr.sin_addr.s_addr=inet_addr(IP_ADDRESS);
	serverAddr.sin_family=AF_INET;
	serverAddr.sin_port=htons(PORT);

	//3.绑定Socket,将Socket与某个协议的某个地址绑定
	err=bind(serverSocket,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
	if (err!=0)
	{
		cout<<"Bind Socket Failed::"<<GetLastError()<<endl;
		return ;
	}


	//4.监听,将套接字由默认的主动套接字转换成被动套接字
	err=listen(serverSocket,10);
	if (err!=0)
	{
		cout<<"listen Socket Failed::"<<GetLastError()<<endl;
		return ;
	}

	cout<<"服务器端已启动......"<<endl;

	int addrLen=sizeof(clientAdd);
	HANDLE hThread=CreateThread(NULL,0,workThread,NULL,0,NULL);
	if (hThread==NULL)
	{
		cout<<"Create Thread Failed!"<<endl;
	}
	CloseHandle(hThread);

	while(true)
	{
		//5.接收请求,当收到请求后,会将客户端的信息存入clientAdd这个结构体中,并返回描述这个TCP连接的Socket
		newClientConn=accept(serverSocket,(struct sockaddr*)&clientAdd,&addrLen);
		if (newClientConn==INVALID_SOCKET)
		{
			cout<<"Accpet Failed::"<<GetLastError()<<endl;
			return ;
		}
		cout<<"客户端连接:"<<inet_ntoa(clientAdd.sin_addr)<<":"<<clientAdd.sin_port<<endl;
		
		//将之前的第6步替换成了上面启动workThread这个线程函数和下面这一行代码
		//在线程函数workThread中会持续对这个变量进行判断
		bNewClientArrive=true;

	}

	closesocket(serverSocket);
	//7.清理Windows Socket库
	WSACleanup();
}


下面是执行输出:


可执行文件可以在这里下载,工程文件可以在这里下载。


参考:

Overlapped I/O模型--完成例程

Windows Socket五种I/O模型




阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页