Java IO learning note 7: multiplexing from single thread to multi thread

Posted by DBHostS on Wed, 10 Nov 2021 08:27:42 +0100

Author: Grey

Original address: Java IO learning note 7: multiplexing from single thread to multi thread

stay Mentioned earlier In the multiplexed server-side code of, we handle both read data and write events:

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

In order to clarify the rights and responsibilities, we have separated two event handling:

                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            // Processing read data
                            readHandler(key);
                        } else if (key.isWritable()) {
                            // Processing write data
                            writeHandler(key);
                        }
                    }

One is responsible for writing and the other for reading

Read event processing, as follows

    public void readHandler(SelectionKey key) {
        System.out.println("read handler.....");
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Where read > 0 means that the data is read from the client before we register a write event:

client.register(key.selector(), SelectionKey.OP_WRITE, buffer);

Other events do not register write events. (PS: write events can be registered as long as the send queue is not full)

The processing logic of write events is as follows:

    private void writeHandler(SelectionKey key) {
        System.out.println("write handler...");
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.flip();
        while (buffer.hasRemaining()) {
            try {
                client.write(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        buffer.clear();
        key.cancel();
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

After writing, call key.cancel() to cancel the registration and close the client.

Test it and run SocketMultiplexingV2.java

And connect through a client:

nc 192.168.205.1 9090

The client sends some content:

nc 192.168.205.1 9090
asdfasdfasf
asdfasdfasf

Data can be received normally.

Consider that an FD takes time to execute, which will block subsequent FD processing in a linear. At the same time, consider resource utilization and make full use of the number of cpu cores.

We implement a multiplexing model based on multithreading.

Group N FDS (FDS here are Socket connections), each group has a selector, and press a selector onto a thread (the best number of threads is: cpu cores or cpu cores * 2)

fd in each selector is executed linearly. Suppose there are 100w connections. If there are four threads, each thread processes 25w connections.

The grouped FDS and the selectors that handle this stack of FDS are encapsulated in a data structure, assuming that they are called SelectorThread, and their member variables are at least as follows:

Selector selector = null;
// Save Selector corresponding to FD queue to be processed
LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();

Because its processing is linear and we need to open many threads to process, SelectorThread itself is a thread class (implementing the Runnable interface)

public class SelectorThread implements Runnable {
...

}

In the run method, we can transplant the conventional operation code of single thread processing Selector:

....
while (true) {
....
 if (selector.select() > 0) {
       Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
       while (iter.hasNext()) {
            SelectionKey key = iter.next();
            iter.remove();
            if (key.isAcceptable()) {
                acceptHandler(key);
            } else if (key.isReadable()) {
                 readHandler(key);
            } else if (key.isWritable()) {
            }
     }
  }
....
}
....

After the SelectorThread is designed, we need a class that can organize selectorthreads. Suppose it is called SelectorThreadGroup. The main responsibility of this class is to arrange which FD S are taken over by which selectors. This class holds two SelectorThread arrays, one for allocating the server and the other for allocating each Socket request of the client.

// Server, you can start multiple servers
SelectorThread[] bosses;
// Socket request from client
SelectorThread[] workers;

Both arrays are initialized in the SelectorThreadGroup constructor

    SelectorThreadGroup(int bossNum, int workerNum) {
        bosses = new SelectorThread[bossNum];
        workers = new SelectorThread[workerNum];
        for (int i = 0; i < bossNum; i++) {
            bosses[i] = new SelectorThread(this);
            new Thread(bosses[i]).start();
        }
        for (int i = 0; i < workerNum; i++) {
            workers[i] = new SelectorThread(this);
            new Thread(workers[i]).start();
        }
    }

The following code shows how to allocate the Selector for each request:

...
    public void nextSelector(Channel c) {
        try {
            SelectorThread st;
            if (c instanceof ServerSocketChannel) {
                st = nextBoss();
                st.lbq.put(c);
                st.setWorker(workerGroup);
            } else {
                st = nextWork();
                st.lbq.add(c);
            }
            st.selector.wakeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private SelectorThread nextBoss() {
        int index = xid.incrementAndGet() % bosses.length;
        return bosses[index];
    }

    private SelectorThread nextWork() {
        int index = xid.incrementAndGet() % workers.length;  //Thread allocation using worker
        return workers[index];
    }

...

Here, we need to distinguish between two types of channels. One is ServerSocketChannel, that is, the server we start each time, and the other is the Socket request connecting the server. These two types are best divided into queues in different selectorthreads. The allocation algorithm is a simple polling algorithm (modulus divided by the length of the array)

In this way, our main function only needs to interact with SelectorThreadGroup:

public class Startup {

    public static void main(String[] args) {
  // Three selectorthreads are opened for the server and three selectorthreads are opened for the client to receive sockets
        SelectorThreadGroup group = new SelectorThreadGroup(3,3);
        group.bind(9999);
        group.bind(8888);
        group.bind(6666);
        group.bind(7777);
    }
}

Start Startup,
Open a client, request the server, and test:

[root@io io]# nc 192.168.205.1 7777
sdfasdfs
sdfasdfs

The data requested by the client can be returned, and the server can listen to the client's request:

Thread-1 register listen
Thread-0 register listen
Thread-2 register listen
Thread-1 register listen
Thread-1   acceptHandler......
Thread-5 register client: /192.168.205.138:44152

Because we have four ports for listening, but we only set three server selectorthreads, we can see that Thread-1 listens to two server terminals.

The newly accessed client connection starts from Thread-5 and will not conflict with the previous Thread-0, Thread-1 and Thread-2.

A new client connection again

[root@io io]# nc 192.168.205.1 8888
sdfasdfas
sdfasdfas

If you enter some content, you can still get the response from the server

The server side log displays:

Thread-3 register client: /192.168.205.138:33262
Thread-3 read......

It shows that Thread-3 captures a new connection and will not conflict with the previous Thread-0, Thread-1 and Thread-2.

Source code: Github

Topics: Java Back-end