【网络编程】事件选择模型

十、基于I/O模型的网络开发

10.9 事件选择模型

10.0.1 基本概念

事件选择(WSAEventSelect) 模型是另一个有用的异步 I/O 模型。和 WSAAsyncSelect 模 型类似的是,它也允许应用程序在一个或多个套接字上接收以事件为基础的网络事件通知,最 主要的差别在于网络事件会投递至一个事件对象句柄,而非投递到一个窗口例程。

10.9.2 WSAEventSelect函数

WSAEventSelect 模型主要由函数WSAEventSelect 来实现。注意,这里用了"主要由", 说明还有其他配套函数一起辅助来实现这个模型。

后面会讲到其他函数。这里先看一下 WSAEventSelect。

WSAEventSelect 函数将一个已经创建好的事件对象(由WSACreateEvent 创建)与某个套 接字关联在一起,同时注册自己感兴趣的网络事件类型。WSAEventSelect 的函数声明如下:

cpp 复制代码
int WSAAPI WSAEventSelect(
    SOCKET S,
    WSAEVENT hEventObject, long lNetworkEvents);
  • 其中,s 是套接字描述符;

  • hEventObject标识要与指定的网络事件集关联的事件对象的句 柄 ;

  • INetworkEvents指定应用程序感兴趣的网络事件组合的位掩码。

  • 如果函数成功,那么返回值为零;否则,将返回值SOCKET_ERROR, 并且可以通过调用 WSAGetLastError来获取特定的错误号。

  • 与select 和 WSAAsyncSelect 函数一样,WSAEventSelect 通常用于确定何时可以进行数据 收发操作(确定调用send或 recv能立即成功的时间点)。如果时间点没到,那么函数会返回 WSAEWOULDBLOCK, 此时我们要正确处理这个错误码。

10.9.3 实 战WSAEventSelect模型

事件选择模型的基本思路是:为感兴趣的一组网络事件创建一个事件对象,再调用 WSAEventSelect 函数将网络事件和事件对象关联起来。当网络事件发生时,Winsock 会使相 应的事件对象收到通知,在事件对象上等待的函数就会返回。之后,再调用 WSAEnumNetworkEvents函数便可获取发生了什么网络事件。

事件选择模型写的TCP 服务器实现的过程如下:

  • (1)创建事件对象和套接字。创建一个事件对象的方法是调用 WSACreateEvent 函数, 它的定义如下:
cpp 复制代码
WSAEVENT WSAAPI WSACreateEvent();
  • 如果没有发生错误,那么函数将返回事件对象的句柄;

  • 否则,返回值为 WSA_INVALID_EVENT, 可以通过WSAGetLastError 函数获取更多的错误信息。这个事件对 象创建后,其初始状态为"未受信",就是没有收到通知状态。

  • WSACreateEvent 创建的事件有两种工作状态以及两种工作模式:工作状态分别是"有信 号 " (signaled) 和"无信号" (nonsignaled), 工作模式包括"人工重设" (manual reset) 和"自动重设" (auto reset) 。WSACreateEvent 创建的事件开始是处于一种无信号的工作状 态,并用一种人工重设模式来创建事件句柄。

  • (2)将事件对象与套接字关联在一起,同时注册自己感兴趣的网络事件类型(FD_READ、 FD_WRITE、FD_ACCEPT、FD_CONNECT、FD_CLOSE 等),这个过程通过函数 WSAEventSelect实现。

  • (3)调用事件等待函数WSAWaitForMultipleEvents在所有事件对象上等待,该函数返回后,我们就可以确认在哪些套接字上发生了网络事件。 当一个或所有指定的事件对象处于信号状态、超时或执行了 I/O 完成例程时,函数 WSAWaitForMultipleEvents返回,该函数声明如下:

