windows Socket编程之EventSelect网络模型

时间:2022-01-29 00:16:19

前一篇文章我们讲了select网络模型,它解决了有N个客户端连接,就有N+1个线程的问题,现在我们只需要2个线程就能搞定了。但是,我们的服务端仅仅是这样的话,它的效率还是非常低的。在select模型中,我们在工作者线程里边调用了select函数来判断是否有数据到来了(有信号了),如果没有信号,select就会卡在这里,虽然我们可以给它一个超时时间,但是它在底层还是调用了sleep函数。所以它浪费了CPU的时钟周期,由于我们不知道客户是什么发送数据过来,可能是1秒,可能是1小时,也有可能是1天,所有等待数据到来占了非常大的一部分时间,效率是比较低的。接下来我们来看下EventSelect模型,它要解决的问题就是将这个等待数据到来的时间给节省掉了。

首先,初始化网络环境,创建一个监听的socket,然后进行bind,listen操作。接下来我们会创建一个网络事件对象,它和我们讲内核态下线程同步里边事件对象很类似,我们可以调用WSACreateEvent来创建它,其声明如下:

WSAEVENT  WSACreateEvent (void);

然后我们再调用WSAEventSelect,来将监听的socket与该事件进行一个关联,其声明如下:

int WSAEventSelect(  
  SOCKET s,                 //套接字
  WSAEVENT hEventObject,    //网络事件对象
  long lNetworkEvents       //需要关注的事件
);
因为我们这里是将监听的socket与事件对象进行关联,因此我们只需要关注两个事件,一个是客户端的连接,一个是socket关闭这两个事件。然后,我们可以创建一个工作者线程了。最后将我们监听的socket和我们创建的那个网络事件对象保存到各自的全局数组里边去。这时候我们的主线程就结束了。

然后我们来看下我们的工作者线程做了哪些工作。在工作者线程里边,会有一个死循环,在循环刚开始的时候,会调用WSAWaitForMultipleEvents函数,来查看我们那个全局事件对象数组里边是否至少有一个有信号到来,其声明如下:

DWORD WSAWaitForMultipleEvents(  
  DWORD cEvents,                  //指定了事件对象数组里边的个数,最大值为64
  const WSAEVENT FAR *lphEvents,  //事件对象数组
  BOOL fWaitAll,                  //等待类型,TRUE表示要数组里全部有信号才返回,FALSE表示至少有一个就返回,这里必须为FALSE
  DWORD dwTimeout,                //等待的超时时间
  BOOL fAlertable                 //当系统的执行队列有I/O例程要执行时,是否返回,TRUE执行例程返回,FALSE不返回不执行,这里为FALSE
);
如果该函数执行成功,就返回一个索引值,这个值代表了那个有信号的事件对象的索引值。
我们在主线程里边关注了两个事件,一个是客户端的连接,一个是关闭事件,那我们如何知道出现了哪个网络事件呢,我需要调用WSAEnumNetworkEvents,来检测指定的socket上的网络事件。其声明如下:

int WSAEnumNetworkEvents
(  
  SOCKET s,                             //指定的socket
  WSAEVENT hEventObject,                //事件对象
  LPWSANETWORKEVENTS lpNetworkEvents    //WSANETWORKEVENTS<span style="font-family:Arial, Helvetica, sans-serif;">结构地址</span>
);
当我们调用这个函数成功后,它会将我们指定的socket和事件对象所关联的网络事件的信息保存到WSANETWORKEVENTS这个结构体里边去,我们来看下这个结构体的声明:

