Winsock 套接字非阻塞模式小例程 WinSock IO模型

Winsock I/O模型:

1.阻塞/非阻塞套接字

2.Select模型

3.WSAAsyncSelect模型

4.WSAEventSelect模型

5.重叠I/O模型

6.完成端口模型


1.Winsock非阻塞模式,小并发,循环服务器例子

服务器端使用非阻塞套接字,允许有多个客户接入。客户端使用阻塞套接字。

服务器端循环轮询方式。

	/***设套接字为非阻塞模式***/
	unsigned long ul = 1;	
	int	nRet=ioctlsocket(sock_server, FIONBIO, &ul); //设置套接字非阻塞模式
	if (nRet == SOCKET_ERROR)
	{
		cout << "ioctlsocket failed! error:" << WSAGetLastError() << endl;
		closesocket(sock_server);
		WSACleanup();
		return 0;
	}

新创建的套接字默认是阻塞模式。用ioctlsocket()函数将其设置为非阻塞模式。

对流式套接字而言,当监听套接字被设置为非阻塞模式后,则accept()函数从该套接字上返回的已连接套接字也将是非阻塞模式。

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
ioctlsocket(
    _In_ SOCKET s,    //s要设置的套接字
    _In_ long cmd,    //用于设置套接字的命令
    _Inout_ u_long FAR * argp    //指向cmd所需参数的指针
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

ioctlsocket()函数执行成功返回0,否则返回SOCKET_ERROR,错误代码可调用WSAGetLastError()获得!

server.cpp

// server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include "iostream"
#include "winsock2.h"
#define PORT 65432         //定义端口号常量
#define N 10     //N为允许接入的最大客户数
#define BUFFER_LEN 500 //定义接收缓冲区大小
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main(int argc, char **argv)
{
	/***定义相关的变量***/
	SOCKET sock_server,newsock[N+1];//定义监听套接字变量及已连接套接字数组,从下标1开始计,下标0不用!
	struct sockaddr_in addr; //存放本地地址的sockaddr_in结构变量
	struct sockaddr_in  client_addr;//存放客户端地址的sockaddr_in结构变量
	char msgbuffer[BUFFER_LEN];//定义用于接收客户端发来信息的缓区
	/***发给客户端的信息***/
	char msg[] ="Connect succeed. Please send a message to me.\n";
	/***初始化winsock2.DLL***/
	WSADATA wsaData;
	WORD wVersionRequested=MAKEWORD(2,2);  //生成版本号2.2
	if(WSAStartup(wVersionRequested,&wsaData)!=0)
	{
		cout<<"加载winsock.dll失败!\n";
		return 0;
	}
	/***创建套接字***/
	if ((sock_server = socket(AF_INET,SOCK_STREAM,0))<0) 
	{
		cout<<"创建套接字失败!\n";
		WSACleanup();
		return 0;
	}
	/***设套接字为非阻塞模式***/
	unsigned long ul = 1;	
	int	nRet=ioctlsocket(sock_server, FIONBIO, &ul); //设置套接字非阻塞模式
	if (nRet == SOCKET_ERROR)
	{
		cout << "ioctlsocket failed! error:" << WSAGetLastError() << endl;
		closesocket(sock_server);
		WSACleanup();
		return 0;
	}
	/***填写要绑定的本地地址***/
	int addr_len = sizeof(struct sockaddr_in);
	memset((void *)&addr,0,addr_len);
	addr.sin_family =AF_INET;
	addr.sin_port = htons(PORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);//允许套接字使用本机任何IP地址
	/***给监听套接字绑定地址***/
	if(bind(sock_server,( struct sockaddr *)&addr,sizeof(addr))!=0)    
	{
		cout<<"地址绑定失败!\n";
		closesocket(sock_server);
		WSACleanup();
		return 0;
	}
	/***将套接字设为监听状态****/
	if(listen(sock_server,0)!=0)
	{
		cout<<"listen函数调用失败!\n";
		closesocket(sock_server);
		WSACleanup();
		return 0;
	}
	else
		cout << "listenning......\n"; 
	int n=0; //保存成功接入客户数量的变量
	int err;//保存错误代码的变量
	int i,k;//循环变量
	/***循环:接收连接请求并收发数据***/
	while(true)
	{
		if(n<N)
		{
			n++;
			if((newsock[n] = accept(sock_server, (struct sockaddr *)&client_addr, &addr_len))==INVALID_SOCKET)
			{
				n--;
				err=WSAGetLastError();
				if(err!=WSAEWOULDBLOCK)
				{
					cout<<"accept函数调用失败!\n";
					break;  //accept出错终止while循环
				}
			}
			else
			{
				cout<<"与"<<inet_ntoa(client_addr.sin_addr)<<"连接成功!活跃连接数:"<< n <<endl;
				while( send(newsock[n],msg,sizeof(msg),0)<0) //给客户发送一段信息
				{
					err=WSAGetLastError();
					if(err!=WSAEWOULDBLOCK)
					{
						cout<<"数据发送失败!连接断开"<<endl;
						closesocket(newsock[n]);   //关闭出错已连接套接字
						n--;  break;
					}
				}
			}
		}
		send(newsock[n],msg,sizeof(msg),0);  //给客户端发送一段信息
		i=1;
		while(i<=n)  //依次尝试在每一个已连接套接字上接收数据
		{
			memset((void *) msgbuffer,0, sizeof(msgbuffer));//接收缓冲区清零
			if(recv(newsock[i],msgbuffer,sizeof(msgbuffer),0)<0) //接收信息
			{
				err=WSAGetLastError();
				if(err!=WSAEWOULDBLOCK)
				{
					cout<<"接收信息失败!"<<err<<endl;
					closesocket(newsock[i]);   //关闭出错已连接套接字
					for(k=i;k<n;k++)newsock[k]=newsock[k+1]; 
					n--;   
				}
				else
					i++;
			}
			else
			{
				getpeername(newsock[i], (struct sockaddr *) &client_addr, &addr_len);
				cout<< "The message from "<< inet_ntoa(client_addr.sin_addr)
				<<":"<< msgbuffer << endl; 
				closesocket(newsock[i]);   //通信完毕关闭已连接套接字
				for(k=i;k<n;k++)newsock[k]=newsock[k+1]; 
				n--;    //使newsock数组的前n个元素为未关闭已连接套接字
			}
		}
	}
	/***结束处理***/
	for(i=1;i< n++; i++) closesocket (newsock[i]);//关闭所有已连接套接字
	closesocket(sock_server);//关闭监听套接字
	WSACleanup();//注销WinSock动态链接库
	return 0;
}

/*

listenning......
与127.0.0.1连接成功!活跃连接数:1
与127.0.0.1连接成功!活跃连接数:2
与127.0.0.1连接成功!活跃连接数:3
The message from 127.0.0.1:连接1,的信息!
与127.0.0.1连接成功!活跃连接数:3
The message from 127.0.0.1:你好!
The message from 127.0.0.1:hello!
The message from 127.0.0.1:hi!

*/

client.cpp

// client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include "iostream"
#include "winsock2.h"
#define PORT 65432         //定义要访问的服务器端口常量
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main(int argc, char **argv)
{
	/***定义相关的变量***/
	int sock_client;  //定义客户端套接字
	struct sockaddr_in server_addr; //定义存放服务器端地址的结构变量
	int addr_len = sizeof(struct sockaddr_in); //地址结构变量长度
	char msgbuffer[1000]; //接收/发送信息的缓冲区
	/***初始化winsock DLL***/ 
	WSADATA wsaData;
	WORD wVersionRequested=MAKEWORD(2,2);  //生成版本号2.2
	if(WSAStartup(wVersionRequested,&wsaData)!=0)
	{
		cout<<"加载winsock.dll失败!\n";
		return 0;
	}
	/***创建套接字***/
	if ((sock_client = socket(AF_INET,SOCK_STREAM,0))<0) 
	{
		cout<<"创建套接字失败!错误代码:"<<WSAGetLastError()<<endl;
		WSACleanup();
		return 0;
	}
	/***填写服务器地址***/
	char IP[20]="127.0.0.1";
	//cout<<"请输入服务器IP地址:";
	//cin>>IP;
	memset((void *)&server_addr,0,addr_len);//地址结构清0
	server_addr.sin_family =AF_INET;
	server_addr.sin_port = htons(PORT);
	server_addr.sin_addr.s_addr = inet_addr(IP);//填写服务器IP地址
        /***与服务器建立连接***/
	if(connect(sock_client,(struct sockaddr *)&server_addr,addr_len)!=0)    
	{
		cout<<"连接失败!错误代码:"<<WSAGetLastError()<<endl;
		closesocket(sock_client);
		WSACleanup();
		return 0;
	}
	/***接收信息并显示***/
	int size;
	if((size=recv(sock_client,msgbuffer,sizeof(msgbuffer),0))<0)
	{
		cout<<"接收信息失败!错误代码:"<<WSAGetLastError()<<endl;
		closesocket(sock_client);//关闭已连接套接字
		WSACleanup(); //注销WinSock动态链接库
		return 0;
	}
	else if(size==0)
	{
		cout<<"对方已关闭连接!\n";
		closesocket(sock_client);//关闭已连接套接字
		WSACleanup(); //注销WinSock动态链接库
		return 0;
	}
	else
		cout<<"The message from Server: "<<msgbuffer<<endl; 
	/***从键盘输入一行文字发送给服务器***/
	cout<<"从键盘输入发给服务器的信息!\n";
	cin>>msgbuffer;
	if((size=send(sock_client,msgbuffer,sizeof(msgbuffer),0))<0)
	cout<<"发送信息失败!错误代码:"<<WSAGetLastError()<<endl;
	else if(size==0)
	cout<<"对方已关闭连接!\n";
	else
	cout<<"信息发送成功!\n";
	/***结束处理***/
	closesocket(sock_client); //关闭socket
	WSACleanup(); //注销WinSock动态链接库
	return 0;
}

/*
The message from Server: Connect succeed. Please send a message to me.

从键盘输入发给服务器的信息!

*/


2.Select模型

Select模型继承自BSD(Berkeley Software Distribution伯克利软件套件),该模型使用select()函数来管理I/O。

程序通过调用select()函数可以获得一组指定的套接字的状态,这样可以保证及时捕获到最先得到满足的网络I/O事件,从而可以保证对各个套接字的I/O操作的及时性。

这里的网络I/O事件是指:监听套接字上有用户请求到达、非监听(已连接)套接字接收到数据、套接字已经准备好发生数据等事件。

套接字集合fd_set

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */    //保存集合中套接字的数目
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */    //套接字数组用于存储各个套接字的描述符 FD_SETSIZE=64
} fd_set;

