Detailed explanation of Java NIO (2)

Asynchronous IO
Asynchronous I/O is a method of reading and writing data without blocking. Typically, when code makes a read() call, the code blocks until there is data to read. Also, the write() call will block until data can be written, see another article Java IO for synchronous IO.

On the other hand, asynchronous I/O calls do not block, instead, you can register for specific I/O events such as data being read, a new connection coming, etc., and when such an event of interest occurs, the system will tell you.

One advantage of asynchronous I/O is that it allows you to perform I/O from a large number of inputs and outputs at the same time. Synchronizers often require help with polling, or create many threads to handle large numbers of connections. With asynchronous I/O, you can listen for events on any number of channels without polling and without additional threads.

Selector
has introduced Buffer and Channel in the three core objects of Java NIO in detail in my JavaNIO Detailed Explanation (1), and now we will focus on the third core object Selector. Selector is an object that can be registered on many Channels, monitor events that occur on each Channel, and can decide to read and write Channels according to the event. In this way, a large number of network connections can be handled by managing multiple Channels by one thread.

The benefits of using the Selector pattern
With the Selector, we can use a thread to handle all the channels. Switching between threads is expensive for the operating system, and each thread also consumes certain system resources. Therefore, it is better for the system to use as few threads as possible.

However, keep in mind that modern operating systems and CPUs are getting better at multitasking, so the overhead of multithreading is getting smaller and smaller over time. In fact, if a CPU has multiple cores, not using multitasking can be a waste of CPU power. Anyway, the discussion of that kind of design should be put in a different article. Here, it is enough to know that using Selector can handle multiple channels.

The following picture shows a thread processing 3 Channels:

How to create a Selector

The core object in asynchronous I/O is named Selector. The Selector is where you register your interest in various I/O events, and when those events occur, it's this object that tells you what happened.

Selector selector = Selector.open();

Then, you need to register the Channel to the Selector.

How to register Channel to Selector

In order for Channel to work with Selector, we need to register Channel with Selector. Registration is achieved by calling the channel.register()method :

channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);

Note that the registered Channel must be set to asynchronous mode , otherwise asynchronous IO will not work, which means that we cannot register a FileChannel to the Selector, because FileChannel does not have asynchronous mode, but SocketChannel in network programming is possible.

It should be noted that the second parameter of the register() method is an "interest set" , which means which times in the Channel the registered Selector is interested in. There are four types of events:

  1. Connect
  2. Accept
  3. Read
  4. Write

Channel triggering an event means that the event is Ready . So, a Channel successfully connected to another server is called Connect Ready. A ServerSocketChannel is called ready to accept new connections Accept Ready, a channel with data to read can be said to be Read Ready, and a channel waiting to write data can be said to be Write Ready.

The above four events correspond to the four constants in SelectionKey:

1. SelectionKey.OP_CONNECT
2. SelectionKey.OP_ACCEPT
3. SelectionKey.OP_READ
4. SelectionKey.OP_WRITE

If you are interested in multiple events, you can connect these constants with the or operator:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

About SelectionKey

Note that register()the return value of the call to is a SelectionKey . SelectionKey represents this registration of this channel on this Selector. When a Selector notifies you of an incoming event, it does so by providing the SelectionKey corresponding to the event. SelectionKey can also be used to unregister a channel. SelectionKey contains the following properties:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

Interest Set

As we mentioned earlier, register the Channel to the Selector to listen to the events of interest, and the interest set is the collection of events of interest that you want to select. You can read and write interest sets through the SelectionKey object:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;   

As can be seen from the above example, we can find the event we are interested in from the SelectionKey by doing the operation with the constant in the SelectionKey.

Ready Set

A ready set is a collection of operations for which a channel is ready. After a Selection, you should first access the ready set. Selection will be explained in the next subsection. The ready collection can be accessed like this:

int readySet = selectionKey.readyOps();
12

You can detect what events or actions are ready in the Channel in the same way as you detect the interest collection. However, the following four methods are also available, all of which return a boolean type:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel and Selector

We can get Selector and registered Channel through SelectionKey:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector(); 

Attach an object

An object or more information can be attached to the SelectionKey, which makes it easy to identify a given channel. For example, you can attach a Buffer used with a channel, or some object that contains aggregated data. The method of use is as follows:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

You can also attach objects when registering the Channel with the Selector using the register() method. Such as:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

Select channel by Selector

Once one or more channels are registered with the Selector, several overloaded select()methods can be called. These methods return those channels that are ready for the event you are interested in (such as connect, accept, read, or write). In other words, if you are interested in "Read Ready" channels, the select() method will return those channels that are ready for read events:

  • int select(): Block until at least one channel is ready on the event you registered
  • int select(long timeout): the same as select(), except that it will block the longest timeout milliseconds (parameter)
  • int selectNow(): Does not block, returns immediately no matter what channel is ready, this method performs a non-blocking selection operation. If no channel has become selectable since the previous select operation, this method simply returns zero.

The int value returned by the select() method indicates how many channels are ready. That is, how many channels have become ready since the last time the select() method was called. If the select() method is called, it returns 1 because one of the channels becomes ready, and if the select() method is called again, it will return 1 again if another channel is ready. If nothing was done on the first ready channel, there are now two ready channels, but between each select() method call, only one channel is ready.

