“This is the 19th day of my participation in the First Challenge 2022, for more details: First Challenge 2022”.

Introduction to the

Why is Netty fast? This is because the underlying layer of Netty uses JAVA NIO technology and has been optimized based on it. Although Netty is not pure JAVA NIO, the underlying layer of Netty is based on NIO technology.

Nio was introduced in JDK1.4 to differentiate it from traditional IO, so nio can also be called new IO.

Nio’s three core components are Selector,channel, and Buffer, and in this article we’ll delve into the relationship between NIO and Netty.

NIO common usage

Before we get to the NIO implementation in Netty, let’s review how NIO’s selector channel works in the JDK. For NIO, selector is used primarily to accept connections from clients, so it’s usually used on the server side. Let’s use a NIO server and client chat room as an example to illustrate how NIO is used in the JDK.

Since it is a simple chat room, we choose the serverketChannel based on the Socket protocol. First, we open the Server channel:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
serverSocketChannel.configureBlocking(false);
Copy the code

Then register the selector with the server channel:

Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
Copy the code

It is NIO, but for a Selector, its select method is a blocking method that will not return until a matching channel is found. To perform multiple select operations, we need to perform the Selector operation in a while loop:

while (true) {
            selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();
            while (iter.hasNext()) {
                SelectionKey selectionKey = iter.next();
                if (selectionKey.isAcceptable()) {
                    register(selector, serverSocketChannel);
                }
                if (selectionKey.isReadable()) {
                    serverResponse(byteBuffer, selectionKey);
                }
                iter.remove();
            }
            Thread.sleep(1000);
        }
Copy the code

Selector that there will be some SelectionKey, SelectionKey said some of the operating state of the OP Status, according to the OP the different Status, SelectionKey could have four state, IsReadable, isWritable isConnectable and isAcceptable.

When SelectionKey is in the isAcceptable state, the ServerSocketChannel accepts connections, We need to call the Register method to register the socketChannel generated by serverSocketChannel Accept with the selector to listen for its OP READ state and then READ data from it:

    private static void register(Selector selector, ServerSocketChannel serverSocketChannel)
            throws IOException {
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }
Copy the code

When the selectionKey is in isReadable state, it is read from socketChannel and processed:

private static void serverResponse(ByteBuffer byteBuffer, SelectionKey selectionKey) throws IOException { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); log.info(new String(bytes).trim()); If (new String(bytes).trim().equals(BYE_BYE)){log.info(" Goodbye is better than goodbye!" ); SocketChannel. Write (ByteBuffer. Wrap (" goodbye ". GetBytes ())); socketChannel.close(); }else {socketChannel.write(bytebuffer.wrap (" You are a good person ".getBytes()))); } byteBuffer.clear(); }Copy the code

In the serverResponse method above, get the corresponding SocketChannel from the selectionKey, and then call the Read method of the SocketChannel to read the data from the channel into the byteBuffer. To write back a message to a channel, use the same socketChannel, and then call the write method to write back the message to the client. Here a simple write back to the client is done on the server.

NIO client: SocketChannel: SocketChannel: SocketChannel: SocketChannel: SocketChannel

socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527));
Copy the code

We can then use this channel to send and receive messages:

    public String sendMessage(String msg) throws IOException {
        byteBuffer = ByteBuffer.wrap(msg.getBytes());
        String response = null;
        socketChannel.write(byteBuffer);
        byteBuffer.clear();
        socketChannel.read(byteBuffer);
        byteBuffer.flip();
        byte[] bytes= new byte[byteBuffer.limit()];
        byteBuffer.get(bytes);
        response =new String(bytes).trim();
        byteBuffer.clear();
        return response;
    }
Copy the code

A message can be written to a channel using the write method, and a message can be read from a channel using the read method.

This completes a client for NIO.

Although the above is a basic use of NIO’s server and client, it basically covers all the main points of NIO. Let’s take a closer look at how NIO is used in Netty.

