基本的原理是:
上面所说的数据结构其实就是fd_set
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
打开网络库并校验
创建服务器SOCKET
绑定服务器的IP和PORT
启动监听
调用select()函数
int WSAAPI select
(
int nfds, // 忽略,直接填0
fd_set *readfds, // 这是一个双向参数,将fd_set的副本传址进去,函数执行结束后,该参数代表的数据中仅包含有可读响应的socket(如需要执行recv或accept)
fd_set *writefds, // 同上,返回结果表示有可写响应的socket(进行send),可以填NULL
fd_set *exceptfds, // 通常,表示有异常的socket
const timeval *timeout // 最长等待时间,NULL表示完全阻塞(等到客户端有响应才会返回)
);
// 返回值:
// 0 表示客户端在等待时间内没有响应
// >0 表示客户端有有响应或请求
// SOCKET_ERROR 表示出错
结合select()函数的处理逻辑如下(伪代码):
while(1){
int res = select(...);
if(res == 0) {
// 表示客户端无响应
continue;
} else if (res > 0) {
// 表示有响应
// 遍历响应select函数第二个参数的fd_set
for(int i = 0; i < fdset.count; i++){
// 如果是服务器socket 调用accept 与新的客户端建立连接
if (fdset.array[i] == socketSever){
SOCKET newsocket = accept(socketSever, NULL, 0);
//将新的Socket装进全局的fdset
FD_SET(newsocket, &allfdset);
}
// 如果是客户端通信socket,接收数据recv
else {
int nRes = recv(fdset.array[i], buffer, len, 0);
printf(buffer);
// 如果检测到客户端正常下线则从全局fdset中删除以及应关闭全局fdset中的对应socket
}
}
} else {
// 出错处理
// 在winsock中 强制下线也是错误的一种
}
}
场景:
如何回答对select模型的认识?
首先select模型是用在服务器端的,客户端代码并没有什么改变。
而最基本的服务器端的代码逻辑为①创建socket(socket())、②绑定本地IP和端口号(bind)、③启动监听(Listen)、④接收来自客户端的连接(accept)、⑥连接建立成功后使用recv、send函数接收、发送数据(Unix系统上是read和send函数)
但存在一个问题:也即是accept函数以及recv函数都是阻塞的,而且是傻傻等待的那种,只要对方没有连接请求或发送数据,程序就卡死了
为了解决这个阻塞的问题,select模型就出现了:
ft_set以及相应的宏去操作fd_set,传递给select函数(参数2:关心可读socket,参数3:关心可写socket,参数4:关心异常socket)什么是事件机制?
答:
事件集合,程序员通过为某些行为或动作绑定一个事件(本质可能就是一个ID),我们通过调用相应的API函数创建事件;有信号;事件被响应后设置为无信号;Windows下的事件选择模型最具有代表性的API函数是WSAEventSelect(),可以简单理解为select()函数的升级版!
事件选择模型的逻辑:
创建事件WSACreateEvent(),目前创建的事件没有绑定任何操作或动作的,是一个单纯的内核对象
使用WSAEventSelect()绑定socket上的某个行为或操作并投递给操作系统监管,此时我们的程序可以去做其他的事情。因为这是一个异步的操作,就解决了select模型中select()函数本身执行阻塞的情况
通过绑定某个
socket[参数1]的某个动作[参数3]到某个事件[参数2]上,将事件、行为与socket联系起来
通常会创建一个结构如下:
struct fd_es_set
{
unsigned short count; // 表示事件数目
SOCKET sockall[WSA_MAXIMUM_WAIT_EVENTS]; // 表示socket数组
WSAEVENT evnetall[WSA_MAXIMUM_WAIT_EVENTS]; // 表示event数组,与socket数组中相同下表处的元素一一对应
};
服务器如果接收了新的连接,就会产生新的
客户端通信Socket,然后创建新的事件并绑定到该socket之后投递给系统然后再添加到
fd_es_set中为什么一定要创建
fd_es_set呢?回答:为了获取有信号的事件,并进一步处理相应的socket,然后做出正确的动作
int WSAAPI WSAEventSelect
(
SOCKET s, // 某个socket
WSAEVENT hEventObject, // 某个事件对象
long lNetworkEvents // 某个动作 如:ACCEPT READ WRITE
);
查询事件是否有信号—WSAWaitForMultipleEvents()
一般的用法是循环的去询问~
函数返回值指明有信号的事件在事件列表中的索引(其实是
返回值 - WSA_WAIT_EVENT_0)事件列表是参数2
DWORD WSAAPI WSAWaitForMultipleEvents(
DWORD cEvents, // 有效事件数目
const WSAEVENT *lphEvents, // 事件列表
BOOL fWaitAll, // true表示等待所有的事件有信号再返回 false表示任意一个事件有信号就返回
DWORD dwTimeout, // 超时时间或者一直等待或者立即返回
BOOL fAlertable // 重叠I/O模型填TRUE 否则填FALSE
);
有信号时就分类处理WSAEnumNetworkEvents()
注意:
WSAWaitForMultipleEvents()只会返回一个事件索引,如果同时有多个事件有信号,会返回索引最小的事件获得有信号的事件以及对应的
socket后,还需要知道是什么操作触发了信号,因此要使用WSAEnumNetworkEvents()函数该函数通过参数3
lpNetworkEvents返回出信号的事件类型,也即具体操作
int WSAAPI WSAEnumNetworkEvents(
SOCKET s, // 有信号的事件绑定的socket
WSAEVENT hEventObject, // 有信号的事件
LPWSANETWORKEVENTS lpNetworkEvents // 这是一个指针,本质是一个传出参数,其中返回了触发信号的具体操作
);
之后就是基于
NetWorkEvents的分类逻辑处理
关于内核对象:
①内核对象由操作系统在内核中申请,由操作系统进行访问,也就是说所有对内核对象的操作都需要经过系统调用
②创建和释放都需要调用相应的函数,如果没有释放,就会造成内核资源泄露,只能重启电脑才能解决
③常见的windows内核对象,socket、event、file、thread、Mutex、信号量等
如何回答对事件选择模型的理解?
事件选择模型是基于事件机制的,事件机制的核心是事件集合,该模型是简单的select模型的升级版,解决了select模型中select()执行阻塞的问题。
通过将套接字socket和事件event以及相应的行为如,有客户端连接、socket可写可读等绑定在一起并投递给操作系统,让操作系统进行监测,如果有相应的行为或事件发生,该事件就会被置为有信号状态。
之后通过相应的API函数,获取到有信号的事件以及对应的socket。然后再调用API函数进一步获知触发事件的具体行为。
最后根据这些行为进行对应的业务处理,是该accept接收连接还是发送或接收数据等。
其中最关键的是将事件交给操作系统监测与程序执行本身是异步的,这就解决了select模型中select函数执行阻塞的问题。

推荐阅读:(二)Windows网络模型之异步选择模型(基于消息机制)