JAVANIO在Socket 通讯中的应用
1 引言
用Java平台编写Socket(套接字)系统,因为输入输出都必须同步。这样,对于多客户端/ 服务器模式,不得不
使用多线程. 即为每个连接的客户都分配一个线程来处理输入输出,其线程与客户机之比几乎为1∶1,所以易受
到大量线程开销的影响,结果既导致了性能问题又缺乏可伸缩性。为解决这个问题,java平台的制订者引入了非
阻塞I/ O机制。
2 以前的Socket 编程
以前的Socket 编程从服务器端套接字开始,下面我们看一个典型的C/ S系统中服务器端的设计:
创建新的ServerSocket :
ServerSocket s=newServerSocket() ;
接着,接受传入调用。调用accept()完成接入:
Socket conn=s. accept() ; / / 程序在此阻塞,直到服务器接受了一个客户机请求一旦建立连接服务器开始与
客户端进行数据流交换。
while (true) {
Socket socket ;
socket = s. accept() ; / / 取得客户请求,如果没有线程在此处阻塞
printStreamos = newPrintStream(newBufferedOutputStream(socket. getOutputStream())) ;
BufferedReader is = newBufferedReader(newInputStreamReader(clientSocket. getInputStream())) ;
/ / 创建输入输出流,直到缓冲区满才成批的读取或发送数据
. . . . . . .
}
在此模式中,线程将在读或写时阻塞,一直到读或写操作彻底完成。如果在读的时候,数据尚未完全到达套
接字,则线程将在读操作上阻塞,一直到数据可用。我们一般是大量的使用线程来处理阻塞问题,但这个解决办
法会产生负效应:线程开销同时影响性能和可伸缩性,为此java的制订者推出了异步通讯机制来解决这些问题。
3 NIO结构
NIO(NewI/ O) 设计的原理是反应器设计模式(Reactor pattern) 。分布式系统中的服务器应用程序必须处理
收稿日期:2003204218
第18卷第3期
2003年9月
成 都 信 息 工 程 学 院 学 报
JOURNALOFCHENGDUUNIVERSITYOFINFORMATIONTECHNOLOGY
Vol. 18No. 3
Sep. 2003
© 1994-2010 China Academic Journal Electronic Publishing House. All rights reserved. http://www.cnki.net
多个向它们发送服务请求的客户机。然而,在调用特定的服务之前,服务器应用程序必须将每个传入请求多路分
用并分派到各自相应的服务提供者。反应器模式正好适用于这一功能,它允许事件驱动应用程序将服务请求多
路分用并进行分派,然后,这些服务请求被并发地从一个或多个客户机传送到应用程序。NIO就是根据此机理设
计而成。
NIO结构如图1所示:
图1 非阻塞Socket 结构
NIO实现了异步输入输出,这是指在进行输入输出处理时,它不必等到输入输出处理完毕才返回,就实现了
非阻塞。在服务器端,ServerSocketChannel 通过静态函数open()返回一个ServerSocketChannel 实例。然后调用
ServerSocketChannel. socket(). bind()绑定到服务器某端口,并调用register(Selector sel , SelectionKey.OP- ACCEPT)注
册OP- ACCEPT(接受连接)事件到一个选择器中(ServerSocketChannel 只可以注册OP- ACCEPT事件) 。当有客户
请求连接时,选择器就会通知该通道有客户连接请求,就可以进行相应的输入输出控制了;在客户端,客户端通道
实例注册自己感兴趣的事件后(可以是OP- CONNECT,OP- READ,OP- WRITE的组合) ,调用SocketChannel. connect
(InetSocketAddress )连接服务器然后进行相应处理。这里的连接是异步的,即不会等到服务器有相应立即返回而
继续执行后面的代码。
4 NIO的实现
4.1 创建NIO的连接
为了实现基础的非阻塞套接字读和写操作,首先我们要引入NIO包,接着建立连接。限于篇幅,我们仅列出
关键部分代码,客户端建立与服务器的连接:
Stringhost = 192.168.0.1;
InetSocketAddress socketAddress = newInetSocketAddress(host , 80) ;
SocketChannel channel = SocketChannel.open() ;/ / SocketChannel 执行实际读写操作
channel. connect(socketAddress) ;
channel. configureBlockingMethod(false) ;/ / 使通道成为非阻塞的
在非阻塞模式中,线程将读取已经可用的数据(不论多少) ,然后返回执行其它任务。如果将真(true)传递给
configureBlockingMethod() ,则通道的行为将与在Socket 上进行阻塞读或写时的行为完全相同。所不同的是这些阻
塞读和写可以被其它线程中断。
NIO中实现非阻塞I/ O数据的读取主要靠Selector 类的工作。
4.2 Selector的工作
在反应器模式情形中,Selector 类对多个SelectableChannels 的事件进行多路复用。每个Channel 向Selector 注
册事件。当事件从客户机处到来时,Selector 将它们多路分用并将这些事件分派到相应的Channel。
创建Selector 最简单的办法是使用open() 方法:
Selector selector = Selector.open() ;
9 5 2 第3期 王洁:JAVANIO在Socket 通讯中的应用
© 1994-2010 China Academic Journal Electronic Publishing House. All rights reserved. http://www.cnki.net
4.3 接受客户端的连接
要为每个客户机请求提供服务的Channel 创建一个连接。下面的代码创建称为Server 的ServerSocketChannel
并将它绑定到本地端口:
ServerSocketChannel serverChannel = ServerSocketChannel.open() ;
serverChannel. configureBlocking(false) ;
InetAddress ia = InetAddress. getLocalHost() ;
InetSocketAddress isa = newInetSocketAddress(ia, port ) ;
serverChannel. socket(). bind(isa) ;
每个要为客户机请求提供服务的Channel 都必须接着将自己向Selector 注册。Channel 应根据它将处理的事
件进行注册。接受传入连接的Channel 应这样注册:
SelectionKeyacceptKey = channel. register( selector,SelectionKey.OP- ACCEPT) ;
Channel 向Selector 的注册用SelectionKey对象表示。当Channel 被关闭,Selector 被关闭,调用Key的cancel()
时Key失效。
Selector 在select() 调用时阻塞。接着,它开始等待,直到建立了一个新的连接,或者另一个线程将它唤醒,或
者另一个线程将原来的阻塞线程中断。
4.4 服务器的工作
Server 是那个将自己向Selector 注册以接受所有传入连接的ServerSocketChannel :SelectionKeyacceptKey =
serverChannel. register(sel , SelectionKey.OP- ACCEPT) ;
while (acceptKey. selector(). select() > 0){
Server 被注册后,根据每个关键字(key)的类型以迭代方式对一组关键字进行处理。一个关键字被处理完成
后,就都被从就绪关键字(readykeys)列表中除去:
Set readyKeys = sel. selectedKeys() ;
Iterator it = readyKeys.iterator() ;
while (it. hasNext())
{
SelectionKeykey = (SelectionKey)it. next() ;
it. remove() ;
. . . .
}
如果关键字是可接受(acceptable)的,则接受连接,注册通道,以接受更多的事件(例如:读或写操作) 。如果关
键字是可读的(readable)或可写的(writable) ,则服务器会指示它已经就绪于读写本端数据:
SocketChannel socket ;
if (key.isAcceptable()) {
ServerSocketChannel schannel = (ServerSocketChannel) key. channel() ;
socket = (SocketChannel) schannel. accept() ;
socket. configureBlocking(false) ;
SelectionKeyakey = socket. register(sel ,SelectionKey.OP- READ| SelectionKey.OP- WRITE) ;
}
if (key.isReadable()) {
Stringtempstr = readline(key) ;
if (tempstr.length() > 0) {
0 6 2 成 都 信 息 工 程 学 院 学 报 第18卷
© 1994-2010 China Academic Journal Electronic Publishing House. All rights reserved. http://www.cnki.net
writeMessage(socket ,tempstr) ;
}
}
if (key.isWritable()) {
Stringtempstr = readline(key) ;
socket = (SocketChannel)key. channel() ;
if (result.length() > 0) {
writeline(socket ,tempstr) ;
}
}
5 结束语
NIO可以很好的解决以前Socket 编程中出现的问题,主要表现在有两方面:线程不再在读或写时阻塞,以及
Selector 能够处理多个连接,从而大幅降低了服务器应用程序开销。