NIO and EventLoopGroup

Take ServerBootstrap as an example. The ServerBootstrap group method needs to be specified when the netty server is started.

public ServerBootstrap group(EventLoopGroup group) {
        return group(group, group);
    }

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    ...
}
Copy the code

ServerBootstrap can accept either one EventLoopGroup or two EventLoopGroups. The EventLoopGroup is used to process all events and IO. For ServerBootstrap, There can be two EventLoopGroups, but for Bootstrap there is only one EventLoopGroup. Two EventLoopGroups represent acceptor groups and worker groups.

An EventLoopGroup is simply an interface. A common implementation is NioEventLoopGroup, which is a common netty server-side code:

EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new FirstServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) .childOption(ChannelOption.SO_KEEPALIVE, true); ChannelFuture f = b.bind(port).sync(); // Bind the port and start receiving connections. F.channel ().closeFuture().sync();Copy the code

There are two classes associated with NIO, NioEventLoopGroup and NioServerSocketChannel, and there are actually two similar classes underneath them called NioEventLoop and NioSocketChannel. Let’s take a look at some of their underlying implementations and logical relationships.

NioEventLoopGroup

NioEventLoopGroup like DefaultEventLoopGroup are inherited from MultithreadEventLoopGroup:

public class NioEventLoopGroup extends MultithreadEventLoopGroup 
Copy the code

