如本章第1节中提到的,Selector类可用于避免使用非阻塞式客户端中很浪费资源的"忙等"方法。例如,考虑一个即时消息服务器。可能有上千个客户端同时连接到了服务器,但在任何时刻都只有非常少量的(甚至可能没有)消息需要读取和分发。这就需要一种方法阻塞等待,直到至少有一个信道可以进行I/O操作,并指出是哪个信道。NIO的选择器就实现了这样的功能。一个Selector实例可以同时检查(如果需要,也可以等待)一组信道的I/O状态。用专业术语来说,选择器就是一个多路开关选择器,因为一个选择器能够管理多个信道上的I/O操作。
要使用选择器,需要创建一个Selector实例(使用静态工厂方法open())并将其注册(register)到想要监控的信道上(注意,这要通过channel的方法实现,而不是使用selector的方法)。最后,调用选择器的select()方法。该方法会阻塞等待,直到有一个或更多的信道准备好了I/O操作或等待超时。select()方法将返回可进行I/O操作的信道数量。现在,在一个单独的线程中,通过调用select()方法就能检查多个信道是否准备好进行I/O操作。如果经过一段时间后仍然没有信道准备好,select()方法就返回0,并允许程序继续执行其他任务。
下面来看一个例子。假设我们想要使用信道和选择器来实现一个回显服务器,并且不使用多线程和忙等。为了使不同协议都能方便地使用这个基本的服务模式,我们把信道中与具体协议相关的处理各种I/O的操作(接收,读,写)分离了出来。TCPProtocol定义了通用TCPSelectorServer类与特定协议之间的接口,包括三个方法,每个方法代表了一种I/O型式。当有信道准备好I/O操作时,服务器只需要调用相应的方法即可。
TCPProtocol.java
0 import java.nio.channels.SelectionKey;
1 import java.io.IOException;
2
3 public interface TCPProtocol {
4 void handleAccept(SelectionKey key) throws IOException;
5 void handleRead(SelectionKey key) throws IOException;
6 void handleWrite(SelectionKey key) throws IOException;
7 }
TCPProtocol.java
在服务器端创建一个选择器,并将其与每个侦听客户端连接的套接字所对应的ServerSocketChannel注册在一起。然后进行反复循环,调用select()方法,并调用相应的操作器例程对各种类型的I/O操作进行处理。
TCPServerSelector.java
0 import java.io.IOException;
1 import java.net.InetSocketAddress;
2 import java.nio.channels.SelectionKey;
3 import java.nio.channels.Selector;
4 import java.nio.channels.ServerSocketChannel;
5 import java.util.Iterator;
6
7 public class TCPServerSelector {
8
9 private static final int BUFSIZE = 256; // Buffer size
(bytes)
10 private static final int TIMEOUT = 3000; // Wait timeout
(milliseconds)
11
12 public static void main(String[] args) throws
IOException {
13
14 if (args.length < 1) { // Test for correct # of args
15 throw new IllegalArgumentException("Parameter(s):
16 }
17
18 // Create a selector to multiplex listening sockets and
connections
19 Selector selector = Selector.open();
20
21 // Create listening socket channel for each port and
register selector
22 for (String arg : args) {
23 ServerSocketChannel listnChannel =
ServerSocketChannel.open();
24 listnChannel.socket().bind(new
InetSocketAddress(Integer.parseInt(arg)));
25 listnChannel.configureBlocking(false); // must be
nonblocking to register
26 // Register selector with channel. The returned key is
ignored
27 listnChannel.register(selector,
SelectionKey.OP_ACCEPT);
28 }
29
30 // Create a handler that will implement the protocol
31 TCPProtocol protocol = new
EchoSelectorProtocol(BUFSIZE);
32
33 while (true) { // Run forever, processing available I/O
operations
34 // Wait for some channel to be ready (or timeout)
35 if (selector.select(TIMEOUT) == 0) { // returns # of
ready chans
36 System.out.print(".");
37 continue;
38 }
39
40 // Get iterator on set of keys with I/O to process
41 Iterator
selector.selectedKeys().iterator();
42 while (keyIter.hasNext()) {
43 SelectionKey key = keyIter.next(); // Key is bit mask
44 // Server socket channel has pending connection
requests?
45 if (key.isAcceptable()) {
46 protocol.handleAccept(key);
47 }
48 // Client socket channel has pending data?
49 if (key.isReadable()) {
50 protocol.handleRead(key);
51 }
52 // Client socket channel is available for writing and
53 // key is valid (i.e., channel not closed)?
54 if (key.isValid() && key.isWritable()) {
55 protocol.handleWrite(key);
56 }
57 keyIter.remove(); // remove from set of selected keys
58 }
59 }
60 }
61 }
TCPServerSelector.java
1.设置:第14-19行
验证至少有一个参数,创建一个Selector实例。
2.为每个端口创建一个ServerSocketChannel:第22-28行
创建一个ServerSocketChannel实例:第23行
使其侦听给定端口:第24行
需要获得底层的ServerSocket,并以端口号作为参数调用其bind()方法。任何超出适当数值范围的参数都将导致抛出IOException异常。
配置为非阻塞模式:第25行
只有非阻塞信道才可以注册选择器,因此需要将其配置为适当的状态。
为信道注册选择器:第27行
在注册过程中指出该信道可以进行"accept"操作。
3.创建协议操作器:第31行
为了访问回显协议中的操作方法,创建了一个EchoSelectorProtocol实例。该实例包含了需要用到的方法。
4.反复循环,等待I/O,调用操作器:第33-59行
选择:第35行
这个版本的select()方法将阻塞等待,直到有准备好I/O操作的信道,或直到发生了超时。该方法将返回准备好的信道数。返回0表示超时,这时程序将打印一个点来标记经过的时间和迭代次数。
获取所选择的键集:第41行
调用selectedKeys()方法返回一个Set实例,并从中获取一个Iterator。该集合中包含了每个准备好某一I/O操作的信道的SelectionKey(在注册时创建)。
在键集上迭代,检测准备好的操作:第42-58行
对于每个键,检查其是否准备好进行accep()操作,是否可读或可写,并调用相应的操作器方法对每种情况进行指定的操作。
从集合中移除键:第57行
由于select()操作只是向Selector所关联的键集合中添加元素,因此,如果不移除每个处理过的键,它就会在下次调用select()方法是仍然保留在集合中,而且可能会有无用的操作来调用它。
TCPServerSelector的大部分内容都与协议无关,只有协议赋值那一行代码是针对的特定协议。所有协议细节都包含在了TCPProtocol接口的具体实现中。EchoSelectorProtocol类就实现了该回显协议的操作器。你可以轻松地为自其他协议编写自己的操作器,或在我们的回显协议操作器上进行改进。
EchoSelectorProtocol.java
0 import java.nio.channels.SelectionKey;
1 import java.nio.channels.SocketChannel;
2 import java.nio.channels.ServerSocketChannel;
3 import java.nio.ByteBuffer;
4 import java.io.IOException;
5
6 public class EchoSelectorProtocol implements
TCPProtocol {
7
8 private int bufSize; // Size of I/O buffer
9
10 public EchoSelectorProtocol(int bufSize) {
11 this.bufSize = bufSize;
12 }
13
14 public void handleAccept(SelectionKey key) throws
IOException {
15 SocketChannel clntChan = ((ServerSocketChannel)
key.channel()).accept();
16 clntChan.configureBlocking(false); // Must be
nonblocking to register
17 // Register the selector with new channel for read and
attach byte buffer
18 clntChan.register(key.selector(), SelectionKey.
OP_READ, ByteBuffer.allocate(bufSize));
19
20 }
21
22 public void handleRead(SelectionKey key) throws
IOException {
23 // Client socket channel has pending data
24 SocketChannel clntChan = (SocketChannel)
key.channel();
25 ByteBuffer buf = (ByteBuffer) key.attachment();
26 long bytesRead = clntChan.read(buf);
27 if (bytesRead == -1) { // Did the other end close?
28 clntChan.close();
29 } else if (bytesRead > 0) {
30 // Indicate via key that reading/writing are both of
interest now.
31 key.interestOps(SelectionKey.OP_READ |
SelectionKey.OP_WRITE);
32 }
33 }
34
35 public void handleWrite(SelectionKey key) throws
IOException {
36 /*
37 * Channel is available for writing, and key is valid
(i.e., client channel
38 * not closed).
39 */
40 // Retrieve data read earlier
41 ByteBuffer buf = (ByteBuffer) key.attachment();
42 buf.flip(); // Prepare buffer for writing
43 SocketChannel clntChan = (SocketChannel)
key.channel();
44 clntChan.write(buf);
45 if (!buf.hasRemaining()) { // Buffer completely
written?
46 // Nothing left, so no longer interested in writes
47 key.interestOps(SelectionKey.OP_READ);
48 }
49 buf.compact(); // Make room for more data to be read
in
50 }
51
52 }32
EchoSelectorProtocol.java
1.声明实现TCPProtocol接口:第6行
2.成员变量和构造函数:第8-12行
每个实例都包含了将要为每个客户端信道创建的缓冲区大小。
3. handleAccept():第14-20行
从键中获取信道,并接受连接:第15行
channel()方法返回注册时用来创建键的Channel。(我们知道该Channel是一个ServerSocketChannel,因为这是我们注册的惟一一种支持"accept"操作的信道。)accept()方法为传入的连接返回一个SocketChannel实例。
设置为非阻塞模式:第16行
再次提醒,这里无法注册阻塞式信道。
为信道注册选择器:第18-19行
可以通过SelectionKey类的selector()方法来获取相应的Selector。我们根据指定大小创建了一个新的ByteBuffer实例,并将其作为参数传递给register()方法。它将作为附件,与register()方法所返回的SelectionKey实例相关联。(在此我们忽略了返回的键,但当信道准备好读数据的I/O操作时,可以通过选出的键集对其进行访问。)
4. handleRead():第22-33行
获取键关联的信道:第24行
根据其支持数据读取操作可知,这是一个SocketChannel。
获取键关联的缓冲区:第25行
连接建立后,有一个ByteBuffer附加到该SelectionKey实例上。
从信道中读数据:第27行
检查数据流的结束并关闭信道:第27-28行
如果read()方法返回-1,则表示底层连接已经关闭,此时需要关闭信道。关闭信道时,将从选择器的各种集合中移除与该信道关联的键。
如果接收完数据,将其标记为可写:第29-31行
注意,这里依然保留了信道的可读操作,虽然缓冲区中可能已经没有剩余空间了。
5. handleWrite():第35-50行
获取包含数据的缓冲区:第41行
附加到SelectionKey上的ByteBuffer包含了之前从信道中读取的数据。
准备缓冲区的写操作:第42行
Buffer的内部状态指示了在哪里放入下一批数据,以及缓冲区还剩多少空间。flip()方法用来修改缓冲区的内部状态,以指示write()操作从什么地方获取数据,以及还有剩余多少数据。(下一章将对其进行详细介绍。)该方法的作用是使写数据的操作开始消耗由读操作产生的数据。
获取信道:第43行
向信道写数据:第44行
如果缓冲区为空,则标记为不再写数据:第45-48行
如果缓冲区中之前接收的数据已经没有剩余,则修改该键关联的操作集,指示其只能进行读操作。
压缩缓冲区:第49行
如果缓冲区中还有剩余数据,该操作则将其移动到缓冲区的前端,以使下次迭代能够读入更多的数据(第5.4.5节将对这个操作的语义进行详细介绍)。在任何情况下,该操作都将重置缓冲区的状态,因此缓冲区又变为可读。注意,除了在handleWrite()方法内部,与信道关联的缓冲区始终是设置为可读的。
现在我们已经准备好对三大NIO 抽象的细节进行深入研究了。
相关下载:
Java_TCPIP_Socket编程(doc)
http://download.csdn.net/detail/undoner/4940239
文献来源:
UNDONER(小杰博客) :http://blog.csdn.net/undoner
LSOFT.CN(琅软中国) :http://www.lsoft.cn