Scan the qr code below or search the wechat official account, cainiao Feiyafei, you can follow the wechat official account, read more Spring source code analysis and Java concurrent programming articles.

The problem

Old rules, Netty source is very difficult, very complex, in order to learn faster to understand the new knowledge, so or with the problem to learn the source code. Netty, as an event-driven high-performance networking framework, actually still uses NIO in the JDK at the bottom. Netty has made a lot of optimizations and encapsulation in THE JDK NIO, making it easier for developers to use NIO.

When you do network programming with the JDK’s native NIO, you need to do two things first: 1. Create and initialize a ServerSocketChannel; 2. Bind the ServerSocketChannel to the multiplexer Selector. Since Netty encapsulates THE NIO of the JDK, when do these two steps occur in Netty? (in Netty, the server Channel is NioServerSocketChannel.)

The obvious answer is: when the Netty server is started. The rest of this article will take a look at Netty’s approach to server Channel initialization using source code. An analysis of the source code binding to Selector will be published in the next article.

Component description

Before looking at the source code of the Netty server startup process, let’s take a brief look at the related components of Netty. These components are very important and will be explained in detail in separate articles, but I’ll just briefly cover them today.

NioEventLoopGroup NioEventLoopGroup NioEventLoopGroup NioEventLoopGroup It can be simply understood as a thread group, which contains a group of threads, which are used to perform tasks such as client access and IO data reading and writing.

NioEventLoop, can be simply understood as a thread, multiple NioEventLoop together is a NioEventLoopGroup.

Pipeline, which is a two-way linked list of ChannelHandlerContext elements, ChannelHandlerContext wrapped in a ChannelHandler. For the server, whenever a new connection comes in, it is propagated through the Pipeline. This component is very important, in the actual work, the use of Netty to achieve their own custom business logic, is to modify Pipeline to achieve.

Server startup

Let’s start with a bit of server startup demo code.

In the example code, ① to ⑤ set some properties for the Netty server. What needs additional explanation is step 2. In step 2, you need to set the channel type to NioServerSocketChannel for the server. When calling channle (NioServerSocketChannel. Class) method, will be called to AbstractBootstrap class channel () method, it do is create a a ReflectiveChannelFactory factory, And assign the ReflectiveChannelFactory instance to the channelFactory property of the AbstractBootstrap class.

For ReflectiveChannelFactory, it has a Constructor type property called Constructor, which is the reflection Constructor that will be used later to create a Channel. For here, the constructor is NioServerSocketChannel. A reflection of the class constructor, through which can create a NioServerSocketChannel instance objects. Source code for the channel() method

Source code for the ReflectiveChannelFactory constructor

In step 6, the bind() method of ServerBootstrap is called, which passes in a port number: 8080. From the outside, this method looks extremely simple, but it does a lot. In this method, we initialize the channel on the server, register the channel with the Selector, and then bind the port to start the server. Let’s look at how the server Channel is initialized and bound by analyzing its source code.

When serverbootstrap.bind (8080) is called, serverbootstrap.dobind () is called. I have simplified the source code of the doBind() method, keeping only the core code as follows.

Two very important methods are called in the doBind() method. One is the initAndRegister() method, which initializes a Channel and registers it with a Selector. The other is the doBind0() method, which is used to bind port numbers. Only the first half of the initAndRegister() method will be examined today.

The channel to initialize

The source code for initAndRegister() is as follows.

In initAndRegister () method, first by calling the channelFactory. NewChannel () to create a Channle, for the service side is NioServerSocketChannel created here. So how is it created?

The channelFactory (ReflectiveChannelFactory) contains a property called constructor. The constructor is NioServerSocketChannel. A reflection of the class constructor, by calling the constructor. NewInstance () method, which is called the NioServerSocketChannel class no arguments constructor. NioServerSocketChannel’s no-parameter constructor is as follows.

public NioServerSocketChannel(a) {
    / / DEFAULT_SELECTOR_PROVIDER value is SelectorProvider provider ()
    // call newSocket()
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
Copy the code

The newSocket() method is first called in the no-argument constructor, which creates a ServerSocketChannel through the JDK’s native API.

private static ServerSocketChannel newSocket(SelectorProvider provider) {
    try {
        / / the value of the provider is SelectorProvider provider ();
        // Create ServerSocketChannel by calling NIO's native API in JDK
        return provider.openServerSocketChannel();
    } catch (IOException e) {
        throw new ChannelException(
                "Failed to open a server socket.", e); }}Copy the code

When the newSocket() method is called, a ServerSocketChannel is returned, followed by a call to this(ServerSocketChannel), a parameter constructor of the NioServerSocketChannel class.

public NioServerSocketChannel(ServerSocketChannel channel) {
	// Call the constructor of the parent class, selectionKey. OP_ACCEPT with a value of 1, indicating that the service's channel is interested in receiving events
    super(null, channel, SelectionKey.OP_ACCEPT);
    // config is used to save server channel configuration
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
Copy the code

And then it goes all the way up to the constructor of the parent class, and it ends up in the constructor of the AbstractNioChannel class.

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    // Continue calling the parent class
    super(parent);
    this.ch = ch;
    // Save the event of interest. OP_ACCEPT indicates that the event of interest is received.
    // It is only saved, but it does not mean that it is ready to receive new connections
    this.readInterestOp = readInterestOp;
    try {
    	// Set to non-blocking mode, the JDK's native NIO is consistent
        ch.configureBlocking(false);
    } catch (IOException e) {
        / /...}}Copy the code

Here we call the constructor of the parent class, save channle, readInterestOp, and set channel to non-blocking mode. Note that this only saves the value of readInterestOp. It does not mean that the server is ready to receive new connections because it has no bound port. Then look at what the parent constructor does. The AbstractChannel constructor is eventually called.

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    id = newId();
    unsafe = newUnsafe();
    pipeline = newChannelPipeline();
}
Copy the code