The difference between them is the newChild method, which builds the actual object in the Group, and NioEventLoopGroup, which returns a NioEventLoop object, Take a look at the newChild method of NioEventLoopGroup:

    protected EventLoop newChild(Executor executor, Object... args) throws Exception {
        SelectorProvider selectorProvider = (SelectorProvider) args[0];
        SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
        RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
        EventLoopTaskQueueFactory taskQueueFactory = null;
        EventLoopTaskQueueFactory tailTaskQueueFactory = null;

        int argsLength = args.length;
        if (argsLength > 3) {
            taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
        }
        if (argsLength > 4) {
            tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
        }
        return new NioEventLoop(this, executor, selectorProvider,
                selectStrategyFactory.newSelectStrategy(),
                rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
    }
Copy the code

In addition to the fixed executor arguments, the newChild method can perform additional functions based on the arguments passed in by the NioEventLoopGroup constructor.

SelectorProvider, SelectStrategyFactory, RejectedExecutionHandler, taskQueueFactory, and tailTaskQueueFactory are passed in as arguments. Including two EventLoopTaskQueueFactory behind is not a must.

Finally, all parameters are passed to the NioEventLoop constructor to construct a new NioEventLoop.

Before going into more detail about NioEventLoop, let’s look at what these parameter types passed in are actually doing.

SelectorProvider

SelectorProvider is a class in the JDK that provides a static provider() method that loads and instantiates the corresponding SelectorProvider class from a Property or ServiceLoader.

In addition, it also provides openDatagramChannel, openPipe, openSelector, openServerSocketChannel, openSocketChannel and other practical NIO operation methods.

SelectStrategyFactory

SelectStrategyFactory is an interface that defines only one method that returns a SelectStrategy:

public interface SelectStrategyFactory {

    SelectStrategy newSelectStrategy();
}
Copy the code

What is a SelectStrategy?

Let’s look at what strategies are defined in the SelectStrategy:

    int SELECT = -1;

    int CONTINUE = -2;

    int BUSY_WAIT = -3;
Copy the code

There are three strategies defined in SelectStrategy, SELECT, CONTINUE, and BUSY_WAIT.

We know that in general, in NIO, the SELECT operation itself is a blocking operation, that is, a block operation, and the strategy of this operation is SELECT, that is, the select block state.

If we want to skip this block and re-enter the next event loop, the corresponding strategy will be CONTINUE.

BUSY_WAIT is a special strategy that polls IO cycles for new events without blocking. This strategy is supported only in epoll mode, not in NIO and Kqueue mode.

RejectedExecutionHandler

RejectedExecutionHandler netty own class, and Java. Util. Concurrent. RejectedExecutionHandler similar, But it’s especially for SingleThreadEventExecutor. This interface defines a rejected method, used to represent because SingleThreadEventExecutor capacity limits that add the task failed and rejected situation:

void rejected(Runnable task, SingleThreadEventExecutor executor);
Copy the code

EventLoopTaskQueueFactory

EventLoopTaskQueueFactory is an interface that is used to create storage is submitted to the EventLoop taskQueue:

Queue<Runnable> newTaskQueue(int maxCapacity);
Copy the code

The Queue must be thread-safe, and to inherit from Java. The util. Concurrent. BlockingQueue.

With these parameters explained, we can now take a closer look at the specific NIO implementation of NioEventLoop.

NioEventLoop

NioEventLoop, like DefaultEventLoop, is derived from SingleThreadEventLoop:

public final class NioEventLoop extends SingleThreadEventLoop
Copy the code

Represents an EventLoop that uses a single thread to execute a task.

First, as an implementation of NIO, we must have a selector. In NioEventLoop, we define two selectors: selector and unwrappedSelector:

    private Selector selector;
    private Selector unwrappedSelector;
Copy the code

In the constructor of NioEventLoop, they are defined like this:

        final SelectorTuple selectorTuple = openSelector();
        this.selector = selectorTuple.selector;
        this.unwrappedSelector = selectorTuple.unwrappedSelector;
Copy the code

We call the openSelector method, and then we get the corresponding selector and unwrappedSelector from the returned SelectorTuple.

What’s the difference between these two selectors?

In the openSelector method, we first return a Selector by calling the provider’s openSelector method, which is called unwrappedSelector:

final Selector unwrappedSelector;
unwrappedSelector = provider.openSelector();
Copy the code

Then check whether DISABLE_KEY_SET_OPTIMIZATION is set. If not, then unwrappedSelector and selector are actually the same selector:

DISABLE_KEY_SET_OPTIMIZATION specifies whether to optimize a select key set:

if (DISABLE_KEY_SET_OPTIMIZATION) {
      return new SelectorTuple(unwrappedSelector);
   }

        SelectorTuple(Selector unwrappedSelector) {
            this.unwrappedSelector = unwrappedSelector;
            this.selector = unwrappedSelector;
        }
Copy the code

If DISABLE_KEY_SET_OPTIMIZATION is set to false, this means that we need to optimize the select key set. How is this optimized?

Let’s take a look at the final return:

return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
Copy the code

Finally return SelectorTuple the second parameter is the selector, the selector is a SelectedSelectionKeySetSelector object.

SelectedSelectionKeySetSelector inherited from the selector, the first parameter to the constructor to is a delegate, all of the methods defined in the selector is done by invoking the delegate, The reset method of selectedKeySet is called first. The following is an example of the isOpen and SELECT methods to observe the code implementation:

    public boolean isOpen() {
        return delegate.isOpen();
    }

    public int select(long timeout) throws IOException {
        selectionKeys.reset();
        return delegate.select(timeout);
    }
Copy the code

SelectedKeySet is a SelectedSelectionKeySet object, is a collection of the set, used to store SelectionKey, in openSelector () method, using new to instantiate the object:

final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
Copy the code

Netty actually wanted to use this SelectedSelectionKeySet class to manage selectedKeys in selectedKeys, so netty then used a tricky object substitution operation.

SelectorImpl (sun.nio.ch.selectorImpl)

Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { try { return Class.forName( "sun.nio.ch.SelectorImpl", false, PlatformDependent.getSystemClassLoader()); } catch (Throwable cause) { return cause; }}});Copy the code

SelectorImpl has two Set fields:

    private Set<SelectionKey> publicKeys;
    private Set<SelectionKey> publicSelectedKeys;
