I/O多路复用器两种实现方法:
对一个文件描述符指定的文件或设备, 有两种工作方式: 阻塞与非阻塞。
所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。
而对于非阻塞状态, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待。
缺省情况下, 文件描述符处于阻塞状态。例如,在实现聊天室时, server 需要轮流查询与各client 建立的 socket, 一旦可读就将该 socket 中的字符读出来并向所有其他client 发送。并且, server 还要随时查看是否有新的 client 试图建立连接,这样, 如果 server 在任何一个地方阻塞了, 其他 client 发送的内容就会受到影响,得不到服务器的及时响应。新 client 试图建立连接也会受到影响。
非阻塞通信方法
使用fcntl()函数,伪代码:
server端:client端:while ( 1)
if 有新连接 then 建立并记录该新连接;
for ( 所有的有效连接)
begin
if 该连接中有字符可读 then
begin
读入字符串;
for ( 所有其他的有效连接)
begin
将该字符串发送给该连接;
end;
end;
end;
end.
建立连接之后, 只需要处理两个文件描述符, 一个是建立了连接的 socket 描述符, 另一个是标准输入. 和 server 一样, 如果使用阻塞方式的话, 很容易因为其中一个暂时没有输入而影响另外一个的读入。 因此将它们都变成非阻塞的, 然后client 进行如下动作:while ( 不想退出)
begin
if ( 与 server 的连接有字符可读)
begin
从该连接读入, 并输出到标准输出上去.
End;
if ( 标准输入可读)
Begin
从标准输入读入, 并输出到与 server 的连接中去.
End;
End.分析上面的程序可以知道, 不管是 server 还是 client, 它们都不停的轮流查询各个文件描述符, 一旦可读就读入并进行处理. 这样的程序, 不停的在执行, 只要有CPU 资源, 就不会放过。因此对系统资源的消耗非常大。
select 方法这种I/O模型主要涉及以下几个函数及宏:#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);FD_ZERO(fd_set *set) – 清除一个文件描述符集合
FD_SET(int fd, fd_set *set) – 添加fd到集合
FD_CLR(int fd, fd_set *set) – 从集合中移去fd
FD_ISSET(int fd, fd_set *set) – 测试fd是否在集合中
虽然我们不希望在某一个用户没有反应时阻塞其他的用户,但我们却应该在没有任何用户有反应的情况之下停止程序的运行,让出抢占的系统资源,进入阻塞状态。有没有这种方法呢?现在的UNIX系统中都提供了select方法,具体实现方式如下:
select 方法中, 所有文件描述符都是阻塞的. 使用 select 判断一组文件描述符中是否有一个可读(写), 如果没有就阻塞, 直到有一个的时候就被唤醒. 我们先看比较简单的 client 的实现:
由于 client 只需要处理两个文件描述符, 因此, 需要判断是否有可读写的文件描述符只需要加入两项:
FD_ZERO( sockset);
// 将 sockset 清空
FD_SET( sockfd, sockset);
// 把 sockfd 加入到 sockset 集合中
FD_SET( 0, sockset);
// 把 0 (标准输入) 加入到 sockset 集合中
FD_CLR(sockfd, sockset)
// 从清除 sockset 集合中清除sockfdclient端:while ( 不想退出)
{
select( sockfd+1, &sockset, NULL, NULL, NULL);
// 此时该函数将阻塞直到标准输入或者 sockfd 中有一个可读为止
// 第一个参数是 0 和 sockfd 中的最大值加一
// 第二个参数是 读集, 也就是 sockset
// 第三, 四个参数是写集和异常集, 在本程序中都为空
// 第五个参数是超时时间, 即在指定时间内仍没有可读, 则出错
// 并返回. 当这个参数为NULL 时, 超时时间被设置为无限长.
// 当 select 因为可读返回时, sockset 中包含的只是可读的
// 那些文件描述符.
if ( FD_ISSET( sockfd, &sockset))
{
// FD_ISSET 这个宏判断 sockfd 是否属于可读的文件描述符
从 sockfd 中读入, 输出到标准输出上去.
}
if ( FD_ISSET( 0, &sockset))
{
// FD_ISSET 这个宏判断 sockfd 是否属于可读的文件描述符
从标准输入读入, 输出到 sockfd 中去.
}
重新设置 sockset. (即将 sockset 清空, 并将 sockfd 和 0 加入)
}
server服务端:
FD_ZERO( sockset);
FD_SET( sockfd, sockset);
for ( 所有有效连接)
FD_SET( userfd[i], sockset);
maxfd = 最大的文件描述符号 + 1;
while ( 1)
{
select( maxfd, &sockset, NULL, NULL, NULL);
if ( FD_ISSET( sockfd, &sockset))
{
// 有新连接
建立新连接, 并将该连接描述符加入到 sockset 中去了.
}
for ( 所有有效连接)
{
if ( FD_ISSET ( userfd[i], &sockset))
{
// 该连接中有字符可读
从该连接中读入字符, 并发送到其他有效连接中去.
}
}
重新设置 sockset;
}
由于采用 select 机制, 因此当没有字符可读时, 程序处于阻塞状态,最小程度的占用CPU 资源。
缺点:
对套接字数组扫描的效率问题,由于存在对套接字句柄的扫描处理,这个肯定会影响效率。看看是否有办法解决。
对客户端实时响应问题,程序在处理待决的套接字的时候,是逐个处理的,如果响应某个Client的时间长到一定程度的话,肯定会影响对其它客户端的响应。解决方法是当这个套接字处于可读的待决状态的话,产生一个子线程去处理(接收数据和处理数据)。这样主线程继续自己的工作,其它Client可以得及时的响应;当然当有大量的Client请求时,对线程的控制会成为一个新问题。
原文参考:
http://fish.cndev.org/2005/02/24/7287/
评论