cpp 复制代码
#include <winsock2.h>
#pragma comment(lib, "Ws232.lib")
DWORD WSAAPI WSAWaitForMultipleEvents(DWORD CEvents,
                                      const WSAEVENT *lphEvents, BOOL fWaitAll,
                                      DWORD dwTimeout,
                                      BOOL fAlertable);
  • cEvents: 表 示lphEvent 所指数组中的事件对象句柄数,事件对象句柄的最大数量是 WSA_MAXIMUM_WAIT_EVENTS, 必须指定一个或多个事件。
  • lphEvents: 指向事件对象句柄数组的指针,数组可以包含不同类型对象的句柄,如果 后面参数fWaitAll 设置为TRUE, 那么它不能包含同一句柄的多个副本,如果在等待 仍处于挂起状态时关闭其中一个句柄,那么WSAWaitForMultipleEvents 的行为将不 可知。另外,句柄必须具有同步访问权限。
  • fWaitAll: 输入参数,用于指定等待类型的值。如果赋值为TRUE, 那么当lphEvents 数组中所有对象的状态都处于有信号时,函数将返回。注意,是所有对象都处于信号 状态才返回。如果赋值为FALSE, 则当向任一事件对象发出信号时,函数返回。在 这一种情况下,返回值减去WSA_WAIT_EVENT_0 表示其状态导致函数返回的事件 对象的索引。如果在调用期间有多个事件对象发出信号,那么返回值指示信号事件对 象的lphEvents 数组索引的最小值。
  • dwTimeout: 超时时间,单位是毫秒。如果超时时间到,则函数返回,即使不满足 fWaitAll 参数指定的条件。如果 dw Timeout参数为零,则函数将测试指定事件对象的 状态并立即返回。如果dwTimeout 是 WSA_INFINITE, 则函数将永远等待。
  • fAlertable: 指定线程是否处于可警报的等待状态,以便系统可以执行I/O 完成例程。 如果为TRUE, 则线程将处于可警报的等待状态,并且当系统执行I/O 完成例程时, 函数可以返回。在这种情况下,将返回 WSA_WAIT_IO_COMPLETION, 并且尚未 发出正在等待的事件的信号。应用程序必须再次调用WSAWaitForMultipleEvents 函 数。如果为FALSE, 则线程不会处于可警报的等待状态,也不会执行I/O 完成例程。

如果函数成功,那么返回值为以下值之一:

  • WSA_WAIT_EVEN_0 到 (WSA_WAIT_EVENT_0+cEvents-1): 如果参数 fWaitAll 参数为 TRUE, 则返回值指示已向所有指定的事件对象发出信号。如果 fWaitAll 参数为FALSE, 则返回值减去WSA_WAIT_EVENT_0 表示其状态导致函数 返回的事件对象的索引。如果在调用期间有多个事件对象发出信号,则返回值指示信 号事件对象的lphEvents 数组索引的最小值。
  • WSA_WAIT_IO_COMPLETION:等待被执行的一个或多个I/O 完成例程结束。正在 等待的事件尚未发出信号,应用程序必须再次调用WSAWaitForMultipleEvents 函数。 只有fAlertable 参数为TRUE 时,才能返回此返回值。
  • WSA_WAIT_TIMEOUT: 超时间隔已过,并且未满足fWaitAll参数指定的条件,未
    执行任何I/O完成例程。

如果函数失败,则返回值为WSA_WAIT_FAILED。此时可以通过函数WSAGetLastError 获取更多错误码,常见错误码如下:

  • WSANOTINITIALISED: 在调用本API 之前应成功调用WSAStartup()。

  • WSAENETDOWN: 网络子系统失效。

  • WSA_NOT_ENOUGH_MEMORY: 无足够内存完成该操作。

  • WSA_INVALID_HANDLE:lphEvents 数组中的一个或多个值不是合法的事件对象句柄。

  • WSA_INVALID_PARAMETER:cEvents参数未包含合法的句柄数目。

  • (4)检测所指定套接字上发生网络事件,然后处理发生的网络事件,完毕继续在事件对 象上等待。检测所指定套接字上发生网络事件是通过函数WSAEnumNetworkEvents 来实现, 该函数声明如下:

cpp 复制代码
#include <winsock2.h>
#pragma comment(lib, "Ws232.lib")

int WSAAPI WSAEnumNetworkEvents(
    SOCKET s,
    WSAEVENT hEventObject,
    LPWSANETWORKEVENTS lpNetworkEvents);
  • s: 套接字描述符。
  • hEventObject: 标识要重置的关联事件对象的可选句柄。
  • lpNetworkEvents: 指 向WSANETWORKEVENTS 结构的指针,该结构由发生的网络 事件和任何相关错误代码的记录填充。

如果操作成功,函数返回值为零;否则,将返回值SOCKET ERROR, 并且可以通过调用WSAGetLastError来获取特定的错误码。

以上4步是使用事件选择模型的基本步骤。下面我们看一个实例。

服务端

cpp 复制代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS


#include <winsock2.h>
#include <Windows.h>
#include <iostream>
#pragma comment(lib,"ws2_32.lib")

using std::cout;
using std::cin;
using std::endl;
using std::ends;

