Socket编程模型之事件选择模型

时间:2023-02-05 00:04:51

一 原理与关键函数

Winsock提供了另一个有用的异步I/O模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。对于表1总结的、由WSAAsyncSelect模型采用的网络事件来说,它们均可原封不动地移植到新模型。在用新模型开发的应用程序中,也能接收和处理所有那些事件。该模型最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。

跟WSAAsyncSelect类似,但是不是通过消息实现,而是通过事件对象。因为是基于select实现,一个线程也只能管理64个socket。事件选择模型是基于消息的。它允许程序通过Socket,接收以事件为基础的网络事件通知。事件选择模型相关函数主要有4个,第一个是:

WSAEVENT WSACreatEvent(void); 

它用来创建事件对象。如果函数成功,则返回值即是事件对象的句柄。如果函数失败,返回WSA_INVALID_EVENT。应用程序可通过调用WSAGetLastError()函数获取进一步的错误信息。主要的错误代码有:

WSANOTINITIALISED       //在调用本API之前应成功调用WSAStartup()。
WSAENETDOWN //网络子系统失效。
WSA_NOT_ENOUGH_MEMORY //无足够内存创建事件对象。

第2个是:

int WSAEventSelect(SOCKET s, WSAEVENT hEventObject,long  lNetworkEvents); 

WSAEventSelect模型是WindowsSockets提供的一个有用异步I/O模型。该模型允许在一个或者多个套接字上接收以事件为基础的网络事件通知。Windows Sockets应用程序在创建套接字后,调用WSAEventSelect()函数,将一个事件对象与网络事件集合关联在一起。当网络事件发生时,应用程序以事件的形式接收网络事件通知。
WSAEventSelect创建的事件拥有两种工作状态,以及两种工作模式。其中,两种工作状态分别是“已传信”(signaled)和“未传信”(non signaled)。工作模式则包括“人工重设”(manual reset)和“自动重设”(auto reset)。WSAEventSelect最开始在一种未传信的工作状态中,并用一种人工重设模式,来创建事件句柄。随着网络事件触发了与一个套接字关联在一起的事件对象,工作状态便会从“未传信”转变成“已传信”。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I/O请求的处理之后,我们的应用程序需要负责将工作状态从已传信更改为未传信。要做到这一点,可调用WSAResetEvent函数,对它的定义如下:BOOL WSAResetEvent(WSAEVENT hEvent);唯一的参数是前面用WSACreateEvent函数创建的事件对象句柄,成功返回TRUE,失败返回FALSE。当应用程序完成了对一个事件对象的处理后,应调用BOOL WSACloseEvent(WSAEVENT hEvent);函数释放由hEvent句柄占用的系统资源。成功返回TRUE,失败返回FALSE。

第3个是:

DWORD WSAAPI WSAWaitForMultipleEvents( DWORD cEvents,const WSAEVENT FAR * lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable );

只要指定事件对象中的一个或全部处于有信号状态,或者超时间隔到,则返回。cEvents:指出lphEvents所指数组中事件对象句柄的数目。事件对象句柄的最大值为WSA_MAXIMUM_WAIT_EVENTS。lphEvents:指向一个事件对象句柄数组的指针。fWaitAll:指定等待类型。若为真TRUE,则当lphEvents数组中的所有事件对象同时有信号时,函数返回。若为假FALSE,则当任意一个事件对象有信号时函数即返回。在后一种情况下,返回值指出是哪一个事件对象造成函数返回。dwTimeout:指定超时时间间隔(以毫秒计)。当超时间隔到,函数即返回,不论fWaitAll参数所指定的条件是否满足。如果dwTimeout为零,则函数测试指定的时间对象的状态,并立即返回。如果dwTimeout是WSA_INFINITE,则函数的超时间隔永远不会到。fAlertable:指定当系统将一个输入/输出完成例程放入队列以供执行时,函数是否返回。若为真TRUE,则函数返回且执行完成例程。若为假FALSE,函数不返回,不执行完成例程。请注意在Win16中忽略该参数。
如果函数成功,返回值指出造成函数返回的事件对象。(这一句有问题),应该改成:如果事件数组中有某一个事件被传信了,函数会返回这个事件的索引值,但是这个索引值需要减去预定义值 WSA_WAIT_EVENT_0才是这个事件在事件数组中的位置。如果函数失败,返回值为WSA_WAIT_FAILED。可调用WSAGetLastError()来获取进一步的错误信息。
错误代码:

WSANOTINITIALISED     //在调用本API之前应成功调用WSAStartup()。
WSAENETDOWN //网络子系统失效。
WSA_NOT_ENOUGH_MEMORY //无足够内存完成该操作。
WSA_INVALID_HANDLE //lphEvents数组中的一个或多个值不是合法的事件对象句柄。
WSA_INVALID_PARAMETER //cEvents参数未包含合法的句柄数目。


第4个函数:

int WSAAPI WSAEnumNetworkEvents ( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents, LPINT lpiCount);

检测所指定的套接口上网络事件的发生。s:标识套接口的描述字。hEventObject:(可选)句柄,用于标识需要复位的相应事件对象。lpNetworkEvents:一个WSANETWORKEVENTS结构的数组,每一个元素记录了一个网络事件和相应的错误代码。lpiCount:数组中的元素数目。在返回时,本参数表示数组中的实际元素数目;如果返回值是WSAENOBUFS,则表示为获取所有网络事件所需的元素数目。
如果操作成功则返回0。否则的话,将返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()来获取相应的错误代码。
错误代码:

