Netty series: detailed explanation of NIO and netty

Posted by onthespot on Wed, 09 Mar 2022 12:05:19 +0100

brief introduction

Why is netty fast? This is because the bottom layer of netty uses JAVA nio technology and optimizes the performance on its basis. Although netty is not a pure JAVA nio, the bottom layer of netty is still based on NiO technology.

nio is jdk1 4, which is different from traditional IO, so nio can also be called new io.

The three cores of NiO are Selector,channel and Buffer. In this article, we will explore the relationship between NiO and netty.

NIO common usage

Before explaining the NIO implementation in netty, let's review how NIO selector and channel work in JDK. For NIO, the selector is mainly used to accept the connection of the client, so it is generally used on the server side. We take a NIO server and client chat room as an example to explain how NIO is used in JDK.

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

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

Then register the selector in the server channel:

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

Although it is NIO, for the selector, its select method is a blocking method. It will not return until a matching channel is found. In order to select multiple times, we need to select the selector 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);
        }

There will be some selectionkeys in the selector, and there are some OP statuses representing the operation status in the selectionKey. According to the different OP statuses, the selectionKey can have four statuses: isReadable,isWritable,isConnectable and isAcceptable.

When the SelectionKey is in isAcceptable state, it means that the ServerSocketChannel can accept the connection. We need to call the register method to register the socketChannel generated by serverSocketChannel accept into the selector to listen to its OP READ state. Later, we can 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);
    }

When the selectionKey is in isReadable state, it means that the data can be read from the socketChannel and then 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("It's better not to see than to say goodbye!");
            socketChannel.write(ByteBuffer.wrap("bye".getBytes()));
            socketChannel.close();
        }else {
            socketChannel.write(ByteBuffer.wrap("You're a good person".getBytes()));
        }
        byteBuffer.clear();
    }

In the serverResponse method above, get the corresponding SocketChannel from selectionKey, then invoke the read method of SocketChannel, read the data in channel to byteBuffer, return the message to channel, or use the same socketChannel, then call write method to write back the message to the client end. Here, a simple write back to the server side of the client message is completed.

The next step is the corresponding NIO client. The NIO client needs to use SocketChannel. First establish a connection with the server:

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

Then you can 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;
    }

The write method can be used to write messages to the channel, and the read method can be used to read messages from the channel.

Such a NIO client is completed.

Although the above points of NIO and client are basically covered. Next, let's take a detailed look at how NIO is used in netty.

NIO and EventLoopGroup

Take nety's ServerBootstrap as an example. When starting, you need to specify its group. First, let's take a look at the group method of ServerBootstrap:

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

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

ServerBootstrap can accept one EventLoopGroup or two eventloopgroups. Eventloopgroups are used to handle all events and IO. For ServerBootstrap, there can be two eventloopgroups and only one EventLoopGroup for Bootstrap. Two eventloopgroups represent acceptor group and worker group.

EventLoopGroup is just an interface. One of our commonly used implementations is NioEventLoopGroup. As shown below, it is a commonly used 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);

            // Bind ports and start receiving connections
            ChannelFuture f = b.bind(port).sync();
            // Wait for the server socket to close
            f.channel().closeFuture().sync();

There are two classes related to NIO, namely NioEventLoopGroup and NioServerSocketChannel. In fact, there are two similar classes at their bottom, called NioEventLoop and NioSocketChannel. Next, we will explain some of their underlying implementations and logical relationships.

NioEventLoopGroup

NioEventLoopGroup, like DefaultEventLoopGroup, inherits from MultithreadEventLoopGroup:

public class NioEventLoopGroup extends MultithreadEventLoopGroup 

The difference between them lies in the newChild method. newChild is used to build the actual object in the Group. For NioEventLoopGroup, newChild returns a NioEventLoop object. First, let's 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);
    }

In addition to the fixed executor parameters, the newChild method can also realize more functions according to the parameters passed in by the constructor of NioEventLoopGroup.

Here, the parameters SelectorProvider, SelectStrategyFactory, RejectedExecutionHandler, taskQueueFactory and tailTaskQueueFactory are passed in. The latter two eventlooptaskqueuefactories are not required.

Finally, all parameters will be passed to the constructor of NioEventLoop to construct a new NioEventLoop.

Before explaining NioEventLoop in detail, let's study the actual role of these parameter types passed in.

SelectorProvider

SelectorProvider is a class in JDK. It provides a static provider() method, which can load and instantiate the corresponding SelectorProvider class from Property or ServiceLoader.

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

SelectStrategyFactory

SelectStrategyFactory is an interface that defines only one method to return SelectStrategy:

public interface SelectStrategyFactory {

    SelectStrategy newSelectStrategy();
}

What is SelectStrategy?

Let's first look at the strategies defined in SelectStrategy:

    int SELECT = -1;

    int CONTINUE = -2;

    int BUSY_WAIT = -3;

Three strategies are defined in select strategy: SELECT, CONTINUE and BUSY_WAIT.

We know that generally, in NIO, the SELECT operation itself is a blocking operation, that is, a block operation. The strategy corresponding to 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 is CONTINUE.

