Java learning note 14 netty thread model and source code analysis

Keywords: Programming Java Netty network

Netty introduction

Netty is a high performance. The highly scalable asynchronous event driven network application framework greatly simplifies network programming such as TCP and UDP client and server development. Netty's four key elements:

  1. Reactor thread model: a high performance multithreaded programming idea
  2. Self defined Channel concept in Netty: enhanced Channel concept
  3. Channel pipeline responsibility chain design pattern: event handling mechanism
  4. Memory management: enhanced ByteBuf buffer
Netty thread model

In order to make NIO better use of multithreading, Netty implements the Reactor thread model.

There are four concepts in the Reactor model:

  1. Resources (request / task)
  2. Synchronous Event Demultiplexer
  3. Dispatcher distributor
  4. Request Handler
Clients Reactor thread (Boss) Worker thread pool
Request -- > Event multiplexer -- > dispatcher -- > processor
Event Loop (Selector event polling) - > request handler long time consuming task or I/O processing read write.. -- >
EventLoopGroup initialization process

EventLoopGroup constructs a group of eventloops (threads). Generally, the multiplex Reactor thread model is adopted. Two groups of eventloopgroups are used to deal with different events in different channels. One group of Main mainly deals with the accept event of the server, and the other group of Sub mainly deals with the I/O event of the client

EventLoopGroup(Main) EventLoopGroup(Sub)
EventLoop (executor thread executor, EventLoop corresponds to a thread)
↓ Run (poll channel event assignment to perform task task) ↓ Processing request
TaskQueue| Selector
More eventloops -->Assign to Sub I/O processing
Main processing server accept Mainly handle client I/O
Execution process Netty4.1.44.Final source location
new NioEventLoopGroup(): constructor ↓ NioEventLoopGroup: Line 43
Determine the number of threads: default cpus*2 ↓ MultithreadEventLoopGroup: 52 lines, 40 lines
new Executor: build thread executor MultithreadEventExecutorGroup: Line 76
For - > newchild(): build EventLoop MultithreadEventExecutorGroup: Line 84
new EventExecutorChooser MultithreadEventExecutorGroup: Line 111
Start of EventLoop

EventLoop implements the executor interface. When the executor method is called to submit a task, it determines whether to start. If it is not started, it calls the built-in executor to create a new thread to trigger the execution of the run method.

Execution process Netty4.1.44.Final source location
executor: request to execute task SingleThreadEventExecutor: execute(): 828 lines
addTask: add to task queue SingleThreadEventExecutor: addTask(): 840 lines
Determine whether it is the call of EventLoop itself SingleThreadEventExecutor: inEventLoop(): 841 lines
startThread -> doStartThread↓ SingleThreadEventExecutor: startThread(): 842 lines
Using executor to create a new thread to execute run SingleThreadEventExecutor: doStartThread(): Line 990; SingleThreadEventExecutor.this.run(): Line 1001
Bind port procedure
Execution process Netty4.1.44.Final source location
Bind (port): ServerBootstrap.bind(20480) ↓ AbstractBootstrap: bind(): 233 lines
Create and initialize channels AbstractBootstrap: initAndRegister(): 260 lines; AbstractBootstrap: newChannel(): 298 lines; ServerBootstrap: init(): 125 lines
Register to the Selector in the EventLoop (submit the task to the EventLoop for execution. After registration, continue binding) AbstractBootstrap: config().group().register(channel): 311 lines; AbstractChannel: register(): 452 lines; doRegister(): 497 lines; AbstractNioChannel: doRegister(): 380 lines
doBind0() -> channel.bind↓ AbstractBootstrap: doBind0(): 344 lines
pipeline.bind↓ AbstractChannel: pipeline.bind: 247 lines; AbstractChannelHandlerContext: bind: 479 lines; AbstractChannelHandlerContext: invokeBind: 506 lines
HandlerContext.bind↓ DefaultChannelPipeline: unsafe.bind: 1346 lines
AbstractUnsafe.bind↓ AbstractChannel: doBind: Line 551
NioServerSocketChannel.doBind NioServerSocketChannel: doBind: Line 132
Channel concept

