Deep understanding of NIO

Preface

Based on the Server side of BIO implementation, how many threads will there be when 100 connections are established? How many threads will there be based on NIO?

BIO

The so-called BIO is the most traditional socket link, for example:

int port = 4343; //Port number
// Socket server (simple sending information)
Thread sThread = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (true) {
                // Waiting for connection
                Socket socket = serverSocket.accept();
                Thread sHandlerThread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try (PrintWriter printWriter = new PrintWriter(socket.getOutputStream())) {
                            printWriter.println("hello world!");
                            printWriter.flush();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
                sHandlerThread.start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});
sThread.start();

// Socket client (receiving information and printing)
try (Socket cSocket = new Socket(InetAddress.getLocalHost(), port)) {
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(cSocket.getInputStream()));
    bufferedReader.lines().forEach(s -> System.out.println("Client:" + s));
} catch (UnknownHostException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

The flow chart is roughly as follows

So there will be 101 threads, one accept thread and 100 link threads.

It's not complicated.

NIO

In fact, we don't know how to write NIO code ourselves, but we can use excellent open source libraries like netty.

This is the schematic logical representation

First, each client will correspond to a socketchannel channel channel (generally, the channel reads and writes data through buffer), and then these socketchannels will be registered into the selector. selector is equivalent to a manager. It will poll all socketchannels, query all available socketchannels, and then handle these socketchannels

Server side

public class NIOServerSocket {

    //Queue to store SelectionKey
    private static List<SelectionKey> writeQueue = new ArrayList<SelectionKey>();
    private static Selector selector = null;

    //Add SelectionKey to queue
    public static void addWriteQueue(SelectionKey key){
        synchronized (writeQueue) {
            writeQueue.add(key);
            //Wake up main thread
            selector.wakeup();
        }
    }

    public static void main(String[] args) throws IOException {

        // 1. Create ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2. Bind port
        serverSocketChannel.bind(new InetSocketAddress(60000));
        // 3. Set non blocking nio to use non blocking mode only
        serverSocketChannel.configureBlocking(false);
        // 4. Create channel selector
        selector = Selector.open();
        /*
         * 5.Register event type
         *
         *  sel:Channel selector
         *  ops:Event type = = > selectionkey: a wrapper class that contains the event type and the channel itself. Four constant types represent four event types
         *  SelectionKey.OP_ACCEPT Get message selectionkey.op'connect
         *  SelectionKey.OP_READ Read selectionkey.op'write
         */
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            System.out.println("Server side: listening for port 60000");
            // 6. Obtain available I/O channels and how many channels are available
            int num = selector.select();
            if (num > 0) { // Determine if there are available channels
                // Get all keys
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                // Use iterator to traverse all keys
                Iterator<SelectionKey> iterator = selectedKeys.iterator();
                // Iterate through the current I/O channel
                while (iterator.hasNext()) {
                    // Get the current key
                    SelectionKey key = iterator.next();
                    // Calling the remove() method of iterator does not remove the current I/O channel, indicating that the current I/O channel has been processed.
                    iterator.remove();
                    // Judge the event type and handle it accordingly
                    if (key.isAcceptable()) {
                        ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
                        SocketChannel socketChannel = ssChannel.accept();
 
                        System.out.println("Processing request:"+ socketChannel.getRemoteAddress());
                        // Get client data
                        // Set non blocking status
                        socketChannel.configureBlocking(false);
                        // Register to selector
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        System.out.println("Read events");
                        //Cancel monitoring of read events
                        key.cancel();
                        //Call read operation tool class
                        NIOHandler.read(key);
                    } else if (key.isWritable()) {
                        System.out.println("Writing events");
                        //Cancel monitoring of read events
                        key.cancel();
                        //Call write tool class
                        NIOHandler.write(key);
                    }
                }
            }else{
                synchronized (writeQueue) {
                    while(writeQueue.size() > 0){
                        SelectionKey key = writeQueue.remove(0);
                        //Register write events
                        SocketChannel channel = (SocketChannel) key.channel();
                        Object attachment = key.attachment();
                        channel.register(selector, SelectionKey.OP_WRITE,attachment);
                    }
                }
            }
        }
    }
}

Message processor, where multithreading is used to process messages, the number of threads is generally related to the number of cpu cores of the server, and the main purpose is to play the performance of all cpu cores.

public class NIOHandler {
 