The constructor in AbstractChannel does three main things. First: Assign a value to the id of the channel, which is a unique identifier and not very useful. Second: with newUnsafe(), a NioMessageUnsafe object is created, which is the object behind the server that receives connections and so on. Unsafe doesn’t immediately remind you of the Unsafe class in the JDK, which does a similar thing, manipulating memory directly and being very powerful. Third: newChannelPipeline() creates a Pipeline through which all new connections will propagate. The Pipeline initializes a header and a tail as follows.

protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

    tail = new TailContext(this);
    head = new HeadContext(this);

    head.next = tail;
    tail.prev = head;
}
Copy the code

The structure of Pipeline is as follows.

At this point, the NioServerSocketChannel constructor is finally finished and returned to initAndRegister(), where the channel object has a value, and init() is called to initialize the channel. When the init() method is called, the Init () method of ServerBootstrap is called. Init () : select options, attrs, childOptions, childAttrs, childOptions, childAttrs, etc. The code is relatively simple, so I’m not going to expand it all out here.

The core logic of the init() method is on the last line: p.addlast (). When p.addlast () is called, a ChannelHandler will be added to the pipeline. Here is simplified code for the init() method.

void init(Channel channel) throws Exception {
    / /...
    ChannelPipeline p = channel.pipeline();
	// Add an anonymous class
    p.addLast(new ChannelInitializer<Channel>() {
    	// This is followed by a callback to the initChannel() method
        @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            Handler () gets handler, which is the NettyServerHandler class we specified in step 4
            ChannelHandler handler = config.handler();
            if(handler ! =null) {
            	// Add to pipeline
                pipeline.addLast(handler);
            }
            // Add a task to the NioEventLoop, which executes the run() method inside
            ch.eventLoop().execute(new Runnable() {
                @Override
                public void run(a) {
                	// Add a ChannelHandler to the pipeline in the run() method
                    pipeline.addLast(newServerBootstrapAcceptor( ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); }}); }}); }Copy the code

An anonymous class is added to the pipeline: new ChannelInitializer

(){}. Note that the initChannel() method in this anonymous class is not executed. After the addition, the structure of the pipeline becomes the structure shown below.

When a subsequent channel is registered with a Selector, it is called back to the ChannelInitializer’s initChannel(ChannelHandlerContext CTX) method (note that initChannel() is an overloaded method). The source code is as follows.

private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    if (initMap.add(ctx)) { // Guard against re-entrance.
        try {
        	// Callback the initChannel() method of the anonymous class
            initChannel((C) ctx.channel());
        } catch (Throwable cause) {
            // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...) .
            // We do so to prevent multiple calls to initChannel(...) .
            exceptionCaught(ctx, cause);
        } finally {
        	// Finally remove the anonymous class from pipeline
            ChannelPipeline pipeline = ctx.pipeline();
            if (pipeline.context(this) != null) {
                pipeline.remove(this); }}return true;
    }
    return false;
}
Copy the code

On the initChannel(ChannelHandlerContext CTX method, the callback is made to the initChannel(Final Channel CH) method of the anonymous class in the pipeline above. In the logic of the anonymous class’s initChannel() method, a NettyServerHandler class is first added to the pipeline, which we specified in step 4. Once added, the pipeline becomes the following structure.

When the NettyServerHandler is added, a task is then added to the NioEventLoop via ch.eventLoop().execute(new Runnable(){}), which is not executed immediately but asynchronously in another thread. When the anonymous class’s initChannel(Final Channel CH) method is complete, it goes back to initChannel(ChannelHandlerContext CTX) and finally executes the block. A piece of code is executed in finally.

ChannelPipeline pipeline = ctx.pipeline();
if (pipeline.context(this) != null) {
    pipeline.remove(this);
}
Copy the code

This code removes the anonymous class created earlier from the pipeline. After removal, the structure of the pipeline becomes as shown below. Why delete it? Since the anonymous class is only used to add elements to the pipeline during startup, the structure of the pipeline is fixed after the server starts, and the anonymous class has no purpose to exist, so it will be removed.

Since a task was added to the NioEventLoop on the front, when the task is executed, another element is added to the pipeline: ServerBootstrapAcceptor. This class is important because it will be used later to take care of all new connections. ServerBootstrapAcceptor is a node in the pipeline, so all new connections are processed by ServerBootstrapAcceptor. The structure of the final pipeline is shown in the figure below. Important things three times: This picture is very important to remember. Keep in mind! Keep in mind!

conclusion

  • This paper mainly analyzes the initialization process of server SERVER SocketChannel with source code, but does not analyze how to register the Server socketChannel with Selector, and the process of server port binding. These two parts will be analyzed in detail in the next two articles.
  • In the analysis of NioServerSocketChannel initialization process, focus on the analysis of pipeline change process and the final structure, which will be of great help to the subsequent analysis of the new connection access process.
  • Finally, Netty’s source code is complex, its startup process is important, and the code is complex. Class inheritance is very complex. Therefore, you are advised to Debug Netty by yourself. This will help you learn Netty.