typedef struct _WSANETWORKEVENTS {
  long     lNetworkEvents;<span style="white-space:pre">			</span>//指定了哪个已经发生的网络事件
  int      iErrorCodes[FD_MAX_EVENTS];<span style="white-space:pre">		</span>//错误码
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
根据这个结构体我们就可以判断是否是我们所关注的网络事件已经发生了。如果是我们那个客户端连接的事件发生了,我们就调用accept函数将客户端和服务端进行连接。连接完成后,我们再次创建一个网络事件对象,然后继续调用WSAEventSelect函数,将这个事件对象和客户端的那个socket进行一个关联,此时我们需要关注的网络事件,数据的读和写,还有关闭这三个事件。然后我们就把客户端的socket和新建的事件对象保存到各自的全局数组里边去。

如果是我们的读的网络事件发生了,那么我们就调用recv函数进行操作。如果是写的网络事件发生了,我们就可以做一些日志等操作。若是关闭的事件发生了,就调用closesocket将socket关掉,在数组里将其置零等操作。

最后,在提一下另一个网络模型---异步选择模型,这个模型呢它和我们的事件选择模型很像,不过它是基于windows消息的,这就说明了我们只能在窗口程序里边来使用,而事件选择是以事件对象为基础,它不管是控制台还是窗口程序都可以使用,因此用的较多的也是我们事件选择模型。有兴趣的可以去了解一下异步选择模型。

以下是EventSelect网络模型的示例代码:

#include <winsock2.h>
#include <stdio.h>
#define PORT 6000
#pragma comment (lib, "Ws2_32.lib")
SOCKET ArrSocket[64] = { 0 };
WSAEVENT ArrEvent[64] = { 0 };
DWORD dwTotal = 0;
DWORD dwIndex = 0;
BOOL WinSockInit()
{
	WSADATA data = { 0 };
	if (WSAStartup(MAKEWORD(2, 2), &data))
		return FALSE;
	if (LOBYTE(data.wVersion) != 2 || HIBYTE(data.wVersion) != 2){
		WSACleanup();
		return FALSE;
	}
	return TRUE;
}

DWORD WINAPI WorkThreadProc(LPARAM lparam)
{
	char buf[1024] = { 0 };
	//用于ACCEPT临时使用的SOCKET
	SOCKET sockClient = INVALID_SOCKET;
	WSANETWORKEVENTS NetWorkEvent = { 0 };
	while (TRUE) 
	{		
		//数组内任意一个WSAEVENT有信号了,返回对应的索引值
		dwIndex = WSAWaitForMultipleEvents(dwTotal, ArrEvent, FALSE, 100, FALSE);
		if (dwIndex == WSA_WAIT_TIMEOUT) {
			continue;
		}
		//检测指定的socket的网络事件的发生
		WSAEnumNetworkEvents(ArrSocket[dwIndex - WSA_WAIT_EVENT_0], ArrEvent[dwIndex - WSA_WAIT_EVENT_0], &NetWorkEvent);//调用完成后NetWorkEvent保存了网络事件及一些标志位
		//如果第3位数据是1,代表有客户端进行连接
		if (NetWorkEvent.lNetworkEvents & FD_ACCEPT)
		{
			//如果出错了,就跳过			
			if (NetWorkEvent.iErrorCode[FD_ACCEPT_BIT] != 0)
			{
				continue;
			}
			sockClient = accept(ArrSocket[dwIndex - WSA_WAIT_EVENT_0], NULL, NULL);
			if (sockClient == INVALID_SOCKET)
				continue;
			//连接完成后,将客户端的SOCKET保存到数据,同时新建EVENT与SOCKET建立关系
			WSAEVENT newEvent = WSACreateEvent();
			WSAEventSelect(sockClient, newEvent, FD_READ | FD_WRITE | FD_CLOSE);
			
			ArrSocket[dwTotal] = sockClient;
			ArrEvent[dwTotal] = newEvent;
			++dwTotal;
		}

		if (NetWorkEvent.lNetworkEvents & FD_READ) 
		{
			if (NetWorkEvent.iErrorCode[FD_READ_BIT] != 0)
			{
				continue;
			}
			int len = recv(ArrSocket[dwIndex - WSA_WAIT_EVENT_0], buf, sizeof(buf), 0);
			printf("Recv: %s\n", buf);
			send(ArrSocket[dwIndex - WSA_WAIT_EVENT_0], buf, strlen(buf), 0);
		}

		if (NetWorkEvent.lNetworkEvents & FD_WRITE) 
		{
			if (NetWorkEvent.iErrorCode[FD_WRITE_BIT] != 0)
			{
				continue;
			}
			printf("Send something\n");
		}

		if (NetWorkEvent.lNetworkEvents & FD_CLOSE) 
		{
			if (NetWorkEvent.iErrorCode[FD_CLOSE_BIT] != 0)
			{
				continue;
			}
			closesocket(ArrSocket[dwIndex - WSA_WAIT_EVENT_0]);
			ArrSocket[dwIndex - WSA_WAIT_EVENT_0] = 0;							
		}
	}
}
int main()
{
	//初始化环境
	WinSockInit();
	SOCKET sockListen = INVALID_SOCKET;
	sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sockListen == INVALID_SOCKET)
	{
		printf("Create socket error");
		return -1;
	}
	sockaddr_in service;
	service.sin_family = AF_INET;
	service.sin_addr.S_un.S_addr = INADDR_ANY;
	service.sin_port = htons(PORT);
	//绑定
	if (bind(sockListen, (sockaddr*)&service, sizeof(service)) == SOCKET_ERROR)
	{
		printf("bind failed\n");
		return -1;
	}
	//监听
	if (listen(sockListen, SOMAXCONN) == SOCKET_ERROR) {
		printf("listen error\n");
		return -1;
	}

	//创建一个网络事件对象
	WSAEVENT ListenEvent = WSACreateEvent();
	// 把WSAEVENT与一个SOCKET进行关联,告诉关联的对象需要关注的事件有哪些
	WSAEventSelect(sockListen, ListenEvent, FD_ACCEPT | FD_CLOSE);
	//创建一个子进程,用子进程来处理所有的SOCKET上面的事件
	CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)WorkThreadProc, NULL, NULL, NULL);
	//保存到全局数组
	ArrSocket[dwTotal] = sockListen;
	ArrEvent[dwTotal] = ListenEvent;
	++dwTotal;

	system("pause");
	if (sockListen != INVALID_SOCKET)
	{
		closesocket(sockListen);
	}
	WSACleanup();
	return 0;
}