Channel in Netty is an abstract concept, which can be understood as the enhancement and expansion of JDK NIO Channel.

A lot of properties and methods have been added. For complete information, see code comments. Here are some common properties and methods:

AbstractChannel
-pipeline DefaultChannelPipeline / / in channel event handling link
-eventLoop EventLoop / / the bound EventLoop. The user performs the operation
-unsafe Unsafe / / provides encapsulation of I/O related operations
...
+config() ChannelConfig / / channel configuration information is returned
+read() Channel / / start reading data and trigger the read link call
+write(Object msg) ChannelFuture / / write data and trigger the link call
+Bind (socketaddresses socketaddresses) channelfuture / / bind
...

Netty thread model test code

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Author: Wenx
 * @Description:
 * @Date: Created in 2019/11/25 13:46
 * @Modified By: 
 */
public class MyNettyServer {
    public static void main(String[] args) {
        // Create eventloopgroup accept thread group
        MyEventLoopGroup mainGroup = new MyEventLoopGroup(1);
        // Create eventloopgroup I / O thread group
        MyEventLoopGroup subGroup = new MyEventLoopGroup(2);
        try {
            // Server startup and guidance tool class
            MyServerBootstrap b = new MyServerBootstrap();
            // Configure the reactor thread group processed by the server and other configurations of the server
            b.group(mainGroup, subGroup).channel(ServerSocketChannel.class)
                    .option("SO_BACKLOG", 100)
                    .handler("new LoggingHandler()")
                    .childHandler("new EchoServerHandler()");
            // Start the service through bind
            FutureTask f = b.bind(20480);
            // Block the main thread until the network service is shut down
            f.get();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // Close thread group
            mainGroup.shutdown();
            subGroup.shutdown();
        }
    }
}

class MyServerBootstrap {
    /**
     * main Thread group
     */
    private volatile MyEventLoopGroup group;
    /**
     * sub Thread group
     */
    private volatile MyEventLoopGroup childGroup;

    /**
     * Bind thread group, single reactor mode
     *
     * @param group main/sub Thread group
     * @return
     */
    public MyServerBootstrap group(MyEventLoopGroup group) {
        return group(group, group);
    }

    /**
     * Bind thread group, multi reactor mode
     *
     * @param parentGroup main Thread group
     * @param childGroup  sub Thread group
     * @return
     */
    public MyServerBootstrap group(MyEventLoopGroup parentGroup, MyEventLoopGroup childGroup) {
        if (parentGroup == null) {
            throw new NullPointerException("group");
        }
        if (group != null) {
            throw new IllegalStateException("group set already");
        }
        group = parentGroup;

        if (childGroup == null) {
            throw new NullPointerException("childGroup");
        }
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = childGroup;

        return this;
    }

    /**
     * Configure the channel factory
     *
     * @param channelClass channel Factory class
     * @return
     */
    public MyServerBootstrap channel(Class<?> channelClass) {
        // Pseudo code. Here is the value assigned to channelFactory, which is convenient for subsequent initialization of channel
        // Because we are NIO TCP, we will directly use ServerSocketChannel, SocketChannel in the future

        return this;
    }

    /**
     * configuration parameter
     *
     * @param option Parameter KEY
     * @param value  Parameter value
     * @return
     */
    public MyServerBootstrap option(Object option, Object value) {
        // Pseudo code. Here is to add options to options for later configuration.

        return this;
    }

    /**
     * main Thread group processor
     *
     * @param handler processor
     * @return
     */
    public MyServerBootstrap handler(Object handler) {
        // Pseudocode. Here, the handler is added to the groupHandler to be used by the subsequent pipeline
        // We will simulate the log Handler later, and print the client information when accept ing directly

        return this;
    }

    /**
     * sub Thread group processor
     *
     * @param childHandler processor
     * @return
     */
    public MyServerBootstrap childHandler(Object childHandler) {
        // Pseudo code. Here, the handler is added to childHandler for subsequent pipeline to use
        // We will simulate the EchoHandler to print the data sent by the client and return the data processing

        return this;
    }

