Java I/O (4): Selector in AIO and NIO

Among the three cores of Java NIO, except for Channel and Buffer, the rest is Selector. In some places it is called a selector, and in some places it is called a multiplexer (such as Netty).

As mentioned before, data is always read from Channel to Buffer, or written from Buffer to Channel, and a single thread can monitor multiple Channels—the Selector is the implementation mechanism behind this thread (hence the name Selector).

Selector handles multiple Channels by controlling a single thread. If the application opens multiple Channels, but the traffic transmitted each time is very low, it will be very convenient to use Selector (as for why, we will analyze it in Netty). So the benefits of using Selector are obvious: use the least resources to achieve the most operations, avoiding the overhead caused by thread switching.

Still take the code as an example to demonstrate the function of Selector. Create a new class and enter the following code in the main() method:

/**
 * Selector in NIO
 *
 * @author xiangwang
 */
public class TestSelector {
    public static void main(String args[]) throws IOException {
        // create ServerSocketChannel
        ServerSocketChannel channel1 = ServerSocketChannel.open();
        channel1.socket().bind(new InetSocketAddress("127.0.0.1", 8080));
        channel1.configureBlocking(false);
        ServerSocketChannel channel2 = ServerSocketChannel.open();
        channel2.socket().bind(new InetSocketAddress("127.0.0.1", 9090));
        channel2.configureBlocking(false);

        // Create a Selector object
        Selector selector = Selector.open();
        // According to the literal meaning, it should be like this: selector.register(channel, event);
        // But it's actually like this: channel.register(selector, SelectionKey.OP_READ);
        // Four listening events:
        // OP_CONNECT (connect ready)
        // OP_ACCEPT (ready to receive)
        // OP_READ (read ready)
        // OP_WRITE (ready for writing)
        // Register the Channel to the Selector, once the event is triggered, the monitoring will end
        SelectionKey key1 = channel1.register(selector, SelectionKey.OP_ACCEPT);
        SelectionKey key2 = channel2.register(selector, SelectionKey.OP_ACCEPT);

        // Template code: When writing a program, most of the time is to add the corresponding business code in the template code
        while(true) {
            int readyNum = selector.select();
            if (readyNum == 0) {
                continue;
            }

            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            // poll
            for (SelectionKey key : selectedKeys) {
                Channel channel = key.channel();
                if (key.isConnectable()) {
                    if (channel == channel1) {
                        System.out.println("channel1 connection ready");
                    } else {
                        System.out.println("channel2 connection ready");
                    }
                } else if (key.isAcceptable()) {
                    if (channel == channel1) {
                        System.out.println("channel1 is ready to receive");
                    } else {
                        System.out.println("channel2 is ready to receive");
                    }
                }
                // Delete after triggering, not here
                // it.remove();
            }
        }
    }
}

After the code is written, start the ServerSocketChannel service, and you can see that I have started it successfully:

Then download a tool called SocketTest.jar on the Internet (beware of poisoning when downloading from some tool websites, if you are worried, you can private message me and give you the address), double-click to open it, and execute it as shown in the figure below:

Click "Connect" to see the changes:

Then click "Disconnect", then enter "9090", and then click "Connect" to try:

You can see that the result shows that the result has changed:

There are two connections, and three messages are printed: it means that the polling of the selector is working (because Set<SelectionKey> contains all the SelectionKeys that are listening). But the "ready to receive" listener event is executed only once and then no longer responds. If you are interested, you can execute the events of OP_READ and OP_WRITE to try it out.

Because Selector is single-threaded polling to monitor multiple Channels, what should we do if data needs to be transferred between Selectors (threads)? ——Pipe appeared. Pipe is a "pipeline" for data transfer between Selectors.

First look at a picture:

You can clearly see how it works.

Or use code to explain.

/**
 * Pipe in NIO
 *
 * @author xiangwang
 */
public class TestPipe {
    public static void main(String args[]) throws IOException {
        // open the pipe
        Pipe pipe = Pipe.open();

        // Write Buffer data to the pipeline
        Pipe.SinkChannel sinkChannel = pipe.sink();
        ByteBuffer buffer = ByteBuffer.allocate(32);
        buffer.put("ByteBuffer".getBytes());
        // switch to write mode
        buffer.flip();
        sinkChannel.write(buffer);

        // read data from the pipe
        Pipe.SourceChannel sourceChannel = pipe.source();
        buffer = ByteBuffer.allocate(32);
        sourceChannel.read(buffer);
        System.out.println(new String(buffer.array()));

        // close the pipe
        sinkChannel.close();
        sourceChannel.close();
    }
}

As I said before, synchronization refers to the completion of one task at a time in order, and the following tasks cannot be executed until the previous task is completed and has a result. Asynchronous means that after the previous task ends, it does not wait for the result of the task, but continues to execute the next task. After all the tasks are "executed", the result is obtained through the task's callback function. So asynchrony has greatly improved application performance. In order to more vividly explain what is asynchronous, you can do an experiment:

