From BIO to NIO, multiplexers, from theory to practice, compare their respective efficiencies and characteristics with actual cases (Part 2)

Article directory

    • Introduction to multiplexers
    • Two stages of multiplexer
    • Multiplexer encapsulation in Java
    • test code
    • Pressure test results
    • Summarize

This article is the next article from BIO to NIO and multiplexers, from theory to practice, and comparing their respective efficiencies and characteristics with actual cases (Part 1). If you haven’t read it, you can read it first, otherwise you may not Incoherent.

Introduction to multiplexers

The multiplexer is an optimization of traditional NIO, which solves the problem that traditional NIO cannot directly obtain the status of all connections, and needs to traverse all connections one by one to see if they are ready. This method will involve many system calls, user mode Switching with kernel state is not efficient.

How is the multiplexer optimized?
First of all, you need to understand who the multi-channel path is ——–>In fact, it is each IO connection

Who knows whether there is data in each path—->The kernel knows. Since the kernel itself knows which connections are connected at a certain time, can we just call the corresponding function method directly, so here There is a multiplexer. When you call this multiplexer, it will return the IO status of all channels to you.

This can obtain the result set of the IO status of all connections through one system call.
Then the program reads and writes the stateful (prepared) connections by itself, which is high performance.

Note here, one more thing, as long as the program reads and writes data by itself, your IO model is synchronous.

Two stages of multiplexer

The multiplexer has two stages, or two types of implementations of the kernel. The ultimate goal of these two types of implementations is the same, which is to help you return the IO status of all IO connections (whether it is readable or not), but the implementation details There are slight differences. It can be understood that epoll is an upgraded version of select poll.

Here again, I would like to remind you that the following two implementations talk about implementations in the operating system, not methods in Java.

  • select poll
    All IO connections need to be stored in a collection, and a copy of this collection is passed to the kernel, that is, calling select or poll. The kernel will give a special identifier to the ready connections in the collection and then return.
    In this way, the program can directly know which connections are stateful and read the data directly.
    Disadvantages:
    If there are 10,000 connections, these 10,000 connections need to be copied to the kernel every time. This copy is the point of loss, and the data needs to be copied to the kernel repeatedly every time.

  • epoll
    It is precisely because select and poll have their own shortcomings that epoll was born.
    Optimization
    By exchanging space for time, the kernel space is opened up and the connection information of the application is cached. In this way, there is no need to copy data repeatedly. No loss is high performance.

    Implementation steps
    1. On a Linux machine, there are many applications, so if an application wants to use epoll, it first needs to open up space in the kernel——corresponding to the epoll_create system call
    2. Then when the connection is created, add the connection to the space——corresponding to the epoll_ctl(add) system call
    3. Then ask to see which IO connections are ready——corresponding to the epoll_wait system call

Multiplexer encapsulation in Java

Under the java.nio package, the practice and use of multiplexing is encapsulated, that is, the Selector class

Which implementation is used at the bottom of Seletor in Java? Select poll or epoll?
Java will actually dynamically decide which implementation to use when running, because it will call a fixed method to start the multiplexer, namely Selector.open. Your program may run on different kernels, and jdk will Give priority to good epoll, but if there is no multiplexer like epoll, only select or poll can run normally.

Introduction to main usage methods:
There are three main methods here. No matter which implementation is used at the bottom, these three methods will be called. However, according to different implementations, the specific things done are different. The differences are as follows:

  1. Selector.open
    Start the multiplexer, preferably epoll, if not, select select or poll.
    If it is epoll, you need to open up space in the kernel, that is, call epoll_create.
  2. register
    select, poll: will create an array in the jvm and put the file descriptor (fd4) corresponding to each connection into it.
    epoll: is equivalent to calling the kernel method epoll_ctl(add), which adds the connection to the kernel space and is directly managed by the kernel.
  3. select
    select, poll: will pass the array in the jvm to the kernel, that is, call select(fd4) or poll(fd4)
    epoll: equivalent to directly calling the kernel method epol_wait, directly asking the kernel

Test code

Server

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @ClassName:
 * @Description: (Describe the role of this class)
 * @author:
 * @date:
 *
 */
public class SelectorTest {<!-- -->

    private static ServerSocketChannel server=null;
    private static Selector selector;
    static int port=9090;
    static int count=5000;
    static long startTime;