    /**
     * Bind port to open service
     *
     * @param inetPort
     * @return
     */
    public FutureTask bind(int inetPort) {
        InetSocketAddress localAddress = new InetSocketAddress(inetPort);
        if (localAddress == null) {
            throw new NullPointerException("localAddress");
        }

        try {
            // 1. Initialization and registration
            ServerSocketChannel channel = initAndRegister();
            // 2. Bind port to start service
            channel.bind(localAddress);
        } catch (Exception e) {
            e.printStackTrace();
        }

        FutureTask futureTask = new FutureTask(() -> null);
        return futureTask;
    }

    /**
     * Initialization and registration
     *
     * @return Channel
     * @throws Exception
     */
    final ServerSocketChannel initAndRegister() throws Exception {
        // 1. Initialize the channel. Use channelFactory to create newChannel. Here, create ServerSocketChannel directly
        ServerSocketChannel channel = ServerSocketChannel.open();
        channel.configureBlocking(false);
        // 2. Initialize the pipeline responsibility chain
        // Pseudo code
        // group load handler, process accept, simulate log handler to print client information
        // childGroup loads childHandler, processes I/O, simulates EchoHandler to print data sent by client and returns data processing

        // 3. Bind serverSocketChannel registration to selector
        // Netty enhances Channel encapsulation with many parameters. If we directly transmit this, it will not be encapsulated. It is easy to understand
        SelectionKey selectionKey = group.register(channel, this);
        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        return channel;
    }

    /**
     * Many parameters should have been included in the Channel encapsulation enhancement, so we will not encapsulate it for easy understanding
     *
     * @param key
     * @throws Exception
     */
    public void channelHandler(SelectionKey key) throws Exception {
        SelectableChannel channel = key.channel();
        if (channel instanceof ServerSocketChannel) {
            acceptHandler((ServerSocketChannel) key.channel());
        } else if (channel instanceof SocketChannel) {
            ioHandler((SocketChannel) key.channel());
        }
    }

    /**
     * mainReactor Processing accept
     *
     * @param server Server Channel
     * @throws Exception
     */
    public void acceptHandler(ServerSocketChannel server) throws Exception {
        SocketChannel client = server.accept();
        client.configureBlocking(false);
        // After receiving the connection from the client, bind the read event registration of SocketChannel to the selector, and distribute it to the I/O thread to continue reading the data.
        SelectionKey selectionKey = childGroup.register(client, this);
        selectionKey.interestOps(SelectionKey.OP_READ);
        System.out.println(Thread.currentThread().getName() + "Receiving connection : " + client.getRemoteAddress());
    }

    /**
     * subReactor Processing I/O
     *
     * @param client Client Channel
     * @throws Exception
     */
    public void ioHandler(SocketChannel client) throws Exception {
        ByteBuffer readBuff = ByteBuffer.allocate(1024);
        while (client.isOpen() && client.read(readBuff) != -1) {
            // In the case of long connection, it is necessary to manually judge whether the reading of data is finished
            // Here is a simple judgment: if the request is more than 0 bytes, it is considered that the request is over
            if (readBuff.position() > 0) {
                break;
            }
        }
        // If there is no data, do not continue the following processing
        if (readBuff.position() == 0) {
            return;
        }
        readBuff.flip();
        byte[] content = new byte[readBuff.limit()];
        readBuff.get(content);
        String response = "Have received:" + new String(content);
        System.out.println(response);
        System.out.println("Come from:" + client.getRemoteAddress());

        // TODO business operation database interface call, etc

        ByteBuffer writeBuff = ByteBuffer.wrap(response.getBytes());
        while (writeBuff.hasRemaining()) {
            client.write(writeBuff);
        }
    }
}

class MyEventLoopGroup {
    /**
     * Default EventLoop threads: cpus*2
     */
    private static final int DEFAULT_EVENT_LOOP_THREADS = Runtime.getRuntime().availableProcessors() * 2;
    /**
     * EventLoop aggregate
     */
    private final MyEventLoop[] children;
    /**
     * Chooser Indexes
     */
    private final AtomicInteger idx = new AtomicInteger();

    /**
     * Constructor
     */
    public MyEventLoopGroup() {
        this(0);
    }

