NIO:Selector 类用法

如本章第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 keyIter =

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

你可能感兴趣的:(NIO:Selector 类用法)