This is the 9th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

What is the BIO

Network programming is the basic model of Client/Server model, which is between the two processes communicate with each other through the network, the Server to provide location information (IP address and listen on port binding), the Client to the Server through join operation to monitor the address of the connection request, through the three-way handshake to establish a connection, if the connection is established successfully, The two parties can communicate with each other through a network Socket. In the development based on the traditional synchronous blocking model, ServerSocket is responsible for binding IP address and starting the listening port. The Socket initiates the connection. After the connection is successful, the two sides communicate synchronously blocking through input and output streams.

First, to get familiar with the BIO server communication model, a server using the BIO communication model is usually monitored by a separate receiver thread, which creates a new thread for each client to manage the communication after receiving the connection request. This is the typical one – request – one – reply communication model.

But can imagine, when the client and increased traffic, server-side thread number will rise sharply, and the server performance will decline, as the concurrent traffic continued to increase, the system will happen thread stack overflow, failed to create a new thread, and eventually lead to process downtime or zombie, cannot provide external services.

Pseudo asynchronous I/o

In order to improve the BIO model, a model of one or more threads processing N clients through a thread pool or message queue was proposed. Because its underlying communication mechanism still uses synchronous blocking IO, it is called “pseudo-asynchrony”. The server of this model processes multiple client access requests through a thread pool, through which thread resources can be flexibly allocated and the maximum number of threads can be set to prevent thread exhaustion due to massive concurrent access.

Specifically, when a new client is connected, the Socket of the client is encapsulated into a task and sent to the thread pool of the server backend for processing. The thread pool maintains a message queue and N active threads to process the tasks in the message queue. Because the thread pool can set the size of the message queue and the maximum number of threads, its resource usage is manageable, and no matter how many clients access it concurrently, it will not cause resource exhaustion or downtime. For details on thread pools, see the Java ThreadPool analogy.

The following example is server-side code for a pseudo-asynchronous IO model that creates a thread pool to receive connection requests from users.

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolServer {
    static int port = 5555;
    ServerSocket serverSocket = null;
    
    class ThreadServer implements Runnable {
        Socket socket;

        public PrintWriter getWriter(Socket socket) throws IOException {
            OutputStream socketoutput = socket.getOutputStream();
            PrintWriter printWriter = new PrintWriter(socketoutput, true);
            return printWriter;
        }

        public BufferedReader getReader(Socket socket) throws IOException {
            InputStream socketinput = socket.getInputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socketinput));

            return bufferedReader;
        }

        public ThreadServer(Socket s) {
            this.socket = s;
        }

        @Override
        public void run(a) {
            System.out.println("Client connected, address:" + socket.getInetAddress() + "Port number:" + socket.getPort());

            try {
                PrintWriter writer = this.getWriter(socket);
                BufferedReader reader = this.getReader(socket);
                String msg = null;
                msg = reader.readLine();
                while((msg) ! =null) {
                    System.out.println(socket.getInetAddress() + "" + socket.getPort() + "Message sent:" + msg);
                    writer.println("Server received:"+ msg); msg = reader.readLine(); }}catch (IOException e) {

            } finally {
                try {
                    if(socket ! =null)
                        socket.close();
                } catch(IOException e) { e.printStackTrace(); }}}}public ThreadPoolServer(a)
    {
        this.serverSocket = null;
    }

    public void server(a) throws IOException {

        try {
            serverSocket = new ServerSocket(port);
            Socket socket = null;
            ExecutorService executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),50.120, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(10000));
            while(true)
            {
                System.out.println("-- -- -- -- -- -- -- -- --");
                socket = serverSocket.accept();
                System.out.println("Received client request"+socket.getInetAddress());
                executorService.execute(newThreadServer(socket)); }}catch (IOException e) {

        }finally {
            if(serverSocket! =null) serverSocket.close(); }}public static void main(String[] args) throws IOException {
        ThreadPoolServer s = newThreadPoolServer(); s.server(); }}Copy the code

When the write method of OutputStream is called to write the OutputStream, it will block until all bytes to be sent have been written, or an exception occurs. As can be known from TCP knowledge, when the message receiver is slow in processing, it cannot read data from the TCP buffer in time, which will cause the TCPwindow size of the sender to decrease continuously until it is 0 and both parties are in keep-alive state. The message sender can no longer write messages to the TCP buffer. If synchronous blocking IO is used,write operations will be blocked indefinitely until TCPwindow size is greater than 0 or an I/O exception occurs.