Copy the code

Those are the two fields we need to replace. If you have SelectorImpl, first use the Unsafe class and call PlatformDependent’s objectFieldOffset method to get the offset of the two fields relative to the object example. Then call putObject to replace the two fields with the previously initialized selectedKeySet object:

Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys"); Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys"); if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) { // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet. // This allows us to also do this in Java9+ without any extra flags. long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField); long publicSelectedKeysFieldOffset = PlatformDependent.objectFieldOffset(publicSelectedKeysField); if (selectedKeysFieldOffset ! = -1 && publicSelectedKeysFieldOffset ! = -1) { PlatformDependent.putObject( unwrappedSelector, selectedKeysFieldOffset, selectedKeySet); PlatformDependent.putObject( unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet); return null; }Copy the code

If Unsafe is not supported in the system setting, do it again with reflection:

Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true); if (cause ! = null) { return cause; } cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true); if (cause ! = null) { return cause; } selectedKeysField.set(unwrappedSelector, selectedKeySet); publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);Copy the code

One of the most important rewriting methods to focus on in NioEventLoop is the run method, which implements the logic of how to execute a task.

Remember the selectStrategy we mentioned earlier? Run method by calling selectStrategy. CalculateStrategy returned to select the strategy, and then by judging the value of the strategy for the corresponding processing.

If strategy is CONTINUE, this skips this loop and goes to the next loop.

BUSY_WAIT is not supported in NIO. If it is in SELECT state, the SELECT operation will be performed again after curDeadlineNanos:

strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); switch (strategy) { case SelectStrategy.CONTINUE: continue; case SelectStrategy.BUSY_WAIT: // fall-through to SELECT since the busy-wait is not supported with NIO case SelectStrategy.SELECT: long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); if (curDeadlineNanos == -1L) { curDeadlineNanos = NONE; // nothing on the calendar } nextWakeupNanos.set(curDeadlineNanos); try { if (! hasTasks()) { strategy = select(curDeadlineNanos); } } finally { // This update is just to help block unnecessary selector wakeups // so use of lazySet is ok (no race condition) nextWakeupNanos.lazySet(AWAKE); } // fall through default:Copy the code

If strategy > 0, the SelectedKeys have been obtained, then call processSelectedKeys to process the SelectedKeys:

    private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized();
        } else {
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }
Copy the code

As mentioned above, NioEventLoop has two selectedKeys and a selectedKeys property, which is called Optimized selectedKeys, and if it’s not null, Call processSelectedKeysOptimized method, otherwise, they call processSelectedKeysPlain method.

ProcessSelectedKeysOptimized processSelectedKeysPlain and the differences between two methods is not big, just selectedKeys different incoming to deal with.

The logic is to get the selectedKeys key and then call its Attachment method to get the attach object:

final SelectionKey k = selectedKeys.keys[i];
            selectedKeys.keys[i] = null;

            final Object a = k.attachment();

            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                processSelectedKey(k, task);
            }
Copy the code

If a channel has not already established a connection, this object may be a NioTask that handles channelReady and channelUnregistered events.

If a channel is already connected, the object might be an AbstractNioChannel.

The processSelectedKey method is called separately for two different objects.

In the first case, task’s channelReady method is called:

task.channelReady(k.channel(), k);
Copy the code

In the second case, methods in ch.unsafe() are called to read or close, depending on the state of readyOps() for SelectionKey.

conclusion

Although NioEventLoop is also a SingleThreadEventLoop, it can make better use of existing resources to achieve better efficiency by using NIO technology. This is why we used NioEventLoopGroup instead of DefaultEventLoopGroup in our project.

This article is available at www.flydean.com/05-2-netty-…

The most popular interpretation, the most profound dry goods, the most concise tutorial, many tips you didn’t know waiting for you to discover!

Welcome to pay attention to my public number: “procedures those things”, understand technology, more understand you!