    //Construct thread pool
    private static ExecutorService executorService  = Executors.newFixedThreadPool(10);
 
    public static void read(final SelectionKey key){
        //Get thread and execute
        executorService.submit(new Runnable() {
 
            @Override
            public void run() {
                try {
                    SocketChannel readChannel = (SocketChannel) key.channel();
                    // I/O read data operation
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int len = 0;
                    while (true) {
                        buffer.clear();
                        len = readChannel.read(buffer);
                        if (len == -1) break;
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            baos.write(buffer.get());
                        }
                    }
                    System.out.println("Data received by the server:"+ new String(baos.toByteArray()));
                    //Add data to key
                    key.attach(baos);
                    //Add registered writes to the queue
                    NIOServerSocket.addWriteQueue(key);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
 
    public static void write(final SelectionKey key) {
        //Get thread and execute
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    // Write operation
                    SocketChannel writeChannel = (SocketChannel) key.channel();
                    //Get the data passed by the client
                    ByteArrayOutputStream attachment = (ByteArrayOutputStream)key.attachment();
                    System.out.println("Data sent by the client:"+new String(attachment.toByteArray()));
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    String message = "Hello, I am the server!!";
                    buffer.put(message.getBytes());
                    buffer.flip();
                    writeChannel.write(buffer);
                    writeChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

Client

public class NIOClientSocket {
 
    public static void main(String[] args) throws IOException {
        //Using threads to simulate concurrent access of users
        for (int i = 0; i < 1; i++) {
            new Thread(){
                public void run() {
                    try {
                        //1. Create SocketChannel
                        SocketChannel socketChannel=SocketChannel.open();
                        //2. Connect to the server
                        socketChannel.connect(new InetSocketAddress("localhost",60000));
                        //Writing data
                        String msg="I am the client"+Thread.currentThread().getId();
                        ByteBuffer buffer=ByteBuffer.allocate(1024);
                        buffer.put(msg.getBytes());
                        buffer.flip();
                        socketChannel.write(buffer);
                        socketChannel.shutdownOutput();
                        //Read data
                        ByteArrayOutputStream bos = new ByteArrayOutputStream();
                        int len = 0;
                        while (true) {
                            buffer.clear();
                            len = socketChannel.read(buffer);
                            if (len == -1)
                                break;
                            buffer.flip();
                            while (buffer.hasRemaining()) {
                                bos.write(buffer.get());
                            }
                        }
                        System.out.println("Client received:"+new String(bos.toByteArray()));
                        socketChannel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                };
            }.start();
        }
    }
}

There is a strange code in the above code, that is, writequeue. The main reason for this is

The ready condition for the op'write event does not occur after the channel's write method is called, but when the underlying buffer has free space. Because the write buffer has free space most of the time, if you register the write event, it will make the write event always ready, and the choice of processing site will always occupy CPU resources. So, only when you do have data to write, register the write operation, and cancel the registration immediately after writing. In fact, in most cases, we just call the channel's write method to write the data directly. There is no need to use the op? Write event. So under what circumstances is the op'write event mainly used?
In fact, the op write event is mainly used when the sending buffer space is full. Such as:

while (buffer.hasRemaining()) {
     int len = socketChannel.write(buffer);   
     if (len == 0) {
          selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
          selector.wakeup();
          break;
     }
}

When the buffer still has data, but the buffer is full, socketChannel.write(buffer) will return the number of bytes that have been written out, which is 0. At this time, we need to register the op write event, so that when there is free space in the buffer, the op write event will be triggered, so that we can continue to write the unfinished data.
And after writing, be sure to log off the op ﹣ write event:
selectionKey.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);
Note that the wakeup() is called after modifying the interest here; the method is to wake up the blocked selector method, so that when while determines that selector returns 0, it will call selector.select() again. The selectionKey's interest is registered to the system to listen every time the selector.select() operation is performed, so the modified interest after the selector.select() call needs to take effect in the next selector.select() call.

So for NIO, 100 links will not have 100 threads, but will have cpu cores + 1 thread, or cpu cores x2 +1

Reference resources

http://www.imooc.com/article/265871 https://blog.csdn.net/zxcc1314/article/details/80918665 https://www.jianshu.com/p/1af407c043cb

Tags: Programming socket Netty

Posted on Sun, 19 Jan 2020 08:32:15 -0500 by gabo0303