1 Select模型
- select模型解决的问题是基本的C/S模型中,accept函数和recv函数傻等阻塞问题;
- 另外还使服务器可以与多个客户端连接,与多个客户端分别通信
- 除了傻等阻塞还有一种阻塞为执行阻塞,例如send()、recv()等函数执行的过程是执行阻塞的,但是这不是select模型解决的问题
-
基本的原理是:
- 首先:服务器这边有两种SOCKET;一种是我们自己主动创建的SOCKET,暂称为服务器SOCKET,另一种是接收到客户端来连接之后又accept()函数返回的SOCKET,暂称为客户端通信SOCKET
- 通过将服务器SOCKET和客户端通信SOCKET通通装进一个数据结构(数组)中,然后调用select函数,遍历数据结构中的SOCKET,当某个Socket有响应,再做相应的处理
- 如果是服务器SOCKET---->调用accept
- 如果是客户端通信SOCKET---->调用recv或send
-
上面所说的数据结构其实就是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;
服务器:(前4步与基本C/S模式一样)
-
打开网络库并校验
-
创建服务器SOCKET
-
绑定服务器的IP和PORT
-
启动监听
-
调用select()函数
- 一般可以放在一个循环之中,循环调用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中 强制下线也是错误的一种
}
}
客户端:同基本C/S一样
- 打开网络库并校验
- 创建SOCKET
- 连接到服务器
- 与服务器进行通信
总结:
- 所谓网络模型的不断优化,其本质就是在解决函数调用过程中的阻塞问题(傻等阻塞与执行阻塞)
- select模型解决的是基本C/S模型中的傻等阻塞(accept和recv)
- accept函数在没有客户端前来连接的时候会一直阻塞等待,也就是傻等阻塞
- recv函数进行通信,在对端没有发来数据的时候也就一直阻塞等待,也就是傻等阻塞
- 但是select函数本身也是阻塞的,这属于执行阻塞,recv和send在执行的时候造成的阻塞也是执行阻塞,比如数据有点多,在发送和接收数据时就会执行阻塞
- 通常在实际应用中用的最多的就是select函数的第2个参数,也即可读的socket响应
场景:
- 小用户量访问,几十、几百,简单方便。
如何回答对select模型的认识?
首先select模型是用在服务器端的,客户端代码并没有什么改变。
而最基本的服务器端的代码逻辑为①创建socket(socket())
、②绑定本地IP和端口号(bind)
、③启动监听(Listen)
、④接收来自客户端的连接(accept)
、⑥连接建立成功后使用recv、send函数接收、发送数据(Unix系统上是read和write函数)
但存在一个问题:也即是accept函数以及recv函数都是阻塞的,而且是傻傻等待的那种,只要对方没有连接请求或发送数据,程序就卡死了
为了解决这个阻塞的问题,select模型就出现了:
- 通过
ft_set
以及相应的宏去操作fd_set
,传递给select函数(参数2:关心可读socket,参数3:关心可写socket,参数4:关心异常socket) - 可以通过循环反复调用select函数,根据select的返回参数,确定是否存在需要处理的socket,如果有就进行相应的处理。
- 如此就解决了accept函数与recv/read函数等存在的阻塞傻等问题,优化了程序
2 事件选择模型(基于事件机制–核心是事件集合)
什么是事件机制?
答:
- Windows处理用户行为的两种方式之一
- 事件机制的核心是
事件集合
,程序员通过为某些行为或动作绑定一个事件(本质可能就是一个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网络模型之异步选择模型(基于消息机制)