selectedKeys()

Once the select()method is called, it will return a value indicating that one or more channels are ready, and then you can selector.selectedKeys()obtain the ready Channels by calling the SelectionKey collection returned by the method. Please see the demo method:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

When you register a Channel through Selector, the channel.register()method returns a SelectionKey object, which represents the Channel you registered. These objects can be obtained by selectedKeys()methods. You can get a ready Channel by iterating over these selected keys, here is the demo code:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) { 
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
    // a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
    // a connection was established with a remote server.
} else if (key.isReadable()) {
    // a channel is ready for reading
} else if (key.isWritable()) {
    // a channel is ready for writing
}
keyIterator.remove();
}

This loop iterates over each key in the set of selected keys and tests each key to see which Channel is ready.

Note the keyIterator.remove()method at the end of the loop. A Selector object does not automatically remove SelectionKey instances from its selected key collection. We need to remove it ourselves when we are done with a Channel. The next time the Channel is ready, the Selector will add it to the selected key collection again.

SelectionKey.channel()The Channel returned by the method needs to be converted to the type you want to deal with, such as ServerSocketChannel or SocketChannel, etc.

WakeUp()和Close()

A thread is blocked after calling the select() method, and there is a way for it to return from the select() method even if no channel is ready. Just let other threads call methods on the object on which the first thread called the select() method Selector.wakeup(). Threads blocked on the select() method will return immediately.

If another thread calls the wakeup() method, but no thread is currently blocked on the select() method, the next thread that calls the select() method will "wake up" immediately

When you are done using a Selector, call the Disuse close()method, which will close the Selector and invalidate all SelectionKey instances registered with the Selector. The channel itself does not close.

a complete example

Let's demonstrate the whole process above through an example of MultiPortEcho.

public class MultiPortEcho {
 private int ports[];
 private ByteBuffer echoBuffer = ByteBuffer.allocate(1024);
 public MultiPortEcho(int ports[]) throws IOException {
      this.ports = ports;
      go();
 }
 private void go() throws IOException {
      // 1. 创建一个selector,select是NIO中的核心对象
      // 它用来监听各种感兴趣的IO事件
      Selector selector = Selector.open();
      // 为每个端口打开一个监听, 并把这些监听注册到selector中
      for (int i = 0; i < ports.length; ++i) {
           //2. 打开一个ServerSocketChannel
           //其实我们没监听一个端口就需要一个channel
           ServerSocketChannel ssc = ServerSocketChannel.open();
           ssc.configureBlocking(false);//设置为非阻塞
           ServerSocket ss = ssc.socket();
           InetSocketAddress address = new InetSocketAddress(ports[i]);
           ss.bind(address);//监听一个端口
           //3. 注册到selector
           //register的第一个参数永远都是selector
           //第二个参数是我们要监听的事件
           //OP_ACCEPT是新建立连接的事件
           //也是适用于ServerSocketChannel的唯一事件类型
           SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);
           System.out.println("Going to listen on " + ports[i]);
      }
      //4. 开始循环,我们已经注册了一些IO兴趣事件
      while (true) {
           //这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时
           // select() 方法将返回所发生的事件的数量。
           int num = selector.select();
           //返回发生了事件的 SelectionKey 对象的一个 集合
           Set selectedKeys = selector.selectedKeys();
           //我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件
           //对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
           Iterator it = selectedKeys.iterator();
           while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                //5. 监听新连接。程序执行到这里,我们仅注册了 ServerSocketChannel
                //并且仅注册它们“接收”事件。为确认这一点
                //我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件
                if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
                     //6. 接收了一个新连接。因为我们知道这个服务器套接字上有一个传入连接在等待
                     //所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞
                     ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                     SocketChannel sc = ssc.accept();
                     sc.configureBlocking(false);
                     // 7. 讲新连接注册到selector。将新连接的 SocketChannel 配置为非阻塞的
                     //而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上
                     SelectionKey newKey = sc.register(selector,SelectionKey.OP_READ);
                     it.remove();
                     System.out.println("Got connection from " + sc);
                } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
                     // Read the data
                     SocketChannel sc = (SocketChannel) key.channel();
                     // Echo data
                     int bytesEchoed = 0;
                     while (true) {
                          echoBuffer.clear();
                          int r = sc.read(echoBuffer);
                          if (r <= 0) {
                               break;
                          }
                          echoBuffer.flip();
                          sc.write(echoBuffer);
                          bytesEchoed += r;
                     }
                     System.out.println("Echoed " + bytesEchoed + " from " + sc);
                     it.remove();
                }
           }
           // System.out.println( "going to clear" );
           // selectedKeys.clear();
           // System.out.println( "cleared" );
      }
 }
 static public void main(String args2[]) throws Exception {
      String args[]={"9001","9002","9003"};
      if (args.length <= 0) {
           System.err.println("Usage: java MultiPortEcho port [port port ...]");
           System.exit(1);
      }
      int ports[] = new int[args.length];
      for (int i = 0; i < args.length; ++i) {
           ports[i] = Integer.parseInt(args[i]);
      }
      new MultiPortEcho(ports);
 }
 }

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324684923&siteId=291194637