• 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):

    <Port> ...");

    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<SelectionKey> 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

  • 相关阅读:
    人生小悟1
    对偶传播神经网络(CPN)
    对偶传播神经网络(CPN)
    学习向量量化神经网络
    学习向量量化神经网络
    自组织特征映射神经网络(SOFM)
    自组织特征映射神经网络(SOFM)
    竞争学习的基本概念和原理
    竞争学习的基本概念和原理
    人工神经网络基础概念、原理知识(补)
  • 原文地址:https://www.cnblogs.com/wuyida/p/6301058.html
Copyright © 2020-2023  润新知