对套接字集合进行操作的四个宏:

#define FD_CLR(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
            while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
                ((fd_set FAR *)(set))->fd_array[__i] = \
                    ((fd_set FAR *)(set))->fd_array[__i+1]; \
                __i++; \
            } \
            ((fd_set FAR *)(set))->fd_count--; \
            break; \
        } \
    } \
} while(0, 0)

#define FD_SET(fd, set) do { \
    u_int __i; \
    for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
        if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
            break; \
        } \
    } \
    if (__i == ((fd_set FAR *)(set))->fd_count) { \
        if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
            ((fd_set FAR *)(set))->fd_array[__i] = (fd); \
            ((fd_set FAR *)(set))->fd_count++; \
        } \
    } \
} while(0, 0)

#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)

#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))

FD_ZERO(*set):初始化set指向的套接字集合为空集合,套接字集合在使用前一定要初始化!

FD_CLR(s,*set):从set指向的套接字集合移除套接字s;

FD_ISSET(s,*set):检查套接字s是不是set指向的套接字集合中的成员,若是,返回真true;

FD_SET(s,*set):添加套接字到set指向的套接字集合;


#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
select(
    _In_ int nfds,    //与Berkaley套接字兼容,这里赋0,
    _Inout_opt_ fd_set FAR * readfds,    //一套接字集合,select()函数将检查该集合套接字的可读性
    _Inout_opt_ fd_set FAR * writefds,    //一套接字集合,select()函数将检查其可写性
    _Inout_opt_ fd_set FAR * exceptfds,    //一套接字集合,select()检查集合中的套接字是否有带外数据或出现错误
    _In_opt_ const struct timeval FAR * timeout    //是select()函数阻塞等待的最长时间,若为NULL,则无限阻塞
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

select()若返回负值,出错;

返回0,则表示timeout指定的时间内没有可读可写或错误的套接字;

返回正数,表示三个集合中剩余可读、可写、或出错的的套接字的总个数,此时,应调用FD_ISSET宏来判断某个套接字是否还存在于相应的集合中。

/*
 * Structure used in select() call, taken from the BSD file sys/time.h.
 */
struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* and microseconds */
};
timeout指向一个结构体变量(struct timeval)。该结构体变量指定了select()函数等待的最长时间。

若timeout=NULL,则无限阻塞,直到有套接字符合不被删除的条件。

若将该结构体变量设置为{0,0},则select()函数将在检查完所有集合中的套接字后立刻返回,返回值为0。

以下server.cpp代码实现服务器端Select模型,允许有多个客户接入:

// server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include "iostream"
#include "winsock2.h"
#define PORT 65432         //定义端口号常量
#define BUFFER_LEN 500   //定义接收缓冲区长度
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main(int argc, char **argv)
{

	/***定义相关的变量***/
	SOCKET sock_server,newsock; //定义监听套接字和临时已连接套接字变量
	fd_set fdsock;  //保存所有套接字的集合
	fd_set fdread;  //select要检测的可读套接字集合
	struct sockaddr_in addr;//存放本地地址的sockaddr_in结构变量
	struct sockaddr_in  client_addr;//存放客户端地址的sockaddr_in结构变量
	char msgbuffer[BUFFER_LEN];//定义用于接收客户端发来信息的缓区
	char msg[] ="Connect succeed. Please send a message to me.\n"; //发给客户端的信息
	/***初始化winsock2.DLL***/
	WSADATA wsaData;
	WORD  wVersionRequested=MAKEWORD(2,2);  //生成版本号2.2
	if(WSAStartup(wVersionRequested,&wsaData)!=0)
	{
		 cout<<"加载winsock.dll失败!\n";
		 return 0;
	}
	/***创建套接字***/
	if ((sock_server = socket(AF_INET,SOCK_STREAM,0))<0) 
	{
		cout<<"创建套接字失败!\n";
		WSACleanup();
		return 0;
	}
	/***填写要绑定的本地地址***/
	int addr_len = sizeof(struct sockaddr_in);
	memset((void *)&addr,0,addr_len);
	addr.sin_family =AF_INET;
	addr.sin_port = htons(PORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);//允许套接字使用本机的任何IP
	/***给监听套接字绑定地址***/
	if(bind(sock_server,( struct sockaddr *)&addr,sizeof(addr))!=0)    
	{
		cout<<"地址绑定失败!\n";
		closesocket(sock_server);
		WSACleanup();
		return 0;
	}
	/***将套接字设为监听状态****/
	if(listen(sock_server,0)!=0)      
	{
		cout<<"listen函数调用失败!\n";
		closesocket(sock_server);
		WSACleanup();
		return 0;
	}
	else
		cout<<"listenning......\n"; 
	FD_ZERO(&fdsock);//初始化fdsock
	FD_SET(sock_server, &fdsock);//将监听套接字加入到套接字集合fdsock
	/***循环:接收连接请求并收发数据***/
	while(true)
	{
		FD_ZERO(&fdread);//初始化fdread
		fdread=fdsock;//将fdsock中的所有套接字添加到fdread中
		if(select(0, &fdread, NULL, NULL, NULL)>0)
		{
			for(int i=0;i<fdsock.fd_count;i++)
			{
				if (FD_ISSET(fdsock.fd_array[i], &fdread)) 
				{
					if(fdsock.fd_array[i]==sock_server)
					{	//有客户连接请求到达,接收连接请求
						newsock=accept (sock_server, (struct sockaddr *) &client_addr, &addr_len);
						if(newsock==INVALID_SOCKET) 
						{  //accept出错终止所有通信,结束程序
							cout<<"accept函数调用失败!\n";
							for(int j=0;j<fdsock.fd_count;j++)
							closesocket(fdsock.fd_array[j]); //关闭所有套接字
							WSACleanup();//注销WinSock动态链接库
							return 0; 
						}
						else
						{
							cout<<inet_ntoa(client_addr.sin_addr)<<"连接成功!\n";
							send(newsock,msg,sizeof(msg),0) ;//发送一段信息
							FD_SET(newsock, &fdsock);//将新套接字加入fdsock
						}
					}
					else
					{	//有客户发来数据,接收数据
						memset((void *) msgbuffer,0, sizeof(msgbuffer));//缓冲区清零
						int size=recv(fdsock.fd_array[i],msgbuffer,sizeof(msgbuffer),0);
						if(size<0) //接收信息
							cout<<"接收信息失败!"<<endl;
						else if(size==0)
							cout<<"对方已关闭!\n";
						else
						{  //显示收到信息
							getpeername(fdsock.fd_array[i], (struct sockaddr *)&client_addr, &addr_len); //获取对方IP地址
			 				cout<< inet_ntoa(client_addr.sin_addr) <<":"<< msgbuffer << endl;
						}
						closesocket(fdsock.fd_array[i]);   //关闭套接字
						FD_CLR(fdsock.fd_array[i],&fdsock);//清除已关闭套接字
					}
				}
			}
		}
		else
		{
			cout<<"Select调用失败!\n";
			break;//终止循环退出程序
		}
	}
	/***结束处理***/
	for(int j=0;j<fdsock.fd_count; j++)
	closesocket (fdsock.fd_array[j]);//关闭所有已连接套接字
	WSACleanup();//注销WinSock动态链接库
	return 0;
}

/*

listenning......
127.0.0.1连接成功!
127.0.0.1连接成功!
127.0.0.1连接成功!
127.0.0.1:你好!
127.0.0.1:hello!
127.0.0.1:hi!


*/

3.WSAAsyncSelect模型

同步模型

异步:函数启动的规定的任务后立即返回到调用处,并返回一个用来标识所启动任务的异步任务句柄。

为了适用Windows的消息驱动机制,同时也解决同步模型中那些耗时较多的套接字函数所引起的程序执行效率地下的问题,WinSock2引入Windows消息驱动机制,对近20个伯克利套接字进来了扩展,这就是所谓的异步扩展,异步扩展后的函数名都以WSA开头。

WSAAsyncSelect模型是Windows套接字的异步版本,仅用于Windows系统环境下,而且,使用该模型必须利用消息循环,即要创建窗口界面!

而select模型是从伯克利套接字继承而来的,广泛用于UNIX类系统和Windows系统,使用select模型不需要创建窗口(消息循环)


在上面的三种套接字编程模型中,阻塞模型是在不知道I/O事件是否发生的情况下,应用程序会按自己既定的流程主动去执行IO操作,结果通常是阻塞并等待相应事件的发生;非阻塞模型也是在不知道I/O事件是否发生的情况下,应用程序按自己既定的流程,反复(循环)执行I/O操作,直对应的I/O事件发生,操作执行成功。

Select模型则是在不知道I/O事件是否发生的情况下,应用程序按既定的流程调用select()函数主动检查关心I/O事件是否发生(也是循环),如果没有发生,select()函数也是阻塞等待的。这三种模型的共同特点就是,不管I/O事件是否发生,应用程序都按既定的流程(只执行一次或循环)主动试着进行I/O操作,而且,直到操作成功才会返回罢休。因此,这三个模型都属于同步模型!