    public static void initServer(){<!-- -->
        try {<!-- -->
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));

            //The implementation of the multiplexer will be automatically selected during compilation
            //It may be select poll or epoll
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {<!-- -->
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {<!-- -->
        initServer();
        System.out.println("The server started...");
        startTime = System.currentTimeMillis();
        try {<!-- -->
            flag:
            while (true){<!-- -->
                //select is equivalent to asking the kernel whether there is data to read or whether there is a connection to establish.
                //The parameter passed in is the timeout time. Passing in 0 means blocking, waiting for someone to establish a connection or send data.
                //If the incoming value is >0, such as 200, it will wait for up to 200 milliseconds, and a result will be returned whether there is any.
                while(selector.select(0)>0){<!-- -->
                    //Retrieve all valid keys from the multiplexer
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while(iterator.hasNext()){<!-- -->
                        SelectionKey key = iterator.next();
                        //Remove after acquisition, otherwise it will be acquired repeatedly
                        iterator.remove();
                        //There is a new connection to establish
                        if(key.isAcceptable()){<!-- -->
                            acceptHander(key);
                        //can be read
                        }else if(key.isReadable()){<!-- -->
                            readHander(key);
                        }
                    }
                    if(count <= 0){<!-- -->
                        System.out.println("Time taken to process 5000 connections:" + (System.currentTimeMillis()-startTime)/1000 + "s");
                        server.close();
                        selector.close();
                        break flag;
                    }
                }


            }
        }catch (Exception e){<!-- -->
            e.printStackTrace();
        }

    }

    private static void readHander(SelectionKey key) {<!-- -->
        //Get the client associated with the current key
        SocketChannel client = (SocketChannel) key.channel();
        //Get the buffer corresponding to the client
        //This buffer is passed in when we establish the connection and is bound one-to-one to the channel.
        ByteBuffer buffer = (ByteBuffer) key.attachment();

        buffer.clear();
        int read=0;
        try {<!-- -->
            for(;;){<!-- -->
                //Read data from the channel and write it into the buffer
                read = client.read(buffer);
                if(read==0){<!-- -->
                    break;
                //There may be a bug here. The client may be shut down and the close_wait state is processed. This event will always be monitored.
                // Simply and violently turn it off here.
                }else if(read<0){<!-- -->
                    client.close();
                    break;
                }else{<!-- -->
                    //For the buffer, we just wrote, now we are reading and calling flip.
                    buffer.flip();

                    byte[] bytes = new byte[buffer.limit()];
                    buffer.get(bytes);

                    String str = new String(bytes);
                    System.out.println(client.socket().getRemoteSocketAddress() + " -->" + str);
                }

            }

        }catch (Exception e){<!-- -->
            e.printStackTrace();

        }


    }

    private static void acceptHander(SelectionKey key) {<!-- -->
        try {<!-- -->
            ServerSocketChannel channel = (ServerSocketChannel) key.channel();
            SocketChannel client = channel.accept();
            client.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            //Give this new connection to the multiplexer for management. This connection can be monitored later in the multiplexer. When we get it, it will return a stateful connection to us.
            //At the same time, channel and buffer are bound one-to-one here, which can be easily written into or read out.
            client.register(selector, SelectionKey.OP_READ,buffer);
            System.out.println("add client port:" + client.socket().getPort());

            count--;


        } catch (IOException e) {<!-- -->
            e.printStackTrace();
        }

    }

}

The client code used in the test remains the same as in the previous article and will not be included here.

Pressure test results

All the above are theories, and theories must be verified by actual results. Here we will still process 5000 connections and receive the same messages to see the actual effect of the multiplexer.

It can be seen that the effect is very obvious. It is much faster than BIO and NIO. Moreover, the code is still a single-threaded model. If it is expanded to multi-threading, the efficiency will be higher.

Summary

From BIO -> NIO -> Multiplexer, we analyzed their respective shortcomings and evolution processes, and compared their respective efficiencies with actual results. I believe you will be more impressed.

The test results for this article are summarized as follows:

That’s it for today’s sharing. If you have any questions, you can leave them in the comment area and we will respond promptly.
I’m bling, and the future won’t be too bad, as long as we don’t be too lazy. See you next time.