void WSAEventServerSocket()
{
	SOCKET server = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (server == INVALID_SOCKET) {
		cout << "创建SOCKET失败!,错误代码:" << WSAGetLastError() << endl;
		return;
	}

	int error = 0;
	sockaddr_in addr_in;
	addr_in.sin_family = AF_INET;
	addr_in.sin_port = htons(6000);
	addr_in.sin_addr.s_addr = INADDR_ANY;
	error = ::bind(server, (sockaddr*)&addr_in, sizeof(sockaddr_in));
	if (error == SOCKET_ERROR) {
		cout << "绑定端口失败!,错误代码:" << WSAGetLastError() << endl;
		return;
	}

	listen(server, 5);
	if (error == SOCKET_ERROR) {
		cout << "监听失败!,错误代码:" << WSAGetLastError() << endl;
		return;
	}
	cout << "成功监听端口 :" << ntohs(addr_in.sin_port) << endl;

	WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];        // 事件对象数组
	SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS];            // 事件对象数组对应的SOCKET句柄
	int nEvent = 0;                    // 事件对象数组的数量 

	WSAEVENT event0 = ::WSACreateEvent();
	::WSAEventSelect(server, event0, FD_ACCEPT | FD_CLOSE);
	eventArray[nEvent] = event0;
	sockArray[nEvent] = server;
	nEvent++;

	while (true) {
		int nIndex = ::WSAWaitForMultipleEvents(nEvent, eventArray, false, WSA_INFINITE, false);
		if (nIndex == WSA_WAIT_IO_COMPLETION || nIndex == WSA_WAIT_TIMEOUT) {
			cout << "等待时发生错误!错误代码:" << WSAGetLastError() << endl;
			break;
		}
		nIndex = nIndex - WSA_WAIT_EVENT_0;
		WSANETWORKEVENTS event;
		SOCKET sock = sockArray[nIndex];
		::WSAEnumNetworkEvents(sock, eventArray[nIndex], &event);
		if (event.lNetworkEvents & FD_ACCEPT) {
			if (event.iErrorCode[FD_ACCEPT_BIT] == 0) {
				if (nEvent >= WSA_MAXIMUM_WAIT_EVENTS) {
					cout << "事件对象太多,拒绝连接" << endl;
					continue;
				}
				sockaddr_in addr;
				int len = sizeof(sockaddr_in);
				SOCKET client = ::accept(sock, (sockaddr*)&addr, &len);
				if (client != INVALID_SOCKET) {
					cout << "接受了一个客户端连接 " << inet_ntoa(addr.sin_addr) << ":" << ntohs(addr.sin_port) << endl;
					WSAEVENT eventNew = ::WSACreateEvent();
					::WSAEventSelect(client, eventNew, FD_READ | FD_CLOSE | FD_WRITE);
					eventArray[nEvent] = eventNew;
					sockArray[nEvent] = client;
					nEvent++;
				}
			}
		}
		else if (event.lNetworkEvents & FD_READ) {
			if (event.iErrorCode[FD_READ_BIT] == 0) {
				char buf[2500];
				ZeroMemory(buf, 2500);
				int nRecv = ::recv(sock, buf, 2500, 0);
				if (nRecv > 0) {
					cout << "收到一个消息 :" << buf << endl;
					char strSend[] = "hi,client,I am server, I recvived your message.";
					::send(sock, strSend, strlen(strSend), 0);
				}
			}
		}
		else if (event.lNetworkEvents & FD_CLOSE) {
			::WSACloseEvent(eventArray[nIndex]);
			::closesocket(sockArray[nIndex]);
			cout << "一个客户端连接已经断开了连接" << endl;
			for (int j = nIndex; j < nEvent - 1; j++) {
				eventArray[j] = eventArray[j + 1];
				sockArray[j] = sockArray[j + 1];
			}
			nEvent--;
		}
		else if (event.lNetworkEvents & FD_WRITE) {
			cout << "一个客户端连接允许写入数据" << endl;
		}
	} // end while
	::closesocket(server);
}

int main(){
	WSADATA wsaData;
	int error;
	WORD wVersionRequested;
	wVersionRequested = WINSOCK_VERSION;
	error = WSAStartup(wVersionRequested, &wsaData);
	if (error != 0) {
		WSACleanup();
		return 0;
	}

	WSAEventServerSocket();

	WSACleanup();
	return 0;
}

客户端

cpp 复制代码
#define _WINSOCK_DEPRECATED_NO_WARNINGS

#include<stdlib.h>
#include<WINSOCK2.H>
#include <windows.h> 
#include <process.h>  

#include<iostream>
#include<string>

using namespace std;

#define BUF_SIZE 64
#pragma comment(lib,"WS2_32.lib")


