1.传统阻塞的Socket中存在的问题
- 每个Socket连接在Recv过程中,存在无数据一直阻塞问题;
(当然,这个可以通过ioctlsocket()函数修改Socket属性为非阻塞,但会产生WSAEWOULDBLOCK错误。) - 多个Socket管理不方便问题,不能在一个线程中同时监视多个Socket;
(注意,我们可以通过为每个Socket都创建一个线程的办法实现同步监视(即一Socket一线程的方法),但这样过于消耗系统资源。)
基于传统的Socket中存在的问题,我们就要就要设计出相应的解决方案。在Windows下,主要有以下5种I/O模型:
- Select模型;
- 异步选择(WSAAsyncSelect)模型;
- 事件选择(WSAEventSelect)模型;
- 重叠I/O(Overlapped I/O);
- 完成端口(Completion Port);
本文主要介绍最基本的Select模型。
2.Select模型设计思路
Select模型是Winsock中最常见的 I/O模型。核心便是利用 select 函数,实现对 I/O的管理。
每次调用Select之前,需将需要监听的Socket放入一个fd_set结构中,利用 select 函数来判断某Socket上是否有数据可读/可写,其(Select)内部通过轮询的方式,实现了同时等待多个套接字,当某个或者多个套接字满足可读写条件时,Select函数返回。这样,同时防止了在套接字处于非阻塞模式中时,产生WSAEWOULDBLOCK错误。
//fd_set 结构的定义如下:
typedef struct fd_set
{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE]; // 其实是将Socket放入数组中
} fd_set;
// Select模型实现 - 核心代码如下:
//(因与Linux下的Select相似,此处直接借用Linux下的C代码)
// C语言代码实现
while (1)
{
// 定义fd_set集合
fd_set client;
FD_ZERO(&client);
/* fill client 将所有需要监视的Socket填充进fd_set集合(内含Socket数组)
* 变量说明:client_count为client_set中所包含的Socket数量。
* 变量说明:client_set数组中是客户端已连接的Socket。
*/
for (i = 0; i < *(p->client_count);++i)
{
FD_SET(p->client_set[i] , &client);
}
/* call select
* 如果有Socket发生了事件,就将那些没有发生事件的Socket从fd_set集合中清除 (注意这里是把集合的地址传入的 &client)
*
* Q:Select内部是如何实现这种监视的?是通过Socket主动发送信号相应信号么?还是轮询?
* 这里留了这么一个疑问,如果哪位伙伴知道,请在下方留言 ~~ 感激 ~~
*/
ret = select(get_max(p->client_set , *(p->client_count)) + 1, &client , NULL , NULL , &time_val);
if ( SOCKET_ERROR == ret)
{
if (errno == EINVAL)
{
usleep(10);
continue;
}
printf("select fail![%s]\n" , strerror(errno));
break;
}
// check 查看返回的集合中,有哪些Socket还在,就说明有关于ta的事件发生。
for (i = 0 ; i < *(p->client_count); ++i)
{
if (FD_ISSET(p->client_set[i] , &client) > 0)
{
// recv
ret = recv(p->client_set[i] , buf , sizeof(buf) , 0);
if (DEF_STD_ERROR == ret || 0 == ret)
{
// TODO:clear current client
continue;
}
printf("recv data:%s\n" , buf);
}
}
// 处理完毕,下次循环将会重新填充fd_set集合,并再次轮询。
}
select 函数返回值:
select 成功完成后,会在 fdset 结构中,返回刚好有未完成的 I/O操作的所有套接字句柄的总量。
若超过 timeval 设定的时间,便会返回0。若 select 调用失败,都会返回 SOCKET_ERROR,应该调用 WSAGetLastError 获取错误码!
用 select 对套接字进行监视之前,必须将套接字句柄分配给一个fdset的结构集合,之后再来调用 select,便可知道一个套接字上是否正在发生上述的 I/O 活动。
Winsock 提供了下列宏操作,可用来针对 I/O活动,对 fdset 进行处理与检查:
● FD_CLR(s, *set):从set中删除套接字s。
● FD_ISSET(s, *set):检查s是否set集合的一名成员;如答案是肯定的是,则返回TRUE。
● FD_SET(s, *set):将套接字s加入集合set。
● FD_ZERO( * set):将set初始化成空集合。
3.select模式的优势和不足
优势:
1.可以同时对多个建立起来的套接字进行有序的管理。
不足:
1.每次调用Select之前都需要将所有Socket放入fd_set结构集合中,这样每次都需要遍历一次数组,但可能每次这个数组都没有变,或只有少部分变化;
2.Select函数返回时,并不知道是哪个Socket发生了事件,所以需要再次遍历Socket数组,找到发生事件的Socket,这个过程中会对没有发生事件的Socket也进行判断,而这个判断是无效的;
(所以,一次Select过程需要轮询遍历两次Socket数组,这样大大降低了CPU的有效效率。)
PS:以上内容如有不足,请多多指正! –netosoul