By calling the CompletableFuture.supplyAsync() method, it can be clearly observed that the "execute this step first" at position 2 will be displayed first, and then the code at position 1 will be executed. And this is the specific implementation of asynchrony.

In order to support asynchrony, NIO has been upgraded to NIO2, which is AIO. And AIO introduces a new concept of asynchronous Channel, and provides the implementation of asynchronous FileChannel and asynchronous SocketChannel. AIO's asynchronous SocketChannel is truly asynchronous non-blocking I/O. This is better illustrated with code:

/**
 * AIO client
 *
 * @author xiangwang
 */
public class AioClient {
    public void start() throws IOException, InterruptedException {
        AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
        if (channel.isOpen()) {
            // socket receive buffer recbuf size
            channel.setOption(StandardSocketOptions.SO_RCVBUF, 128 * 1024);
            // socket send buffer recbuf size
            channel.setOption(StandardSocketOptions.SO_SNDBUF, 128 * 1024);
            // keep long connection status
            channel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
            // connect to the server
            channel.connect(new InetSocketAddress(8080), null,
                    new AioClientHandler(channel));
            // block the main process
            for(;;) {
                TimeUnit.SECONDS.sleep(1);
            }
        } else {
            throw new RuntimeException("Channel not opened!");
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        new AioClient().start();
    }
}

/**
 * AIO client CompletionHandler
 *
 * @author xiangwang
 */
public class AioClientHandler implements CompletionHandler<Void, AioClient> {
    private final AsynchronousSocketChannel channel;
    private final CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
    private final BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

    public AioClientHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }
    @Override
    public void failed(Throwable exc, AioClient attachment) {
        throw new RuntimeException("channel not opened!");
    }
    @Override
    public void completed(Void result, AioClient attachment) {
        System.out.println("send message to server: ");
        try {
            // write input to buffer
            String line = input.readLine();
            channel.write(ByteBuffer.wrap(line.getBytes()));
            // The Java native method native in the operating system has written the data into the buffer
            // Only one buffer is needed here to receive
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (channel.read(buffer).get() != -1) {
                buffer.flip();
                System.out.println("from server: " + decoder.decode(buffer).toString());
                if (buffer.hasRemaining()) {
                    buffer.compact();
                } else {
                    buffer.clear();
                }
                // write input to buffer
                line = input.readLine();
                channel.write(ByteBuffer.wrap(line.getBytes()));
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

/**
 * AIO server
 *
 * @author xiangwang
 */
public class AioServer {
    public void start() throws InterruptedException, IOException {
        AsynchronousServerSocketChannel channel = AsynchronousServerSocketChannel.open();
        if (channel.isOpen()) {
            // socket accepts buffer recbuf size
            channel.setOption(StandardSocketOptions.SO_RCVBUF, 4 * 1024);
            // Port reuse, to prevent the process from terminating unexpectedly, the port is not released, and it fails when restarting
            // Because the process is killed directly, the socket is not explicitly closed to release the port, and it will wait for a period of time before re-using this pass
            // The solution is to use SO_REUSEADDR
            channel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            channel.bind(new InetSocketAddress(8080));
        } else {
            throw new RuntimeException("channel not opened!");
        }
        // handle client connection
        channel.accept(null, new AioServerHandler(channel));
        System.out.println("server started");
        // block the main process
        for(;;) {
            TimeUnit.SECONDS.sleep(1);
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        AioServer server = new AioServer();
        server.start();
    }
}

/**
 * AIO server CompletionHandler
 *
 * @author xiangwang
 */
public class AioServerHandler implements CompletionHandler<AsynchronousSocketChannel, Void> {
    private final AsynchronousServerSocketChannel serverChannel;
    private final CharsetDecoder decoder = Charset.defaultCharset().newDecoder();
    private final BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

    public AioServerHandler(AsynchronousServerSocketChannel serverChannel) {
        this.serverChannel = serverChannel;
    }
    @Override
    public void failed(Throwable exc, Void attachment) {
        // Handle the next client connection
        serverChannel.accept(null, this);
    }
    @Override
    public void completed(AsynchronousSocketChannel result, Void attachment) {
        // Handle the next client connection, similar to chain calls
        serverChannel.accept(null, this);
        try {
            // write input to buffer
            String line = input.readLine();
            result.write(ByteBuffer.wrap(line.getBytes()));
            // The Java native method native in the operating system has written the data into the buffer
            // Only one buffer is needed here to receive
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            while (result.read(buffer).get() != -1) {
                buffer.flip();
                System.out.println("from client: " + decoder.decode(buffer).toString());
                if (buffer.hasRemaining()) {
                    buffer.compact();
                } else {
                    buffer.clear();
                }
                // write input to buffer
                line = input.readLine();
                result.write(ByteBuffer.wrap(line.getBytes()));
            }
        } catch (InterruptedException | ExecutionException | IOException e) {
            e.printStackTrace();
        }
    }
}

Execution of the test shows that both reading and writing are completely asynchronous, whether on the client or the server.

Guess you like

Origin blog.csdn.net/weixin_47367099/article/details/127485934