Recently, I plan to deepen my knowledge of Java network programming (IO, NIO, Socket programming, Netty).
Java I/O streams
Java Network Programming
Java NIO: Buffers
Java NIO: Channels
Java NIO: selector
Java NIO requires an understanding of the three core concepts of buffers, channels, and selectors as a complement to Java I/O to improve the efficiency of mass data transfer.
The traditional Java solution for monitoring multiple sockets is to create a thread for each Socket and block the thread at the read call until the data is readable. This works well on systems with low concurrency, where many threads need to be created (one thread per connection). Too many threads cause frequent context switches, threads are system resources, and the maximum number of threads that can be created is limited and far less than the number of network connections that can be established.
NIO’s selectors are designed to solve this problem by providing the ability to ask multiple channels simultaneously if they are ready to perform I/O, such as if a SocketChannel object has more bytes to read and if ServerSocketChannel has a client connection that has already arrived.
By using selectors, we can listen for the ready state of multiple channels in one thread!
The core concept
Selector: Manages the collection of selectable channels & updates the ready state of the selectable channels
Selectable channels: Sockets are selectable for all channels that inherit from SelectableChannel, but file channels are not. Only selectable channels can be registered with the selector.
Select key: The selectable channel registers with the selector and returns the select key, so the select key is an encapsulation of the channel and selector registration relationship
The relationship between the three: Selectable channels register with the selector and return the select key
Selector use
The usual steps for using a selector are:
-
Construction selector
-
Selectable channels are registered with the selector
-
Selector selection (selects ready channels)
-
Read and write to the ready channel
-
Repeat 2 to 4
Here are some steps to explain
Construction selector
Using static factory method construction (underneath creating Selector instances using SelectorProvider, which supports Java SPI extensions)
Selector selector = Selector.open();
Copy the code
Selectable channels are registered with the selector
Only channels running in non-blocking mode can be registered with the selector
The methods that are registered are defined in the SelectableChannel class, and are registered with optional operations (the four optional operations are defined in the SelectionKey), as well as with attachments
Select key is returned after successful registration. The specific API is as follows:
// Parameter selector + optional operation
public final SelectionKey register(Selector sel, int ops)
// The version with attachments
public abstract SelectionKey register(Selector sel, int ops, Object att)
Copy the code
Selector selection
The select method selects the ready channel and places the SelectionKey associated with that ready channel into the selectedKeys set of the selector.
Read and write to the ready channel
Read and write operations on ready channels are shown in Demo below
Demo
I wrote a demo to integrate the above steps, the code is mainly two parts: optional channel registration & selector selection and ready channel read and write operations
Optional channel registration & selector selection
// Construct selector
Selector selector = Selector.open();
// Initialize the ServerSocketChannel binding local port and set it to non-blocking mode
ServerSocketChannel ch = ServerSocketChannel.open();
ch.bind(new InetSocketAddress("127.0.0.1".7001));
ch.configureBlocking(false);
// The channel registers with the selector and cares about the ACCEPT operation (because it is a Server)
ch.register(selector, SelectionKey.OP_ACCEPT);
// Loop through to select the channel ready state
while (true) {
int n = selector.select();
if (n == 0) {
continue;
}
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
// Walk through all ready channels
while (it.hasNext()) {
SelectionKey key = it.next();
// If the ready action is ACCEPT, ACCEPT the client connection and register with the selector.
if (key.isAcceptable()) {
try {
SocketChannel cch = ch.accept();
cch.configureBlocking(false);
// The client channel registers with the selector selector and cares about the READ operation
cch.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
} catch(IOException e) { e.printStackTrace(); }}else {
// If it is another ready operation, commit it to the thread pool
pool.submit(() -> handle(key));
}
// Remove the select keyit.remove(); }}Copy the code
Read and write to the ready channel
public static void handle(SelectionKey key) {
// If the channel is read-ready
if (key.isReadable()) {
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
SocketChannel ch = (SocketChannel) key.channel();
ByteBuffer bf = (ByteBuffer) key.attachment();
try {
int n = ch.read(bf);
// Read the data (ASCII) and simply output it
if (n > 0) {
bf.flip();
StringBuilder builder = new StringBuilder();
while (bf.hasRemaining()) {
builder.append((char) bf.get());
}
System.out.print(builder.toString());
bf.clear();
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
key.selector().wakeup();
} else if (n < -1) { // Close the connectionch.close(); }}catch (IOException e) {
//}}}Copy the code
Selector depth
Optional operations
There are four optional operations (OP_READ, OP_WRITE, OP_ACCEPT, and OP_ACCEPT). The following shows the Socket channel’s support for these four optional operations
OP_READ | OP_WRITE | OP_ACCEPT | OP_CONNECT | |
---|---|---|---|---|
SocketChannel | support | support | Does not support | support |
SeverScoketChannel | support | support | support | Does not support |
DatagramChannel | support | support | Does not support | Does not support |
In summary: the client Socket channel does not care about ACCEPT, the server Socket channel does not care about CONNECT, and the datagram Socket channel only cares about READ and WRITE
Select key (SelectionKey)
Selectable channels are registered with the selector and a selectable key object is returned that represents an association between the channel and the selector. There are a few main attributes to know about him
-
InterestOps: Collection of selectable operations of interest, initialized when selectable channels are registered with the picker and can be modified
-
ReadyOps: A collection of ready selectable operations that are updated when selectors are selected and cannot be modified by clients.
-
Attachment: Select key can be associated with an object, called an attachment (for example, to a buffer object)
Selector selection process
Before we look at the selection process, let’s look at what the three key sets in the selector mean
-
A collection of registered keys, returned by the keys method. The corresponding selection key is added to the collection when the channel is registered
-
The set of selected keys into which the ready channel’s selection key is placed when selected by the selector
-
The collection of cancelled keys to which the selection key is added after the cancel method for the selection key is called
Specific selection process:
- If the set of canceled keys is not empty, each canceled key is removed from the other two sets and the associated channel is unregistered, and finally the set of canceled keys is emptied.
- Queries the ready state of channels in the collection of registered keys (system calls), updates the collection of selected keys and the collection of readyOps for keys. Update the readyOps collection for a key if it was already in the collection of selected keys prior to the selection; Otherwise add the key to the selected collection and reset the readyOps collection for that key.)
Best practices
We typically use a selector to manage all the alternative channels and delegate the ready channel services to other threads, with only one thread monitoring the ready state of the channel and using a coordinated worker thread pool to read and write data
In this way, the operation bit needs to be removed from the interestOps collection before the relevant operation, so as to avoid putting the selection key into the selected collection the next time the selector is selected. After the relevant operation is done, it adds the operation to the interestOps collection and wakes up the selector for the next selection
The following is a simple read pseudocode
/ / read in place
if (key.isReadable()) {
// Remove READ from interestOps
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
/ /... Data read processing
// Add READ to interestOps
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
// Wake up the selector and reselect it (because the interestOps of the key has changed)
key.selector().wakeup();
}
Copy the code
conclusion
- Alternative channelRegistered toThe selectorOn the backSelect key
- The core idea of the selector is to use one (or a few) selectors to manage all the optional channels, with each selector using one thread to monitor the ready state, and a worker thread pool to handle data reads and writes for the ready channels