void recv(PVOID pt)
{
	SOCKET  sHost = *((SOCKET*)pt);

	while (true)
	{
		char buf[BUF_SIZE];//清空接收数据的缓冲区
		memset(buf, 0, BUF_SIZE);
		int retVal = recv(sHost, buf, sizeof(buf), 0);
		if (SOCKET_ERROR == retVal)
		{
			int  err = WSAGetLastError();
			//无法立即完成非阻塞Socket上的操作
			if (err == WSAEWOULDBLOCK)
			{
				Sleep(1000);
				//printf("\nwaiting  reply!");
				continue;
			}
			else if (err == WSAETIMEDOUT || err == WSAENETDOWN || err == WSAECONNRESET)//已建立连接
			{
				printf("recv failed!");
				closesocket(sHost);
				WSACleanup();
				return;
			}

		}

		Sleep(100);

		printf("\n%s", buf);
		//break;
	}
}


int main()
{
	WSADATA wsd;
	SOCKET sHost;
	SOCKADDR_IN servAddr;//服务器地址
	int retVal;//调用Socket函数的返回值
	char buf[BUF_SIZE];
	//初始化Socket环境
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		printf("WSAStartup failed!\n");
		return -1;
	}
	sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//设置服务器Socket地址
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中
	servAddr.sin_port = htons(6000);
	//计算地址的长度
	int sServerAddlen = sizeof(servAddr);



	//调用ioctlsocket()将其设置为非阻塞模式
	int iMode = 1;
	retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) & iMode);


	if (retVal == SOCKET_ERROR)
	{
		printf("ioctlsocket failed!");
		WSACleanup();
		return -1;
	}


	//循环等待
	while (true)
	{
		//连接到服务器
		retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));
		if (SOCKET_ERROR == retVal)
		{
			int err = WSAGetLastError();
			//无法立即完成非阻塞Socket上的操作
			if (err == WSAEWOULDBLOCK || err == WSAEINVAL)
			{
				Sleep(1);
				printf("check  connect!\n");
				continue;
			}
			else if (err == WSAEISCONN)//已建立连接
			{
				break;
			}
			else
			{
				printf("connection failed!\n");
				closesocket(sHost);
				WSACleanup();
				return -1;
			}
		}
	}


	unsigned long threadId = _beginthread(recv, 0, &sHost);//启动一个线程接收数据的线程   

	while (true)
	{
		//向服务器发送字符串,并显示反馈信息
		printf("input a string to send:\n");
		std::string str;
		//接收输入的数据
		std::cin >> str;
		//将用户输入的数据复制到buf中
		ZeroMemory(buf, BUF_SIZE);
		strcpy_s(buf, str.c_str());
		if (strcmp(buf, "quit") == 0)
		{
			printf("quit!\n");
			break;
		}

		while (true)
		{
			retVal = send(sHost, buf, strlen(buf), 0);
			if (SOCKET_ERROR == retVal)
			{
				int err = WSAGetLastError();
				if (err == WSAEWOULDBLOCK)
				{
					//无法立即完成非阻塞Socket上的操作
					Sleep(5);
					continue;
				}

				else
				{
					printf("send failed!\n");
					closesocket(sHost);
					WSACleanup();
					return -1;
				}
			}
			break;
		}
	}

	return 0;
}

参考书籍《Visual C++2017 网络编程实战》

相关推荐
nihuhui66621 分钟前
关于静态IP的总结
网络·tcp/ip
长安110837 分钟前
计算机网络----主要内容简介
网络·计算机网络·智能路由器
zephyr_zeng1 小时前
VsCode + EIDE + OpenOCD + STM32(野火DAP) 开发环境配置
c语言·c++·vscode·stm32·单片机·嵌入式硬件·编辑器
鹿屿二向箔1 小时前
72MHz的MCU能支持多大频率的传感器数据采样率?
服务器·网络·单片机
东隆科技2 小时前
从芯片到光网络:平面光波导技术(PLC)的核心优势与应用前景
网络·平面
Stack Overflow?Tan902 小时前
c++实现在同一台主机两个程序实现实时通信
开发语言·c++
@@永恒3 小时前
map&set
c++
AORO_BEIDOU3 小时前
科普|卫星电话有哪些应用场景?
网络·人工智能·安全·智能手机·信息与通信
小鹏编程3 小时前
【C++教程】C++中的基本数据类型
开发语言·c++·教程·少儿编程
熊峰峰3 小时前
C++第十节:map和set的介绍与使用
开发语言·c++