尽管,非阻塞套接字和select模型一次能够尝试对多个套接字进行I/O操作,要比阻塞模型效率高很多,但在实际应用中仍然是难以满足要求的。因为,应用程序一旦开始I/O操作,则该操作完成之前程序都是无法进行其他操作的。解决这一问题的方法是采用异步I/O模型!

在异步套接字I/O模型中,当网络中I/O事件发生时,系统采用某种机制通知应用程序,应用程序只有在收到事件通知时才调用相应的套接字函数进行I/O操作。WSAAsyncSelect模型和WSAEventSelect模型都属于异步I/O模型,两者的差别在于系统通知应用程序的方式不同!

WSAAsyncSelect模型的核心是WSAAsyncSelect()函数,该函数为指定的套接字向系统注册一个或多个应用程序需要关注的网络事件。注册的网络事件需要指定事件发生时需要发生的消息以及接收该消息的窗口句柄。程序运行时,一旦被注册的事件发生,系统将向指定的窗口发生指定的消息。

#if INCL_WINSOCK_API_PROTOTYPES
_WINSOCK_DEPRECATED_BY("WSAEventSelect()")
WINSOCK_API_LINKAGE
int
WSAAPI
WSAAsyncSelect(
    _In_ SOCKET s,    //需要注册事件通知的套接字s
    _In_ HWND hWnd,    //当网络I/O事件发生时接收该消息的窗口句柄
    _In_ u_int wMsg,    //当网络I/O事件发生时向窗口发生的用户自定义消息ID
    _In_ long lEvent    //要注册的套接字s的网络事件集合,32位掩码
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */
//如果网络事件注册成功,则函数返回0,否则,返回SOCKRT_ERROR,进一步错误信息用WSAGetLastError获取错误码。

WinSock中定义的网络事件:FD_ACCEPT、FD_CLOSE、FD_CONNECT、FD_READ、FD_WRITE、FD_OOB。


下面是使用WSAAsyncSelect模型实现简单的C/S一对一简单聊天程序。

MFC创建对话框应用程序。



以下是server端代码:

在Resource.h文件中添加自定义消息ID:

#define MsgAccept WM_USER+100

#define MsgRecv WM_USER+101

通过类向导为控件添加控件变量

在AsyncSelectServerDlg.h文件的类定义中添加成员变量:



SOCKET m_ListenSocket; //监听套接字变量
SOCKET m_acceptSocket; //存储连接建立后accept得到的套接字号

struct sockaddr_in addr, client_addr;

在AsyncSelectServerDlg.cpp开头添加常量定义:



#define PORT 65432

#define BUFFER_LEN 1000

在AsyncSelectServerDlg.cpp的对话框初始化函数::OnInitDialog()中创建套接字、给套接字绑定地址、使套接字处于监听状态、并调用WSAAsyncSelect(),为套接字注册FD_ACCEPT事件。

BOOL CAsyncSelectServerDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 将“关于...”菜单项添加到系统菜单中。

	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != NULL)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO:  在此添加额外的初始化代码

	//创建套接字
	if ((m_ListenSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		CString str1("创建套接字失败!");
		WSACleanup();
		MessageBox(str1);
		return FALSE;
	}
	
	int addr_len = sizeof(struct sockaddr_in);
	memset((void*)&addr, 0, addr_len);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(PORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);
	if (bind(m_ListenSocket, (LPSOCKADDR)&addr, sizeof(addr)) != 0)
	{
		CString str1("套接字绑定失败!");
		closesocket(m_ListenSocket);
		WSACleanup();
		MessageBox(str1);
		return FALSE;
	}
	if (listen(m_ListenSocket, 5) != 0)
	{
		CString str1("Listen 失败!");
		closesocket(m_ListenSocket);
		WSACleanup();
		MessageBox(str1);
		return 0;
	}
	//为m_ListenSocket注册FD_ACCEPT异步事件
	//为该事件发生时系统将向本窗口发送自定义消息MsgAccept
	if (WSAAsyncSelect(m_ListenSocket, m_hWnd, MsgAccept, FD_ACCEPT) != 0)
	{
		CString str3("套接字异步事件注册失败!");
		MessageBox(str3);
		closesocket(m_ListenSocket);
	}

	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

利用类向导添加自定义消息MsgAccept及其消息处理函数,该消息发出时,说明有客户端的连接请求到达,因此,该消息处理函数调用accept()与客户建立连接、并为连接成功后得到的已连接套接字注册FD_READ事件和MsgRecv消息。

MsgAccept消息处理代码如下:

afx_msg LRESULT CAsyncSelectServerDlg::OnMsgaccept(WPARAM wParam, LPARAM lParam)
{
	int addr_len=sizeof(struct sockaddr_in);
	if ((m_acceptSocket = accept(m_ListenSocket, (LPSOCKADDR)&client_addr, &addr_len)) == INVALID_SOCKET)
	{
		int errorCode = WSAGetLastError();
		CString str1("accept函数调用失败!,服务器关闭!");
		MessageBox(str1);
		closesocket(m_acceptSocket);
		closesocket(m_ListenSocket);
	}
	else
	{
		//为m_acceptSocket注册FD_ACCEPT异步事件
		//该事件发生时系统将向本窗口发送自定义消息MsgRecv;然后调用该消息的处理函数OnMsgrecv()
		if (WSAAsyncSelect(m_acceptSocket, m_hWnd, MsgRecv, FD_READ) != 0)
		{
			CString str3("套接字异步事件注册失败!");
			MessageBox(str3);
			closesocket(m_acceptSocket);
			closesocket(m_ListenSocket);
		}
	}

	return 0;
}

利用类向导添加自定义消息MsgRecv及其消息处理函数,该消发出时,说明有数据到底,该消息的处理函数要调用recv()函数接收数据,并将接收到的数据显示于ListBox控件上。该消息的处理函数如下:

afx_msg LRESULT CAsyncSelectServerDlg::OnMsgrecv(WPARAM wParam, LPARAM lParam)
{
	char recvBuffer[BUFFER_LEN];
	int size = recv(m_acceptSocket, recvBuffer, sizeof(recvBuffer), 0);
	if (size > 0)
	{
		recvBuffer[size] = '\0';
		m_CListBox.AddString((LPCTSTR)recvBuffer);
	}
	return 0;
}

为发生消息按钮添加事件处理函数:

void CAsyncSelectServerDlg::OnBnClickedOk()
{
	// TODO:  在此添加控件通知处理程序代码
	UpdateData(TRUE);//将数据由控件传向控件变量
	CString black("");
	CString text = black+"I said" + m_Text;
	m_CListBox.AddString(text);
	USES_CONVERSION;
	char* keyChar = T2A(m_Text.GetBuffer(0));
	m_Text.ReleaseBuffer();
	send(m_acceptSocket, keyChar, m_Text.GetLength(), 0);

}

下面是客户端代码:

Resource.h文件添加:



#define MsgRecv WM_USER+101

为控件添加控件变量

在类定义中添加成员变量:

AsyncSelectClientDlg.h

SOCKET m_clientsocket; 

struct sockaddr_in addr;


在AsyncSelectClientDlg.cpp添加:

#define PORT 65432

#define BUFFER_LEN 1000


为连接按钮添加单击事件处理函数:

void CAsyncSelectClientDlg::OnBnClickedButton1()
{
	// TODO:  在此添加控件通知处理程序代码
	if ((m_clientsocket = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	{
		CString str1("创建套接字失败!");
		MessageBox(str1);
		return ;
	}
	int addr_len = sizeof(struct sockaddr_in);
	memset((void*)&addr, 0, addr_len);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(PORT);
	DWORD ipaddr;
	m_IPaddress.GetAddress(ipaddr);
	addr.sin_addr.s_addr = htonl(ipaddr);

	if (connect(m_clientsocket, (LPSOCKADDR)&addr, addr_len)!=0)
	{
		CString str1("连接失败!");
		MessageBox(str1);
		closesocket(m_clientsocket);
		return;
	}
	//注册套接字异步处理事件FD_READ
	if (WSAAsyncSelect(m_clientsocket, m_hWnd, MsgRecv, FD_READ) != 0)
	{
		CString str3("套接字异步注册事件失败!");
		MessageBox(str3);
		closesocket(m_clientsocket);
	}

}

利用类向导添加自定义消息MsgRecv及其消息处理函数。当该消息发出时,说明有数据到达,在该消息的处理函数中调用recv()接收数据并显示在控件上,该消息处理函数代码如下:

afx_msg LRESULT CAsyncSelectClientDlg::OnMsgrecv(WPARAM wParam, LPARAM lParam)
{
	char recvBuffer[BUFFER_LEN];
	int size = recv(m_clientsocket, recvBuffer, sizeof(recvBuffer), 0);
	if (size > 0)
	{
		recvBuffer[size] = '\0';
		m_CListBox.AddString((LPCTSTR)recvBuffer);
	}
	return 0;
}

为发送按钮添加单击事件处理函数:

void CAsyncSelectClientDlg::OnBnClickedOk()
{
	// TODO:  在此添加控件通知处理程序代码
	UpdateData(TRUE);
	CString black("");
	CString text = black + "I said" + m_Edit;
	m_CListBox.AddString(text);
	USES_CONVERSION;
	char* keyChar = T2A(m_Edit.GetBuffer(0));
	m_Edit.ReleaseBuffer();
	send(m_clientsocket, keyChar, m_Edit.GetLength(), 0);
}

4.   WSAEventSelect模型

WSAEventSelect模型与WSAAsyncSelect模型都是异步I/O模型。二者不同之处在于网络事件发生时,系统通知应用程序的方式不同。

WSAAsyncSelect模型是基于Windows消息驱动机制的,网络事件发生时系统将以消息的形式通知应用程序、并且消息必须与窗口句柄相绑定,因此,程序必须要有窗口对象才行;

而WSAEventSelect模型是以事件对象为基础的,网络事件需要与事件对象相关联,当网络事件发生时,经由事件对象句柄通知应用程序,常用于控制台应用程序中。


4.1WinSock中的事件对象

这里的事件对象与Win32(,分为人工重设模式和自动重设模式,,Windows API:CreatEvent()、SetEvent()、ResetEvent()、事件的状态可以被WaitForSingleObject()函数或WaitForMultiObject()函数“等事件通知等待函数所捕获)、(MFC中的CEvent类)多线程中的事件对象完全相同!

1.WSACreateEvent函数:创建一个人工重设模式的事件对象!初始为”无信号“状态!

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
WSAEVENT
WSAAPI
WSACreateEvent(
    void
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

WSACreateEvent可以用CreateEvent()函数来代替,CreateEvent()默认创建自动重设模式的事件对象!

2.WSAResetEvent()函数:将事件对象从有信号状态变为无信号状态!

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
BOOL
WSAAPI
WSAResetEvent(
    _In_ WSAEVENT hEvent
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

3.WSASetEvent()函数:将事件对象状态设为”有信号"状态

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
BOOL
WSAAPI
WSASetEvent(
    _In_ WSAEVENT hEvent
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

4.WSACloseEvent():关闭事件对象,释放事件对象占有的系统资源!

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
BOOL
WSAAPI
WSACloseEvent(
    _In_ WSAEVENT hEvent
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

4.2WSAEventSelect选择模型的函数

1.网络事件注册函数WSAEventSelect():为套接字注册网络事件,将网络事件与事件对象相关联!

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSAEventSelect(
    _In_ SOCKET s,    //套接字
    _In_opt_ WSAEVENT hEventObject,    //对象句柄
    _In_ long lNetworkEvents            //感兴趣的网络事件集合 32位掩码
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */
2.网络事件等待函数WSAWaitForMultipleEvents():等待与套接字关联的事件对象由“无信号”状态变为“有信号”状态。 事件发生前,该函数将阻塞等待!直到事件发生,或等待超时!
#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
DWORD
WSAAPI
WSAWaitForMultipleEvents(
    _In_ DWORD cEvents,
    _In_reads_(cEvents) const WSAEVENT FAR * lphEvents,
    _In_ BOOL fWaitAll,
    _In_ DWORD dwTimeout,
    _In_ BOOL fAlertable
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */
		/*WSAWaitForMultipleEvents()的返回值指出使得该函数返回的事件对象
		分四种情况:
		(1)是一个从WSA_WAIT_EVENT_0到WSA_WAIT_EVENT_0+cEvents-1范围的整数,
		如果参数fWaitAll=TRUE,则表示所有事件对象都处于“有信号”状态,
		如果参数fWaitAll=FALSE,则用函数的返回值减去WSA_WAIT_EVENT_0即为有信号的事件对象在lphEvents数组中的下标位置!
		(2)返回WAIT_TO_COMPLETION:表示一个或多个完成例程已经排队等待执行;
		(3)WSA_WAIT_TIMEOUT:函数调用超时,并且所有的事件对象都处于“无信号”状态。
		(4)如果函数调用失败,则返回WSA_WAIT_FALIED
		*/

3.网络事件枚举函数WSAEnumNetworkEvents():用于获取套接字上发生的网络事件,同时清除系统内部的网络事件记录,还可以重置事件对象!

WINSOCK_API_LINKAGE
int
WSAAPI
WSAEnumNetworkEvents(
    _In_ SOCKET s,
    _In_ WSAEVENT hEventObject,    //被重置的事件对象(可为NULL)
    _Out_ LPWSANETWORKEVENTS lpNetworkEvents    //指向WSANETWORKEVENTS结构变量的指针!,该结构中包含发生网络事件的记录与相关错误代码!
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */
typedef struct _WSANETWORKEVENTS {
       long lNetworkEvents;    //指示发生的网络事件
       int iErrorCode[FD_MAX_EVENTS];    //包含网络事件错误代码的数组!元素值为0表示没有发生错误、可以用来检测是否发生了错误!下标用FD_XXXX_BIT来索引
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;

下面代码实现客户端与服务器端通信:

要求服务器端使用WSAEventSelect模型,允许多个客户端接入!

 server.cpp:

// server.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include "iostream"
#include "winsock2.h"
#define PORT 65432         //定义端口号常量
#define MSGSIZE 1000     //接收缓冲区大小
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main(int argc, char **argv)
{
	/***定义相关的变量***/
	SOCKET sockserver, newsock; //定义监听套接字和临时已连接套接字变量
	SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]; //用于保存监听套接字以及多个已连接套接字的描述符 WSA_MAXIMUM_WAIT_EVENTS=64
	WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS]; //用于保存与sockArray中关联的多个事件对象的句柄
	struct sockaddr_in addr;  //存放本地地址的sockaddr_in结构变量
	struct sockaddr_in  clientaddr; //存放客户端地址的sockaddr_in结构变量
	char msgbuffer[MSGSIZE]; //定义用于接收客户端发来信息的缓区
	/***发给客户端的信息***/
	char msg[] ="Connect succeed. Please send a message to me.\n";
	/***初始化winsock2.DLL***/
	WSADATA wsaData;
	WORD wVersionRequested=MAKEWORD(2,2);  //生成版本号2.2
	if(WSAStartup(wVersionRequested,&wsaData)!=0)
	{
		cout<<"加载winsock.dll失败!\n";
		return 0;
	}
	/***创建套接字***/
	if ((sockserver = socket(AF_INET,SOCK_STREAM,0))<0) 
	{
		cout<<"创建套接字失败!\n";
		WSACleanup();
		return 0;
	}
	/***填写要绑定的本地地址***/
	int addrlen = sizeof(struct sockaddr_in);
	memset((void *)&addr,0,addrlen);
	addr.sin_family =AF_INET;
	addr.sin_port = htons(PORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY); //允许套接字使用分配给本机的任何IP
	/***给监听套接字绑定地址***/
	if(bind(sockserver,( struct sockaddr *)&addr,sizeof(addr))!=0)    
	{
		cout<<"地址绑定失败!\n";
		closesocket(sockserver);
		WSACleanup();
		return 0;
	}
	/***将套接字设为监听状态****/
	if(listen(sockserver,0)!=0)      
	{
		cout<<"listen函数调用失败!\n";
		closesocket(sockserver);
		WSACleanup();
		return 0;
	}
	else
	cout<< "listenning......\n"; 
	/***为监听套接字sockserver创建事件对象并注册网络事件***/
	int nEventTotal = 0;  
	WSAEVENT hEvent = WSACreateEvent();  
	//WSAResetEvent();
	//WSASetEvent()
	//WSACloseEvent()
	WSAEventSelect(sockserver, hEvent, FD_ACCEPT|FD_CLOSE);  
	/***将监听套接字及事件对象添加到数组中 **/  
	eventArray[nEventTotal] = hEvent;  
	sockArray[nEventTotal] = sockserver;     
	nEventTotal++;  
	/***循环:接收连接请求并收发数据***/
	int nIdx, ret;
	WSANETWORKEVENTS netEvent;  
	while(true)
	{
		//在所有事件对象上等待网络事件发生
		nIdx=WSAWaitForMultipleEvents( nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
		/*WSAWaitForMultipleEvents()的返回值指出使得该函数返回的事件对象
		分四种情况:
		(1)是一个从WSA_WAIT_EVENT_0到WSA_WAIT_EVENT_0+cEvents-1范围的整数,
		如果参数fWaitAll=TRUE,则表示所有事件对象都处于“有信号”状态,
		如果参数fWaitAll=FALSE,则用函数的返回值减去WSA_WAIT_EVENT_0即为有信号的事件对象在lphEvents数组中的下标位置!
		(2)返回WAIT_TO_COMPLETION:表示一个或多个完成例程已经排队等待执行;
		(3)WSA_WAIT_TIMEOUT:函数调用超时,并且所有的事件对象都处于“无信号”状态。
		(4)如果函数调用失败,则返回WSA_WAIT_FALIED
		*/
		int i = nIdx - WSA_WAIT_EVENT_0;
		for(int i=nIdx; i<nEventTotal; i++)  
		{  
			ret=WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);  
			if(ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)  
				continue;  
			else  
			{
				WSAEnumNetworkEvents(sockArray[i], eventArray[i], &netEvent); 
				// 获取到来的通知消息并自动重置受信事件
				if(netEvent.lNetworkEvents & FD_ACCEPT) 
				{  //处理FD_ACCEPT通知消息  
					if(netEvent.iErrorCode[FD_ACCEPT_BIT] == 0)  
					{  
						if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)  
						{  
							cout<<" Too many connections! \n";  
							continue;  
						}
						newsock=accept(sockserver, (struct sockaddr*)&clientaddr,&addrlen);
						if(newsock==INVALID_SOCKET)
						{  //接收连接出错,关闭所有套接字及事件对象退出
							for(int j=0;j<nEventTotal;j++)
							{
								closesocket(sockArray[i]);
								WSACloseEvent(eventArray[i]);
							}
							WSACleanup();
						}
						hEvent = WSACreateEvent();  
						WSAEventSelect(newsock, hEvent, FD_READ|FD_CLOSE);  
						//将已连接套接字及关联事件对象添加到相应数组  
						eventArray[nEventTotal] =hEvent;  
						sockArray[nEventTotal] = newsock ;    
						nEventTotal++;  
						cout<<inet_ntoa(clientaddr.sin_addr)<<"连接成功!\n";
						send(newsock,msg,sizeof(msg),0);  //立刻发送消息
					}  
				}  
				else if(netEvent.lNetworkEvents & FD_READ)    
				{   // 处理FD_READ通知消息
					getpeername( sockArray[i], (struct sockaddr*) &clientaddr, &addrlen);
					if(netEvent.iErrorCode[FD_READ_BIT] == 0)  
					{  
						//recv()执行成功返回的是实际从套接字接收缓冲区拷贝到程序自己设置的接收缓冲区buffer中的字节数。
						//有个问题,recv()函数的返回值并不是实际收到了多少个字节!
						int strlenbuffer = strlen(msgbuffer);//==509
						int sizeofbuffer = sizeof(msgbuffer);//500
						//int nRcv= recv(sockArray[i], msgbuffer, strlen(msgbuffer), 0);  
						int nRcv = recv(sockArray[i], msgbuffer, sizeof(msgbuffer), 0);
						if(nRcv > 0) 
						{
							//msgbuffer[nRcv]='\0';//已经是数组越界了!这个'\0’是没必要加的!,因为从客户端发过来的数据是从键盘敲入的字符串,回车时自动在末尾加了字符串结束符'\0';
							cout<<inet_ntoa(clientaddr.sin_addr) <<":"<< msgbuffer << endl; 
						}
						else
						{
							int err=WSAGetLastError();
							cout<< "接收信息失败!"<<err<<endl;
						}
						closesocket(sockArray[i]);
						WSACloseEvent(eventArray[i]);
						for(int j=i; j<nEventTotal-1; j++)  
						{  
							sockArray[j] = sockArray[j+1];  
							eventArray[j] = eventArray[j+1];    
						}  
						nEventTotal--;  
					}  
				}  
				else if(netEvent.lNetworkEvents & FD_CLOSE)  
				{  // 处理FD_CLOSE通知消息  
					if(netEvent.iErrorCode[FD_CLOSE_BIT] == 0)  
					{
						closesocket(sockArray[i]);
						WSACloseEvent(eventArray[i]);
						for(int j=i; j<nEventTotal-1; j++)  
						{  
							sockArray[j] = sockArray[j+1];  
							eventArray[j] = eventArray[j+1]; 
						}  
						nEventTotal--;  
					}  
				}  
			}  
		}  
	}
	return 0;
}

client.cpp

// client.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"

#include "iostream"
#include "winsock2.h"
#define PORT 65432         //定义要访问的服务器端口常量
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main(int argc, char **argv)
{
	/***定义相关的变量***/
	int sock_client;  //定义客户端套接字
	struct sockaddr_in server_addr; //定义存放服务器端地址的结构变量
	int addr_len = sizeof(struct sockaddr_in); //地址结构变量长度
	char msgbuffer[1000]; //接收/发送信息的缓冲区
	/***初始化winsock DLL***/ 
	WSADATA wsaData;
	WORD wVersionRequested=MAKEWORD(2,2);  //生成版本号2.2
	if(WSAStartup(wVersionRequested,&wsaData)!=0)
	{
		cout<<"加载winsock.dll失败!\n";
		return 0;
	}
	/***创建套接字***/
	if ((sock_client = socket(AF_INET,SOCK_STREAM,0))<0) 
	{
		cout<<"创建套接字失败!错误代码:"<<WSAGetLastError()<<endl;
		WSACleanup();
		return 0;
	}
	/***填写服务器地址***/
	char IP[20]="127.0.0.1";
	//cout<<"请输入服务器IP地址:";
	//cin>>IP;
	memset((void *)&server_addr,0,addr_len);//地址结构清0
	server_addr.sin_family =AF_INET;
	server_addr.sin_port = htons(PORT);
	server_addr.sin_addr.s_addr = inet_addr(IP);//填写服务器IP地址
        /***与服务器建立连接***/
	if(connect(sock_client,(struct sockaddr *)&server_addr,addr_len)!=0)    
	{
		cout<<"连接失败!错误代码:"<<WSAGetLastError()<<endl;
		closesocket(sock_client);
		WSACleanup();
		return 0;
	}
	/***接收信息并显示***/
	int size;
	if((size=recv(sock_client,msgbuffer,sizeof(msgbuffer),0))<0)
	{
		cout<<"接收信息失败!错误代码:"<<WSAGetLastError()<<endl;
		closesocket(sock_client);//关闭已连接套接字
		WSACleanup(); //注销WinSock动态链接库
		return 0;
	}
	else if(size==0)
	{
		cout<<"对方已关闭连接!\n";
		closesocket(sock_client);//关闭已连接套接字
		WSACleanup(); //注销WinSock动态链接库
		return 0;
	}
	else
		cout<<"The message from Server: "<<msgbuffer<<endl; 
	/***从键盘输入一行文字发送给服务器***/
	cout<<"从键盘输入发给服务器的信息!\n";
	cin>>msgbuffer;
	//输入hello,打断点,查看msgbuffer数组每个元素,就有msgbuffer[5]='\0'  ,而用strlen求字符串长度时,会自动忽略字符串结尾符'\0',只计算实际输入的字符数!
	//输入你好,打断点,查看msgbuffer数组每个元素,就有msgbuffer[4]='\0' 
	int sizeofmsgbuffer = sizeof(msgbuffer);//1000
	int strlenmsgbuffer = strlen(msgbuffer);//5

	if((size=send(sock_client,msgbuffer,sizeof(msgbuffer),0))<0)
	cout<<"发送信息失败!错误代码:"<<WSAGetLastError()<<endl;
	else if(size==0)
	cout<<"对方已关闭连接!\n";
	else
	cout<<"信息发送成功!\n";
	/***结束处理***/
	closesocket(sock_client); //关闭socket
	WSACleanup(); //注销WinSock动态链接库
	system("pause");
	return 0;
}

5.重叠I/O模型

重叠I/O模型是以Windows重叠I/O机制为基础的套接字I/O模型。

Windows重叠I/O模型本来是一种文件操作技术。

在重叠I/O下,应用程序在调用文件读写函数后就立即返回,而不必等待文件操作的结束。文件读写的同时应用程序可以执行其他操作,这就是所谓的异步I/O模式,应用程序可以连续进行多个文件读写函数的调用,让系统同时执行多个文件的读写,这就是所谓的重叠I/O操作。


重叠I/O核心数据结构:

typedef struct _OVERLAPPED {
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union {
        struct {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;

    HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;

当系统完成文件读写操作时,会将OVERLAPPED结构中的事件句柄hEvent指向的事件对象的状态设置为“有信号”状态,应用程序可以调用WaitForMultipleObjects()函数来等待这个I/O操作完成的通知,在得到通知信号后,就可以调用相关函数来查询操作结果,并进行相关处理。

从WinSock2开始,重叠I/O模型就被引入到WinSock套接字函数中,这些函数都是专门针对Windows的扩展,函数名都以WAS开头。

1.创建套接字函数WSASocket()

WINSOCK_API_LINKAGE
_Must_inspect_result_
SOCKET
WSAAPI
WSASocketW(
    _In_ int af,    //标识地址家族,TCP/IP协议地址族为常量AF_INET
    _In_ int type,    //标识套接字类型:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW
    _In_ int protocol,    //用于指定套接字所用的特点协议,0:默认协议
    _In_opt_ LPWSAPROTOCOL_INFOW lpProtocolInfo,    一个指向PROTOCOL_INFO结构的指针,该结构定义所创建套接字的特性!
    _In_ GROUP g,    //套接字组标识 :0
    _In_ DWORD dwFlags    //套接字属性描述,其中WSA_FLAG_OVERLAPPED指明套接字具备重叠I/O特性!
    );
#ifdef UNICODE
#define WSASocket  WSASocketW
#else
#define WSASocket  WSASocketA
#endif /* !UNICODE */
#endif /* INCL_WINSOCK_API_PROTOTYPES */

2.数据发送函数WSASend():该函数在重叠套接字上发送数据时,一次可以发送多个缓冲区的内容!

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSASend(
    _In_ SOCKET s,    //标识用来发送数据的一个已连接套接字描述符
    _In_reads_(dwBufferCount) LPWSABUF lpBuffers,    //一个指向WSABUF结构数组的指针,指向保存有要发送数据的多个缓冲区
    _In_ DWORD dwBufferCount,    //lpBuffers数组中,WSABUF结构变量的数目!
    _Out_opt_ LPDWORD lpNumberOfBytesSent,   //若发送操作立即完成,则该指针指向的变量中存放所发送的数据的字节数!
    _In_ DWORD dwFlags,        //标志位,于send()函数的标志位相似,用于控制数据的发送方式,通常取0表示正常发送数据!
    _Inout_opt_ LPWSAOVERLAPPED lpOverlapped,    //指向WSAOVERLAPPED结构变量的指针。非重叠套接字忽略该参数。
    _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine    //指向发送操作完成后要调用的完成例程函数的指针,非重叠套接字忽略该参数
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

WSABUF结构是在WinSock2.h中定义的WSASend()和WSARecv()的专用缓冲区!其定义如下:

typedef struct _WSABUF {
    ULONG len;     /* the length of the buffer */    //缓冲区长度,以字节为单位!
    _Field_size_bytes_(len) CHAR FAR *buf; /* the pointer to the buffer */    //缓冲区地址指针
} WSABUF, FAR * LPWSABUF;

WSAOVERLAPPED结构是一个完全与OVERLAPPED定义完全相同的结构。

#define WSAOVERLAPPED           OVERLAPPED
typedef struct _OVERLAPPED {
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union {
        struct {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;

    HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;

完成例程是一个函数,在参数lpCompletionRoutine 不为空NULL的时候,在重叠I/O操作完成时,该参数指定的完成例程将由系统自动调用,以便对已完成的I/O请求进行处理。

一个完成例程必须以下函数原型:

void CALLBACK CompletionROUTINE(
DWORD dwError,        //指明重叠I/O操作完成的状态
DWORD cbTransferred,    //在重叠操作期间,实际传输完成的字节数
LPWSAOVERLAPPED lpOverlapped,    //指明传递给重叠I/O调用的那个WSAOVERLAPPED结构
DWORD deFlages    //返回操作结束时可能用的标志
);

WSASend()函数的返回值:

如数据发送操作即可完成,函数返回0,并且参数lpNumberOfBytesSent指向的变量被更新为已成功发送的字节数;

如数据发送操作被成功初始化,则返回SOCKET_ERROR,相应的错误代码为:WSA_IO_PENDING;数据发送将在稍后完成。、

数据发送完成后可通过WSAGetOverlappedResult()函数获取成功发送的字节数,如果指定了完成例程函数,则可通过完成例程的cbTransferred参数获取成功发送的字节数。




3.数据接受函数WSARecv()

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
int
WSAAPI
WSARecv(
    _In_ SOCKET s,
    _In_reads_(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers,
    _In_ DWORD dwBufferCount,
    _Out_opt_ LPDWORD lpNumberOfBytesRecvd,
    _Inout_ LPDWORD lpFlags,
    _Inout_opt_ LPWSAOVERLAPPED lpOverlapped,
    _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

4.获取重叠I/O操作结果函数WSAGetOverlappedResult()

#if INCL_WINSOCK_API_PROTOTYPES
WINSOCK_API_LINKAGE
BOOL
WSAAPI
WSAGetOverlappedResult(
    _In_ SOCKET s,
    _In_ LPWSAOVERLAPPED lpOverlapped,
    _Out_ LPDWORD lpcbTransfer,
    _In_ BOOL fWait,
    _Out_ LPDWORD lpdwFlags
    );
#endif /* INCL_WINSOCK_API_PROTOTYPES */

下面是使用事件对象通知方法的重叠I/O模型程序的流程(以服务器端为例):


下面是使用完成例程的重叠I/O模型程序流程:(以服务器端为例):





5.2重叠I/O模式的程序流程

当重叠I/O操作完成时,应用程序可以有两种方式来开始进行后续的处理工作:

1.使用事件对象通知

2.使用完成例程(完成例程函数:类似于回调函)

/*回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。*/


6.完成端口模型

这篇博文对完成端口模型介绍很详细:完成端口模型

完成端口(Completion Port)是一种Windows系统的内核对象,利用完成端口,套接字应用程序能够管理数百甚至上千个套接字,而且可以使系统的性能达到最佳。使用完成端口模型之前,需要首先创建一个I/O完成端口对象,再使用该完成端口对象,可以面向任意数量的套接字句柄,管理多个I/O请求;然后,通过指定一定数量的工作线程,为已经完成的重叠I/O提供服务。

完成端口模型使用线程池对线程进行管理,通过事先创建好的多个线程,在处理多个异步并发I/O请求时避免了频繁的线程创建和注销,在没有或I/O请求很少时,不使用的线程会挂起,也不会占用CPU时间。

可以把完成端口看成是系统维护的一个队列,系统把重叠I/O操作完成的事件通知放在该队列中。当某项重叠I/O操作完成时,系统将向完成端口对象发送I/O完成数据包,在收到一个数据包后,完成端口对象中的一个工作线程将被唤醒,处理完后,线程会继续在完成端口上等待后续的通知。




参考文献:[1]杨传栋, 张焕远. Windows网络编程基础教程[M]. 清华大学出版社, 2015.P179

教程参考资料:链接:https://pan.baidu.com/s/1q-CSIR_RKc_6zakcL_vT1A 密码:iwq9

完成端口模型参考链接:https://blog.csdn.net/piggyxp/article/details/6922277

猜你喜欢

转载自blog.csdn.net/m0_37357063/article/details/80700871