BUSY_WAIT is a special strategy, which means that IO loops poll for new events without blocking. This strategy is supported only in epoll mode, but not in NIO and Kqueue modes.

RejectedExecutionHandler

RejectedExecutionHandler is netty's own class, and Java util. concurrent. RejectedExecutionHandler is similar, but especially for SingleThreadEventExecutor. This interface defines a rejected method, which is used to indicate that the task addition fails due to the capacity limit of SingleThreadEventExecutor and is rejected:

void rejected(Runnable task, SingleThreadEventExecutor executor);

EventLoopTaskQueueFactory

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

Queue<Runnable> newTaskQueue(int maxCapacity);

The Queue must be thread safe and inherit from Java util. concurrent. BlockingQueue.

After explaining these parameters, we can check the specific NIO implementation of NioEventLoop in detail.

NioEventLoop

First, NioEventLoop, like DefaultEventLoop, inherits from SingleThreadEventLoop:

public final class NioEventLoop extends SingleThreadEventLoop

Represents an EventLoop that uses a single thread to perform tasks.

Firstly, as an implementation of NIO, there must be a selector. Two selectors are defined in NioEventLoop, namely, selector and unwrapped selector:

    private Selector selector;
    private Selector unwrappedSelector;

In the constructor of NioEventLoop, they are defined as follows:

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

First call the openSelector method, and then get the corresponding selector and unwrappedSelector through the returned SelectorTuple.

What is the difference between the two selector s?

In the openSelector method, first return a Selector by calling the openSelector method of the provider, which is unwrappedSelector:

final Selector unwrappedSelector;
unwrappedSelector = provider.openSelector();

Then check disable_ KEY_ SET_ Whether optimization is set. If not, unwrappedSelector and selector are actually the same selector:

DISABLE_KEY_SET_OPTIMIZATION indicates whether to optimize the select key set:

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

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

If DISABLE_KEY_SET_OPTIMIZATION is set to false, which means that we need to optimize the select key set. How do we optimize it?

Let's take a look at the final return:

return new SelectorTuple(unwrappedSelector,
                                 new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));

The second parameter of the returned SelectorTuple is the selector, which is a SelectedSelectionKeySetSelector object.

SelectedSelectionKeySetSelector inherits from the selector. The first parameter passed in by the constructor is a delegate. All methods defined in the selector are called
The difference is that for the select method, the reset method of selectedKeySet will be called first. Here is the implementation of the code by taking the ispen and select methods as examples:

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

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

selectedKeySet is a SelectedSelectionKeySet object. It is a set set used to store SelectionKey. In openSelector() method, use new to instantiate this object:

final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();

Netty actually wants to use this SelectedSelectionKeySet class to manage selectedKeys in the Selector, so next netty uses a highly skilled object replacement operation.

First, judge whether there is sun in the system nio. Implementation of 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;
                }
            }
        });

There are two Set fields in SelectorImpl:

    private Set<SelectionKey> publicKeys;
    private Set<SelectionKey> publicSelectedKeys;

These two fields are the objects we need to replace. If there is SelectorImpl, first use the Unsafe class, call the objectFieldOffset method in PlatformDependent to get the offset of the two fields relative to the object sample, then call putObject to replace the two fields into the selectedKeySet object that is initialized before:

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;
    }

If the system setting does not support Unsafe, 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);

In NioEventLoop, a very important rewriting method we need to pay attention to is the run method, which implements the logic of how to execute the task.

Remember the selectStrategy we mentioned earlier? The run method calls selectStrategy Calculatestrategy returns the strategy of the select, and then passes the judgment
strategy to perform corresponding processing.

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

BUSY_WAIT is not supported in NIO. If it is in select status, select 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:

If strategy > 0, it means that someone has got SelectedKeys, then you need to call processSelectedKeys method to process SelectedKeys:

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

As mentioned above, NioEventLoop has two selector s and a selectedKeys attribute. The selectedKeys store the Optimized SelectedKeys. If the value is not empty, call the processSelectedKeysOptimized method, otherwise call the processselectedkeysplay method.

processSelectedKeysOptimized and processSelectedKeysPlain are not different, but the selectedKeys passed in to be processed are different.

The logic of the process is to get the key of selectedKeys first, then call its attachment method to get the object of attach:

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);
            }

If the channel has not established a connection, this object may be a NioTask to handle channelReady and channelUnregistered events.

If the channel has established a connection, the object may be an AbstractNioChannel.

Different processSelectedKey methods will be called for two different objects.

For the first case, the channelReady method of task will be called:

task.channelReady(k.channel(), k);

In the second case, various methods in ch.unsafe() will be called according to various states of readyOps() of SelectionKey to perform read or close operations.

summary

NioEventLoop is also a SingleThreadEventLoop, but by using NIO technology, we can make better use of existing resources and achieve better efficiency, which is why we use NioEventLoopGroup instead of DefaultEventLoopGroup in the project.

This article has been included in http://www.flydean.com/05-2-netty-nioeventloop/

The most popular interpretation, the most profound dry goods, the most concise tutorial, and many tips you don't know are waiting for you to find!

Welcome to my official account: "those things in procedure", understand technology, know you better!

Topics: Java Netty NIO