WSANOTINITIALISED //在调用本API之前应成功调用WSAStartup()。
WSAENETDOWN //网络子系统失效。
WSAEINVAL //参数中有非法值。
WSAEINPROGRESS //一个阻塞的WinSock调用正在进行中,或者服务提供者仍在处理一个回调函数WSAENOBUFS 所提供的缓冲区太小。

二 完整实例与运行效果

示例仍然使用反射式服务端与反射式客户端。关于反射式客户端的代码请参见:http://blog.csdn.net/caoshiying/article/details/52550761第三章。关于本示例程序使用的公共组件请参见http://blog.csdn.net/caoshiying/article/details/52550761第四章。可以用类向导创建一个event_server_manager类型,头文件如下:

#pragma once

#include <iserver_manager.h>
#include <WinSock2.h>
#include <common_callback.h>

class event_server_manager:
protected iserver_manager
{
private:
WSADATA wsa;
int iclient_count;
int iaddr_size;
int iport;
SOCKET server;
SOCKET clients[FD_SETSIZE];
WSAEVENT mevents[FD_SETSIZE];
common_callback callback;
BOOL brunning;

private:
void cleanup(int index);

protected:
bool accept_by_crt();
bool accept_by_winapi();
void receive();

public:
void shutdown();
void start_accept_by_crt();
void start_accept_by_winapi();
void start_receive();
public:
event_server_manager();
virtual ~event_server_manager();
};
实现文件如下:

#include "event_server_manager.h"
#include <stdio.h>
#include <tchar.h>
#include <vector>
#include <cassert>

event_server_manager::event_server_manager()
{
WSAStartup(MAKEWORD(2, 2), &wsa);
iclient_count = 0;
iport = 5150;
iaddr_size = sizeof(SOCKADDR_IN);
brunning = FALSE;
callback.set_manager(this);
}

event_server_manager::~event_server_manager()
{
}

bool event_server_manager::accept_by_crt()
{
SOCKET conn_socket;
SOCKADDR_IN local;
SOCKADDR_IN conn_addr;
int iret = 0;

server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(iport);
do
{
iret = bind(server, (struct sockaddr*)&local, iaddr_size);
if (iret == 0)
break;
iport++;
local.sin_port = htons(iport);
} while (iret == SOCKET_ERROR);
listen(server, 3);
printf_s("服务已经启动,监听端口是:%d\n", iport);
while (brunning)
{
conn_socket = accept(server, (struct sockaddr*)&conn_addr, &iaddr_size);
if (conn_socket == INVALID_SOCKET)
{
printf_s("拒绝一个连接。\n");
continue;
}
printf_s("新连接%s:%d。\n", inet_ntoa(conn_addr.sin_addr), ntohs(conn_addr.sin_port));
clients[iclient_count] = conn_socket;
mevents[iclient_count] = WSACreateEvent();
WSAEventSelect(conn_socket, mevents[iclient_count], FD_ALL_EVENTS);
iclient_count++;
}
return true;
}

bool event_server_manager::accept_by_winapi()
{
return true;
}

void event_server_manager::receive()
{
DWORD iret = 0;
int index = 0;
WSANETWORKEVENTS nevent;
char message[1024] = { 0 };

while (brunning)
{
iret = WSAWaitForMultipleEvents(iclient_count, mevents, FALSE, 1000, FALSE);
if (iret == WSA_WAIT_FAILED || iret == WSA_WAIT_TIMEOUT)
continue;
index = iret - WSA_WAIT_EVENT_0;
WSAEnumNetworkEvents(clients[index], mevents[index], &nevent);
if (nevent.lNetworkEvents&FD_READ)
{
iret = recv(clients[index], message, 1024, 0);
if (iret == 0 || (iret == SOCKET_ERROR&&WSAGetLastError() == WSAECONNRESET))
cleanup(index);
else
{
message[iret] = 0;
send(clients[index], message, iret, 0);
}
}
if (nevent.lNetworkEvents&FD_CLOSE)
{
cleanup(index);
}
}
}

void event_server_manager::cleanup(int index)
{
SOCKADDR_IN client;

assert(index < iclient_count);
getpeername(clients[index], (struct sockaddr*)&client, &iaddr_size);
printf_s("客户端%s:%d断开连接。\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
closesocket(clients[index]);
WSACloseEvent(mevents[index]);
if (index < iclient_count - 1)
{
clients[index] = clients[iclient_count - 1];
mevents[index] = mevents[iclient_count - 1];
}
clients[iclient_count - 1] = 0;
mevents[iclient_count - 1] = INVALID_HANDLE_VALUE;
iclient_count--;
}

void event_server_manager::shutdown()
{
brunning = FALSE;
callback.shutdown();
for (int i = 0; i < iclient_count; i++)
{
closesocket(clients[i]);
WSACloseEvent(mevents[i]);
}
closesocket(server);
WSACleanup();
}

void event_server_manager::start_accept_by_crt()
{
brunning = TRUE;
callback.start_accept_by_crt();
}

void event_server_manager::start_accept_by_winapi()
{
brunning = TRUE;
callback.start_accept_by_winapi();
}

void event_server_manager::start_receive()
{
brunning = TRUE;
callback.start_receive();
}

int main(int argc, char** argv)
{
event_server_manager esm;

esm.start_accept_by_crt();
esm.start_receive();
printf_s("按任意键关闭服务端并退出程序。\n");
system("pause");
esm.shutdown();
return 0;
}

运行效果如下:
Socket编程模型之事件选择模型