Moreover, both read and write operations block synchronously, depending on the processing speed of the other IO thread and network I/O transmission speed. Essentially, there is no guarantee that the network condition of the production environment and the application on the other side will be fast enough, and if our application depends on the processing speed of the other side, it will be very unreliable.

Pseudo asynchronous IO is actually just a simple optimization of the previous IO thread model, which cannot fundamentally solve the communication thread blocking problem caused by synchronous I/O.

NIO

The BIO and pseudo asynchronous IO models have been introduced, and their shortcomings and problems have been analyzed. Therefore, NIO is needed to solve their blocking problems.

NIO stands for no-blocking IO. The server program receives a client connection, the client establishes a connection to the server, and the server program and client send and receive data can all operate in a non-blocking manner. The server program only needs to create a single thread to complete the task of communicating with multiple customers simultaneously.

Congestion on the network

When communicating remotely, a thread in a client program may be blocked in the following cases:

  • When a request is made to connect to a server, that is, when the thread executes the Socket’s parameterized constructor or the Socket’s connect() method, it blocks and does not return from the Socket’s constructor or connect() method until the connection succeeds.
  • If a thread does not have enough data to read from the Socket’s input stream, it blocks and does not return or abort from the input stream’s read) method until it reads enough data, or reaches the end of the input stream, or an exception occurs. The following three read functions can be used to determine how much data is in the input stream:

Int read(): One byte in the input stream is sufficient. Int read(byte[] buff): Sufficient as long as the bytes in the input stream match the length of the parameter buff array for several days. String readLine(): As long as there is one line of String in the input stream, this is sufficient. It is worth noting that the InputStream class does not have a readLine(method, which is only available in the BufferedReader class.

  • A thread writing a batch of data to the Socket’s output stream may block until all data is output or an exception occurs before returning or aborting from the write() method of the output stream.

In a server program, a thread may be blocked in the following cases:

  • The thread executes the Accept () method of the ServerSocket, waits for a client connection, and does not return from the Accept () method until it receives a client connection.
  • When a thread reads data from the Socket’s input stream, it blocks if the input stream does not have enough data.
  • A thread writing a batch of data to the Socket’s output stream may block until all data is output or an exception occurs before returning or aborting from the write() method of the output stream.

Thus, whether in the server program or client program, when reading or writing data through the Socket’s input stream or output stream, it may enter a blocking state. Such potentially blocking input and output operations are called blocking IO. In contrast, input and output operations that do not block are called non-blocking IO.

The three major components

The three main components of NIO are buffers, channels, and selectors.

Buffer

A Buffer is an object that contains some data to be written or read. The addition of Buffer objects to the NIO class library represents an important difference between the new library and the original I/O. In stream-oriented IO, data can be written or read directly into a Stream object. In the NIO library, all data is processed with buffers. When reading data, it reads directly into the buffer; When writing data, it is written to a buffer. Any time you access data in NIO, you are operating through the buffer.

There are several types of Buffer, of which ByteBuffer is the most commonly used

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

Channel is the Channel

A Channel is a Channel through which network data is read and written, just like a water pipe. A channel differs from a stream in that it is bidirectional, with a stream moving only in one direction, whereas a channel can be used for reading, writing, or both.

The following four channels are common: FileChannel is used for file transfer, and the other three channels are used for network communication

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

A multiplexer Selector

The multiplexer provides the ability to select tasks that are already in place. Basically, a Selector constantly polls a Channel registered on it, and if a read or write event happens on a Channel, that Channel is in a ready state and polled by the Selector, The SelectionKey is then used to retrieve a collection of ready channels for subsequent I/O operations. This means that thousands of clients can be accessed with only one thread polling the Selector, which is a huge improvement.

Before using selector, there are two ways to handle socket connections:

  1. Using multi-threading technology, open up a thread for each connection, respectively to handle the corresponding socket connection (BIO)
  2. Using thread pool technology, let threads in the thread pool handle connections (pseudo asynchronous IO)

The drawbacks of both approaches were described in the previous section, but the idea of using a selector is to manage multiple channels with a single thread (fileChannel is blocking, so you can’t use a selector), get events that happen on those channels, These channels work in non-blocking mode (they don’t let a thread hang on a channel) and can execute tasks in other channels when there are no tasks in one channel. Suitable for scenarios with a large number of connections but low traffic. If the event is not ready, the selector blocks the thread until a ready event occurs on the channel. With these events in place, the SELECT method returns them to Thread for processing