    /**
     * Constructor
     *
     * @param nThreads EventLoop Number of threads
     */
    public MyEventLoopGroup(int nThreads) {
        if (nThreads <= 0) {
            nThreads = DEFAULT_EVENT_LOOP_THREADS;
        }

        children = new MyEventLoop[nThreads];
        for (int i = 0; i < nThreads; i++) {
            try {
                children[i] = newChild();
            } catch (Exception e) {
                throw new IllegalStateException("failed to create a child event loop", e);
            }
        }
    }

    /**
     * Create EventLoop thread
     *
     * @return EventLoop
     * @throws Exception
     */
    public MyEventLoop newChild() throws Exception {
        return new MyEventLoop();
    }

    /**
     * Select an EventLoop thread
     *
     * @return EventLoop
     */
    public MyEventLoop next() {
        // Chooser
        int index = Math.abs(idx.getAndIncrement() % children.length);
        return children[index];
    }

    /**
     * Bind Channel registration to the Selector of EventLoop
     *
     * @param channel
     * @return
     * @throws Exception
     */
    public SelectionKey register(SelectableChannel channel, MyServerBootstrap b) throws Exception {
        return next().register(channel, b);
    }

    /**
     * Close EventLoopGroup
     */
    public void shutdown() {
        for (MyEventLoop el : children) {
            el.shutdown();
        }
    }
}

class MyEventLoop implements Executor {
    /**
     * selector
     */
    private Selector selector;
    /**
     * Task queue
     */
    private final Queue<Runnable> taskQueue;
    /**
     * Task thread
     */
    private volatile Thread thread;
    /**
     * Thread running status
     */
    private volatile boolean running = false;

    /**
     * Constructor
     *
     * @throws IOException
     */
    public MyEventLoop() throws IOException {
        // Initialize selector and task queue
        selector = Selector.open();
        taskQueue = new LinkedBlockingQueue<>();
    }

    /**
     * Add task to task queue and wait for thread to execute
     *
     * @param task Tasks to be performed
     */
    @Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }

        taskQueue.offer(task);
        if (!running || thread == null) {
            startThread();
        }
    }

    /**
     * Start task thread
     */
    private void startThread() {
        if (!running) {
            running = true;
            if (thread == null) {
                thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        while (running) {
                            try {
                                processSelectedKeys();
                            } finally {
                                runAllTasks();
                            }
                        }
                    }
                });
            }
            thread.start();
            System.out.println("Start up:" + thread.getName());
            System.out.println("Come from:" + Thread.currentThread().getName());
        }
    }

    /**
     * Process SelectedKeys
     */
    private void processSelectedKeys() {
        try {
            selector.select(1000);
            // Get query results
            Set<SelectionKey> selected = selector.selectedKeys();
            // Traverse query results
            Iterator<SelectionKey> iterator = selected.iterator();
            while (iterator.hasNext()) {
                // Encapsulated query results
                SelectionKey key = iterator.next();
                iterator.remove();
                int readyOps = key.readyOps();
                // Follow Read and Accept events
                if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                    try {
                        ((MyServerBootstrap) key.attachment()).channelHandler(key);
                        if (!key.channel().isOpen()) {
                            key.cancel(); // If closed, unsubscribe from this KEY
                        }
                    } catch (Exception ex) {
                        key.cancel(); // If there is any exception, cancel the subscription of this KEY
                    }
                }
            }
            selector.selectNow();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Perform all tasks
     */
    private void runAllTasks() {
        // Perform tasks in the queue
        Runnable task;
        while ((task = taskQueue.poll()) != null) {
            task.run();
        }
    }

    public SelectionKey register(SelectableChannel channel, MyServerBootstrap b) throws Exception {
        // register is submitted in the form of task to ensure that there is no contest between selector and the thread of selector.select()
        FutureTask<SelectionKey> f = new FutureTask<>(() -> channel.register(selector, 0, b));
        execute(f);
        System.out.println("register: " + channel.getClass().getName());
        return f.get();
    }

    /**
     * Close EventLoop
     */
    public void shutdown() {
        try {
            running = false;
            if (thread != null) {
                thread.interrupt();
            }
            selector.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Posted by itguysam on Mon, 25 Nov 2019 22:02:59 -0800