同步、异步、阻塞和非阻塞是几种基本的sockets调用方式,也是在进行网络编程时须要理解和区分的基本概念之一。关于这方面的文章和讨论至关丰富,这里着重讨论其中两个比较容易混淆的两个,即非阻塞与异步的关系。 java
先仍是简单所列一下几中调用方式的常看法释: 程序员
同步:函数没有执行完不返回,线程被挂起; 编程
阻塞:没有收完数据函数不返回,线程也被挂起; 服务器
异步:函数当即返回,经过事件或是信号通知调用者; 网络
非阻塞:函数当即返回,经过select通知调用者 架构
同步和阻塞是比较容易弄明白其含义的,但在实际编程过程当中,异步与非阻塞的概念却并不能直观地区分于“经过事件或是信号通知调用者”与“经过select通知调用者”这种字面解释。 并发
阻塞通讯意味着通讯方法在尝试访问套接字或者读写数据时阻塞了对套接字的访问。在 JDK 1.4 以前,绕过 阻塞限制的方法是无限制地使用线程,但这样经常会形成大量的线程开销,对系统的性能和可伸缩性产生影响。java.nio 包改变了这种情况,容许服务器有效地使用 I/O 流,在合理的时间内处理所服务的客户请求。 异步
没有非阻塞通讯,这个过程就像我所喜欢说的“随心所欲”那样。基本上,这个过程就是发送和读取任何可以发送/读取的东西。若是没有能够读取 的东西,它就停止读操做,作其余的事情直到可以读取为止。当发送数据时,该过程将试图发送全部的数据,但返回实际发送出的内容。多是所有数据、部分数据 或者根本没有发送数据。 socket
阻塞与非阻塞相比确实有一些优势,特别是遇到错误控制问题的时候。在阻塞套接字通讯中,若是出现错误,该访问会自动返回标志错误的代码。错 误多是因为网络超时、套接字关闭或者任何类型的 I/O 错误形成的。在非阻塞套接字通讯中,该方法可以处理的惟一错误是网络超时。为了检测使用非阻塞通讯的网络超时,须要编写稍微多一点的代码,以肯定自从上一 次收到数据以来已经多长时间了。 ide
哪一种方式更好取决于应用程序。若是使用的是同步通讯,若是数据没必要在读取任何数据以前处理的话,阻塞通讯更好一些,而非阻塞通讯则提供了处理任何已经读取的数据的机会。而异步通讯,如 IRC 和聊天客户机则要求非阻塞通讯以免冻结套接字。
Java中的阻塞和非阻塞IO包各自的优劣思考
NIO 设计背后的基石:反应器模式,用于事件多路分离和分派的体系结构模式。
反应器(Reactor):用于事件多路分离和分派的体系结构模式
一般的,对一个文件描述符指定的文件或设备, 有两种工做方式: 阻塞 与非阻塞 。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 若是当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 若是没有东西可读, 或者不可写, 读写函数立刻返回, 而不会等待 。
一种经常使用作法是:每创建一个Socket链接时,同时建立一个新线程对该Socket进行单独通讯(采用阻塞的方式通讯)。这种方式具备很高的响应 速度,而且控制起来也很简单,在链接数较少的时候很是有效,可是若是对每个链接都产生一个线程的无疑是对系统资源的一种浪费,若是链接数较多将会出现资 源不足的状况。
另外一种较高效的作法是:服务器端保存一个Socket链接列表,而后对这个列表进行轮询,若是发现某个Socket端口上有数据可读时(读就绪), 则调用该socket链接的相应读操做;若是发现某个 Socket端口上有数据可写时(写就绪),则调用该socket链接的相应写操做;若是某个端口的Socket链接已经中断,则调用相应的析构方法关闭 该端口。这样能充分利用服务器资源,效率获得了很大提升。
传统的阻塞式IO,每一个链接必需要开一个线程来处理,而且没处理完线程不能退出。
非阻塞式IO,因为基于反应器模式,用于事件多路分离和分派的体系结构模式,因此能够利用线程池来处理。事件来了就处理,处理完了就把线程归还。而 传统阻塞方式不能使用线程池来处理,假设当前有10000个链接,非阻塞方式可能用1000个线程的线程池就搞定了,而传统阻塞方式就须要开10000个 来处理。若是链接数较多将会出现资源不足的状况。非阻塞的核心优点就在这里。
为何会这样,下面就对他们作进一步细致具体的分析:
首先,咱们来分析传统阻塞式IO的瓶颈在哪里。在链接数很少的状况下,传统IO编写容易方便使用。可是随着链接数的增多,问题传统IO就不行了。因 为前面说过,传统IO处理每一个链接都要消耗 一个线程,而程序的效率当线程数很少时是随着线程数的增长而增长,可是到必定的数量以后,是随着线程数的增长而减小。这里咱们得出结论,传统阻塞式IO的 瓶颈在于不能处理过多的链接。
而后,非阻塞式IO的出现的目的就是为了解决这个瓶颈。而非阻塞式IO是怎么实现的呢?非阻塞IO处理链接的线程数和链接数没有联系,也就是说处理 10000个链接非阻塞IO不须要10000个线程,你能够用1000个也能够用2000个线程来处理。由于非阻塞IO处理链接是异步的。当某个链接发送 请求到服务器,服务器把这个链接请求看成一个请求"事件",并把这个"事件"分配给相应的函数处理。咱们能够把这个处理函数放到线程中去执行,执行完就把 线程归还。这样一个线程就能够异步的处理多个事件。而阻塞式IO的线程的大部分时间都浪费在等待请求上了。
转载声明: 本文转自 http://blog.csdn.net/liuzhengkang/archive/2008/12/20/3562115.aspx
===========================================================================
非阻塞 Socoket 编程
在互联网至关普及的今天,在互联网上聊天对不少“网虫”来讲已是家常便 饭了。聊天室程序能够说是网上最简单的多点通讯程序。聊天室的实现方法有不少,但都是利用所谓的“多用户空间”来对信息进行交换,具备典型的多路I/O的 架构。一个简单的聊天室, 从程序员的观点来看就是在多个I/O端点之间实现多对多的通讯。其架构如图一所示。这样的实如今用户的眼里就是聊天室内任何一我的输入一段字符以后,其余 用户均可以获得这一句话。这种“多用户空间”的架构在其余多点通讯程序中应用的很是普遍,其核心就是多路I/O通讯。多路I/O通讯又被称为I/O多路复 用(I/OMultiplexing)通常被使用在如下的场合:
客户程序须要同时处理交互式的输入和同服务器之间的网络链接时须要处理I/O多路复用问题;
客户端须要同时对多个网络链接做出反应(这种状况不多见);
TCP服务器须要同时处理处于监听状态和多个链接状态的socket;
服务器须要处理多个网络协议的socket;
服务器须要同时处理不一样的网络服务和协议。
聊 天室所须要面对的状况正是第一和第三两种状况。咱们将经过在TCP/IP协议之上创建一个功能简单的聊天室让你们更加了解多路I/O以及它的实现方法。我 们要讨论的聊天室功能很是简单, 感兴趣的朋友能够将其功能扩展, 发展成一个功能比较完整的聊天室, 如加上用户认证, 用户昵称, 秘密信息, semote 等功能.
首先它是一个 client/server 结构的程序, 首先启动 server, 而后用户使用 client进行链接. client/server 结构的优势是速度快, 缺点是当 server 进行更新时,client 也必需更新.
网络初始化
首先是初始化 server, 使server 进入监听状态: (为了简洁起见,如下引用的程序与实际程序略有出入, 下同)
sockfd = socket( AF_INET,SOCK_STREAM, 0);
// 首先创建一个 socket, 族为 AF_INET, 类型为 SOCK_STREAM.
// AF_INET = ARPA Internet protocols 即便用 TCP/IP 协议族
// SOCK_STREAM 类型提供了顺序的, 可靠的, 基于字节流的全双工链接.
// 因为该协议族中只有一个协议, 所以第三个参数为 0
bind( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));
// 再将这个 socket 与某个地址进行绑定.
// serv_addr 包括 sin_family = AF_INET 协议族同 socket
// sin_addr.s_addr = htonl( INADDR_ANY) server 所接受的全部其余
// 地址请求创建的链接.
// sin_port = htons( SERV_TCP_PORT) server 所监听的端口
// 在本程序中, server 的 IP和监听的端口都存放在 config 文件中.
listen( sockfd, MAX_CLIENT);
// 地址绑定以后, server 进入监听状态.
// MAX_CLIENT 是能够同时创建链接的 client 总数.
server 进入 listen 状态后, 等待 client 创建链接。
Client端要创建链接首先也须要初始化链接:
sockfd = socket( AF_INET,SOCK_STREAM,0));
// 一样的, client 也先创建一个 socket, 其参数与 server 相同.
connect( sockfd, ( struct sockaddr *)&serv_addr, sizeof( serv_addr));
// client 使用 connect 创建一个链接.
// serv_addr 中的变量分别设置为:
// sin_family = AF_INET 协议族同 socket
// sin_addr.s_addr = inet_addr( SERV_HOST_ADDR) 地址为 server
// 所在的计算机的地址.
// sin_port = htons( SERV_TCP_PORT) 端口为 server 监听的端口.
当 client 创建新链接的请求被送到Server端时, server 使用 accept 来接受该
链接:
accept( sockfd, (struct sockaddr*)&cli_addr, &cli_len);
// 在函数返回时, cli_addr 中保留的是该链接对方的信息
// 包括对方的 IP 地址和对方使用的端口.
// accept 返回一个新的文件描述符.
在 server 进入 listen 状态以后, 因为已有多个用户在线,因此程序须要同时对这些用户进行操做,并在它们之间实现信息交换。这在实现上称为I/O多路复用技术。多路复用通常有如下几种方法:
非阻塞通讯方法:将文件管道经过fcntl()设为非阻塞通讯方式,每隔一端时间对他们实行一次轮询,以判断是否能够进行读写操做。这种方式的缺点是费用过高,大部分资源浪费在轮询上。
子进程方法:应用多个子进程,每个对一个单工阻塞方式通讯。全部子进程经过IPC和父进程进行通讯。父进程掌管全部信息。这种方式的缺点是实现复杂,并且因为IPC在各个操做系统平台上并不彻底一致,会致使可移植性下降。
信号驱动(SIGIO)的异步I/O方法:首先,异步I/O是基于信号机制的,并不可靠。其次单一的信号不足以提供更多的信息来源。仍是须要辅助以其余的手段,实现上有很高的难度。
select ()方法:在BSD中提供了一种能够对多路I/O进行阻塞式查询的方法——select()。它提供同时对多个I/O描述符进行阻塞式查询的方法,利用 它,咱们能够很方便的实现多路复用。根据统一UNIX规范的协议,POSIX也采用了这种方法,所以,咱们能够在大多数操做系统中使用select方法。
使用专门的I/O多路复用器:在“UNIX? SYSTEM V Programmer's Guide: STREAMS”一书中详细的说明了构造和使用多路复用器的方法。这里就再也不详述了。
咱们下面分别讨论多路I/O的两种实现方法:
1. 非阻塞通讯方法
对 一个文件描述符指定的文件或设备, 有两种工做方式: 阻塞与非阻塞。所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 若是当时没有东西可读,或者暂时不可写, 程序就进入等待状态, 直到有东西可读或者可写为止。而对于非阻塞状态, 若是没有东西可读, 或者不可写, 读写函数立刻返回, 而不会等待。缺省状况下, 文件描述符处于阻塞状态。在实现聊天室时, server 须要轮流查询与各client 创建的 socket,一旦可读就将该 socket 中的字符读出来并向全部其余client 发送。而且, server 还要随时查看是否有新的 client 试图创建链接,这样, 若是 server 在任何一个地方阻塞了, 其余client 发送的内容就会受到影响,得不到服务器的及时响应。新 client试图创建链接也会受到影响。因此咱们在这里不能使用缺省的阻塞的文件工做方式,而须要将文件的工做方式变成非阻塞方式。在UNIX下,函数 fcntl()能够用来改变文件I/O操做的工做方式,函数描述以下:
fcntl( sockfd, F_SETFL, O_NONBLOCK);
// sockfd 是要改变状态的文件描述符.
// F_SETFL 代表要改变文件描述符的状态
// O_NONBLOCK 表示将文件描述符变为非阻塞的.
为了节省篇幅咱们使用天然语言描述聊天室 server :
while ( 1)
if 有新链接 then 创建并记录该新链接;
for ( 全部的有效链接)
begin
if 该链接中有字符可读 then
begin
读入字符串;
for ( 全部其余的有效链接)
begin
将该字符串发送给该链接;
end;
end;
end;
end.
因为判断是否有新链接, 是否可读都是非阻塞的, 所以每次判断,无论有仍是没有, 都会立刻返回. 这样,任何一个 client 向 server 发送字符或者试图创建新链接,都不会对其余 client 的活动形成影响。
对 client 而言, 创建链接以后, 只须要处理两个文件描述符, 一个是创建了链接的socket 描述符, 另外一个是标准输入. 和 server 同样, 若是使用阻塞方式的话,很容易由于其中一个暂时没有输入而影响另一个的读入.. 所以将它们都变成非阻塞的,而后client 进行以下动做:
while ( 不想退出)
begin
if ( 与 server 的链接有字符可读)
begin
从该链接读入, 并输出到标准输出上去.
End;
if ( 标准输入可读)
Begin
从标准输入读入, 并输出到与 server 的链接中去.
End;
End.
上面的读写分别调用这样两个函数:
read( userfd[i], line, MAX_LINE);
// userfd[i] 是指第 i 个 client 链接的文件描述符.
// line 是指读出的字符存放的位置.
// MAX_LINE 是一次最多读出的字符数.
// 返回值是实际读出的字符数.
write( userfd[j], line, strlen( line));
// userfd[j] 是第 j 个 client 的文件描述符.
// line 是要发送的字符串.
// strlen( line) 是要发送的字符串长度.
分 析上面的程序能够知道, 无论是 server 仍是 client, 它们都不停的轮流查询各个文件描述符, 一旦可读就读入并进行处理. 这样的程序, 不停的在执行, 只要有CPU 资源, 就不会放过。所以对系统资源的消耗很是大。server 或者 client 单独执行时,CPU 资源的 98% 左右都被其占用。极大的消耗了系统资源。
select 方法所以,虽然咱们不但愿在某一个用户没有反应时阻塞其余的用户,但咱们却应该在没有任何用户有反应的状况之下中止程序的运行,让出抢占的系统资源,进入阻塞状态。有没有这种方法呢?如今的UNIX系统中都提供了select方法,具体实现方式以下:
select 方法中, 全部文件描述符都是阻塞的. 使用 select 判断一组文件描述符中是否有一个可读(写), 若是没有就阻塞, 直到有一个的时候就被唤醒. 咱们先看比较简单的 client 的实现:
因为 client 只须要处理两个文件描述符, 所以, 须要判断是否有可读写的文件描述符只须要加入两项:
FD_ZERO( sockset);
// 将 sockset 清空
FD_SET( sockfd, sockset);
// 把 sockfd 加入到 sockset 集合中
FD_SET( 0, sockset);
// 把 0 (标准输入) 加入到 sockset 集合中
而后 client 的处理以下:
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 的状况:
设置 sockset 以下:
FD_ZERO( sockset);
FD_SET( sockfd, sockset);
for ( 全部有效链接)
FD_SET( userfd[i], sockset);
}
maxfd = 最大的文件描述符号 + 1;
server 处理以下:
while ( 1)
select( maxfd, &sockset, NULL, NULL, NULL);
if ( FD_ISSET( sockfd, &sockset))
// 有新链接
创建新链接, 并将该链接描述符加入到 sockset 中去了.
}
for ( 全部有效链接)
if ( FD_ISSET ( userfd[i], &sockset))
// 该链接中有字符可读
从该链接中读入字符, 并发送到其余有效链接中去.
}
}
从新设置 sockset;
}
性能比较 由 于采用 select 机制, 所以当没有字符可读时, 程序处于阻塞状态,最小程度的占用CPU 资源, 在同一台机器上执行一个 server 和若干个client 时, 系统负载只有0.1左右, 而采用原来的非阻塞通讯方法, 只运行一个 server, 系统负载就能够达到1.5左右. 所以咱们推荐使用 select. 参考文献: [1] UNIX Network Programming Volume 1 W.Richard Stevens 1998 Prentice Hall [2] 计算机实用网络编程 汤毅坚 1993 人民邮电出版社 [3] UNIX? SYSTEM V RELEASE 4 Programmer's Guide:STREAMS AT&T 1990 Prentice Hall [4] UNIX? SYSTEM V RELEASE 4 Network Programmer's Guide AT&T 1990 Prentice Hall 全部源程序均登载在eDOC网站上,若有须要能够去http://edoc.163.net下载