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