Netty combat (eight)

Guide

  • 1. Guidance
    • 1.1 What is bootstrap
    • 1.2 Bootstrap classes
    • 1.3 Bootstrap client and connectionless protocol
    • 1.4 Booting the client
    • 1.5 Compatibility of Channel and EventLoopGroup
  • 2. Boot server
    • 2.1 ServerBootstrap class
    • 2.2 Boot server
  • 3. Guide the client from the Channel
  • 4. Add multiple ChannelHandlers during the boot process
  • 5. Use Netty’s ChannelOption and properties
  • 6. Guide DatagramChannel
  • 7. Close

1. Guide

1.1 What is guidance

Bootstrapping an application is the process of configuring it and making it run. Guidance can be simply thought of as combining the scattered ChannelPipeline, ChannelHandler and EventLoop into a module that completes the application.

1.2 Bootstrap class

The bootstrap class hierarchy includes an abstract parent class and two concrete bootstrap subclasses.

Compared to treating specific boot classes as boots for server and client respectively, their original intention is to support the functions of different applications.

The server is dedicated to using a parent Channel to accept connections from clients and create child Channels for communication between them;

And the client will most likely only need a single, parentless Channel for all network interactions.

Such as UDP, because they do not require a separate Channel for each connection.

Several Netty components that we have studied in previous articles are involved in the bootstrapping process, and some of them are used in both client and server. Bootstrap steps that are common between the two application types are handled by AbstractBootstrap, while bootstrap steps specific to the client or server are handled by Bootstrap or ServerBootstrap, respectively.

AbstractBootstrap class declaration:

public abstract class AbstractBootstrap
<B extends AbstractBootstrap<B,C>,C extends Channel>

In this signature, the subtype B is a type parameter of its supertype, so a reference to the runtime instance can be returned to support method chaining (the so-called fluent syntax).

Its subclasses can be declared in two ways, namely:

public class Bootstrap
extends AbstractBootstrap<Bootstrap, Channel>
public class ServerBootstrap
extends AbstractBootstrap<ServerBootstrap, ServerChannel>

Does it look familiar? We used them to build the client and server in the second blog post.

1.3 Bootstrap clients and connectionless protocols

The Bootstrap class is used in clients or applications using a connectionless protocol, and most of its methods are inherited from the AbstractBootstrap class.

Name Description
Bootstrap group(EventLoopGroup) Set the EventLoopGroup used to handle all Channel events
Bootstrap channel(Class) Bootstrap channelFactory(ChannelFactory) The channel() method specifies the implementation class of the Channel. If the implementation class does not provide a default constructor, you can specify a factory class by calling the channelFactory() method, which will be called by the bind() method
Bootstrap localAddress( SocketAddress) Specifies the local address to which the Channel should bind. If not specified, a random address will be created by the operating system. Or, you can also specify localAddress by bind() or connect() method
Bootstrap option(ChannelOption option, T value) Set ChannelOption, which will The ChannelConfig that is applied to each newly created Channel. These options will be set to the Channel via the bind() or connect() method, whichever is called first. Calling this method after the Channel has already been created will have no effect. The supported ChannelOptions depend on the type of Channel used.
Bootstrap attr(Attribute key, T value) Specify the attribute value of the newly created Channel. These property values are set to the Channel via the bind() or connect() methods, depending on which is called first. This method will have no effect after the Channel has been created.
Bootstrap handler(ChannelHandler) Set the ChannelHandler that will be added to the ChannelPipeline to receive event notifications
Bootstrap clone() Create a clone of the current Bootstrap with the same settings as the original Bootstrap
Bootstrap remoteAddress(SocketAddress) Set the remote address. Alternatively, it can also be specified by the connect() method
ChannelFuture connect() Connect to a remote node and return a ChannelFuture, which will be in Receive a notification after the connection operation is completed
ChannelFuture bind() Bind a Channel and return a ChannelFuture, which will be received after the binding operation is completed to the notification, after that the Channel.connect() method must be called to establish the connection

There are too many things here, it is recommended to bookmark this article, and just turn it out and have a look when you use it.

1.4 Bootstrap client

The Bootstrap classes are responsible for creating Channels for clients and applications using connectionless protocols.

Its boot process can be seen in the following diagram:

Let’s take a look at a piece of client code that guides a transport using NIO TCP:

package com.example.netty.bootstrap.niotcp;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

/**
 * @author lhd
 * @date 2023/05/24 15:19
 * @notes bootstrap NIO's Netty client code
 */
public class client {<!-- -->

    public static void main(String[] args) {<!-- -->
        EventLoopGroup group = new NioEventLoopGroup();
        //Create an instance of the Bootstrap class to create and connect a new client Channel
        Bootstrap bootstrap = new Bootstrap();
        //Set EventLoopGroup to provide EventLoop for handling Channel events
        bootstrap. group(group)
                //Specify channel implementation
                .channel(NioSocketChannel.class)
                //Set ChannelInboundHandle for Channel events and data
                .handler(new SimpleChannelInboundHandler<ByteBuf>() {<!-- -->
                    @Override
                    protected void channelRead0(
                            ChannelHandlerContext channelHandlerContext,
                            ByteBuf byteBuf) throws Exception {<!-- -->
                        System.out.println("Received data");
                    }
                } );
        //link to remote host
        ChannelFuture future = bootstrap. connect(
                new InetSocketAddress("www.manning.com", 80));
        future.addListener(new ChannelFutureListener() {<!-- -->
            @Override
            public void operationComplete(ChannelFuture channelFuture)
                    throws Exception {<!-- -->
                if (channelFuture. isSuccess()) {<!-- -->
                    System.out.println("Connection established");
                } else {<!-- -->
                    System.err.println("Connection attempt failed");
                    channelFuture. cause(). printStackTrace();
                }
            }
        } );
    }
}

1.5 Compatibility of Channel and EventLoopGroup

Both Channel and EventLoopGroup have associated EventLoopGroup and Channel implementations. They are mutually compatible.

Compatible EventLoopGroup and Channel:

channel
├───nio
│NioEventLoopGroup
├───oio
│OioEventLoopGroup
└───socket
├───nio
│ NioDatagramChannel
│ NioServerSocketChannel
│ NioSocketChannel
└───oio
OioDatagramChannel
OioServerSocketChannel
OioSocketChannel

2. Boot server

We’ll start our overview of the server bootstrap process with a high-level view of the ServerBootstrap API.

2.1 ServerBootstrap class

Like the Bootstrap class, the ServerBootstrap class has class methods belonging to it.

name description
group Sets the EventLoopGroup to be used by ServerBootstrap. This EventLoopGroup will be used for the I/O processing of the ServerChannel and the accepted sub-Channel
channel Set the ServerChannel class to be instantiated
channelFactory If you cannot create a Channel through the default constructor ①, you can provide a ChannelFactory
localAddress Specifies the local address to which the ServerChannel should bind. If not specified, a random address will be used by the operating system. Alternatively, the localAddress can be specified via the bind() method.
option Specifies the ChannelOption to be applied to the ChannelConfig of the newly created ServerChannel. These options will be set to the Channel via the bind() method. After the bind() method has been called, setting or changing the ChannelOption will have no effect. The supported ChannelOptions depend on the type of Channel used.
childOption Specifies the ChannelOption to apply to the ChannelConfig of the child Channel when the child Channel is accepted. The supported ChannelOptions depend on the type of Channel used.
attr Specify the attribute on the ServerChannel, the attribute will be set to the Channel through the bind() method. Changing them after calling the bind() method will have no effect
childAttr Sets attributes to child Channels that have already been accepted. Subsequent calls will have no effect. The handler setting is added to the ChannelHandler in the ServerChannel’s ChannelPipeline.
childHandler Sets the ChannelHandler that will be added to the ChannelPipeline of accepted child Channels. The difference between the handler() method and the childHandler() method is: the ChannelHandler added by the former is processed by the ServerChannel that accepts the sub-Channel, while the ChannelHandler added by the childHandler() method will be processed by the accepted sub-Channel, which represents a binding Socket to remote node
clone Clone a ServerBootstrap with the same settings as the original ServerBootstrap
bind Binds a ServerChannel and returns a ChannelFuture that will be notified (with success or failure) when the bind operation is complete

How did the server boot? Let’s continue reading.

2.2 Bootstrap server

The above table lists some methods that client-side Bootstrap classes do not have, like: childHandler(), childAttr() and childOption(). These calls support operations specific to server applications. Specifically, ServerChannel implementations are responsible for creating sub-Channels that represent accepted connections. Therefore, ServerBootstrap, which is responsible for bootstrapping a ServerChannel , provides these methods to simplify the task of applying settings to the ChannelConfig of an accepted child Channel.

ServerBootstrap creates a ServerChannel when the bind() method is called, and how does the ServerChannel manage multiple sub-Channels?

Look at this picture:

In code, its boot process should look like this:

NioEventLoopGroup group = new NioEventLoopGroup();
//Create ServerBootstrap
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap. group(group)
//Set the channel
.channel(NioServerSocketChannel.class)
//Set the ChannelInboundHandler used to handle the I/O and data of the accepted sub-Channel
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {<!-- -->

@Override
protected void channelRead0(ChannelHandlerContext ctx,
ByteBuf byteBuf) throws Exception {<!-- -->
System.out.println("Received data");
}
} );
//Bind the Channel through the configured instance of ServerBootstrap
ChannelFuture future = bootstrap. bind(new InetSocketAddress(8080));
future.addListener(new ChannelFutureListener() {<!-- -->
@Override
public void operationComplete(ChannelFuture channelFuture)
throws Exception {<!-- -->
if (channelFuture. isSuccess()) {<!-- -->
System.out.println("Server bound");
} else {<!-- -->
System.err.println("Bound attempt failed");
channelFuture. cause(). printStackTrace();
}
}
} );

3. Bootstrap client from Channel

Suppose your server is handling a client request that requires it to act as a client to a third-party system. When an application (such as a proxy server) must be integrated with an organization’s existing systems (such as Web services or databases), it may arise that the need to bootstrap a client Channel from already accepted sub-Channels. If we create the client in the above way, this will generate extra threads, and the inevitable context switch when exchanging data between the accepted sub-channel and the client channel.

To avoid this, we can share the EventLoop by passing the accepted sub-Channel’s EventLoop to Bootstrap’s group() method. Since all Channels assigned to the EventLoop use the same thread, this avoids the extra thread creation, and associated context switch mentioned earlier.

Implementing EventLoop sharing involves setting up EventLoop by calling the group() method, such as the code:

//Create ServerBootstrap to create ServerSocketChannel, and bind it
ServerBootstrap bootstrap = new ServerBootstrap();
//Set EventLoopGroup, which will provide EventLoop for handling Channel events
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
//Specify the Channel implementation to use
.channel(NioServerSocketChannel.class)
//Set the ChannelInboundHandler used to handle the I/O and data of the accepted sub-Channel
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {<!-- -->
ChannelFuture connectFuture;
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {<!-- -->
Bootstrap bootstrap = new Bootstrap();
//Specify the implementation of Channel
bootstrap.channel(NioSocketChannel.class).handler(
//Set ChannelInboundHandler for inbound I/O
new SimpleChannelInboundHandler<ByteBuf>() {<!-- -->
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in)
throws Exception {<!-- -->
System.out.println("Received data");
}
} );
//Use the same EventLoop as assigned to the accepted child channel
bootstrap.group(ctx.channel().eventLoop());
//Create an instance of the Bootstrap class to connect to the remote host
connectFuture = bootstrap. connect(
new InetSocketAddress("www.manning.com", 80));
}
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {<!-- -->
//When the connection is complete, perform some data operations (such as proxy)
if (connectFuture.isDone()) {<!-- -->
// do something with the data
}
}
} );
//Bind the ServerSocketChannel through the configured ServerBootstrap
ChannelFuture future = bootstrap. bind(new InetSocketAddress(8080));
future.addListener(new ChannelFutureListener() {<!-- -->
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {<!-- -->
if (channelFuture. isSuccess()) {<!-- -->
  System.out.println("Server bound");
} else {<!-- -->
System.err.println("Bind attempt failed");
   channelFuture. cause(). printStackTrace();
}
}
} );

This code expresses a core, which is to reuse EventLoop as much as possible to reduce the overhead caused by thread creation. But sharing EventLoop means sharing threads. Therefore, what we need to pay special attention to is that we cannot bring in stateful data (it was mentioned in the previous article, if you are interested, you can go back and have a look).

4. Add multiple ChannelHandlers during bootstrap

In all the code examples we’ve shown, we’ve called the handler() or childHandler() method to add a single ChannelHandler during bootstrap. This might be sufficient for simple applications, but it won’t satisfy more complex needs. For example, an application that must support multiple protocols will have many ChannelHandlers rather than one large and unwieldy class.

We can deploy as many ChannelHandlers as needed by chaining them together in a ChannelPipeline. But if you can only set one ChannelHandler during bootstrapping, how should you do it?

Here comes the solution: Netty provides a special ChannelInboundHandlerAdapter subclass:

public abstract class ChannelInitializer<C extends Channel>
extends ChannelInboundHandlerAdapter

It defines the following methods:

protected abstract void initChannel(C ch) throws Exception;

How to use this method?

This method provides a convenient way to add multiple ChannelHandlers to a ChannelPipeline.

Simply provide an instance of Bootstrap or ServerBootstrap with a ChannelInitializer implementation, and once the Channel is registered with its EventLoop, your version of initChannel() will be called. After this method returns, the ChannelInitializer instance will remove itself from the ChannelPipeline.

Here’s a code example of it:

//Create ServerBootstrap to create and bind a new Channel
ServerBootstrap bootstrap = new ServerBootstrap();
//Set EventLoopGroup, which will provide EventLoop for handling Channel events
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
//Specify the implementation of Channel
.channel(NioServerSocketChannel.class)
//Register an instance of ChannelInitializerImpl to set up ChannelPipeline
.childHandler(new ChannelInitializerImpl());
//bind to address
ChannelFuture future = bootstrap. bind(new InetSocketAddress(8080));
future. sync();

final class ChannelInitializerImpl extends ChannelInitializer<Channel> {<!-- -->
//Custom ChannelInitializerImpl implementation to set ChannelPipeline
@Override
protected void initChannel(Channel ch) throws Exception {<!-- -->
ChannelPipeline pipeline = ch. pipeline();
//Add the required ChannelHandler to the ChannelPipeline
pipeline. addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
}
}

5. Using Netty’s ChannelOption and properties

Configuring it manually on each Channel creation can become quite tedious. A ChannelOption can be applied to a bootstrap using the option() method. The value we provide will be automatically applied to all Channels created by bootstrap. The available ChannelOption includes details of the underlying connection, such as keep-alive or timeout properties and buffer settings.

How to use ChannelOption to configure Channel:

//Create an AttributeKey to identify the attribute
final AttributeKey<Integer> id = new AttributeKey<Integer>("ID");
//Create an instance of the Bootstrap class to create client Channels and connect them
Bootstrap bootstrap = new Bootstrap();
//Set EventLoopGroup, which provides EventLoop for handling Channel events
bootstrap. group(new NioEventLoopGroup())
//Specify the implementation of Channel
.channel(NioSocketChannel.class)
//Set the ChannelInboundHandler used to handle Channel I/O and data
.handler(new SimpleChannelInboundHandler<ByteBuf>() {<!-- -->
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {<!-- -->
//Use AttributeKey to retrieve the attribute and its value
Integer idValue = ctx.channel().attr(id).get();
// do something with the idValue
}
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {<!-- -->
System.out.println("Received data");
}
}
);
bootstrap.option(ChannelOption.SO_KEEPALIVE,true)
//Set the ChannelOption, which will be set to the created Channel when the connect() or bind() method is called
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
//Store the id attribute
bootstrap.attr(id, 123456);
//Use the configured Bootstrap instance to connect to the remote host
ChannelFuture future = bootstrap. connect(
new InetSocketAddress("www.manning.com", 80));
future.syncUninterruptibly();

6. Bootstrap DatagramChannel

The previous bootstrap code examples used SocketChannel over TCP protocol, but Bootstrap classes can also be used for connectionless protocols. To this end, Netty provides various DatagramChannel implementations. The only difference is that instead of calling the connect() method, only the bind() method is called.

Using Bootstrap and DatagramChannel:

//Create an instance of Bootstrap to create and bind a new datagram Channel
Bootstrap bootstrap = new Bootstrap();
//Set EventLoopGroup, which provides EventLoop for handling Channel events
bootstrap.group(new OioEventLoopGroup()).channel(
//Set ChannelInboundHandlerOioDatagramChannel.class).handler(
new SimpleChannelInboundHandler<DatagramPacket>(){<!-- -->
@Override
public void channelRead0(ChannelHandlerContext ctx,
DatagramPacket msg) throws Exception {<!-- -->
// Do something with the packet
}
}
);
//Call the bind() method because the protocol is connectionless
ChannelFuture future = bootstrap. bind(new InetSocketAddress(0));
future.addListener(new ChannelFutureListener() {<!-- -->
@Override
public void operationComplete(ChannelFuture channelFuture)
throws Exception {<!-- -->
if (channelFuture. isSuccess()) {<!-- -->
System.out.println("Channel bound");
} else {<!-- -->
System.err.println("Bind attempt failed");
channelFuture. cause(). printStackTrace();
}
}
});

7. Close

Bootstrap gets your application up and running, but sooner or later you need to shut it down gracefully. Of course, you can also let the JVM handle everything on exit, but this does not meet the definition of elegance, which means releasing resources cleanly.

We need to close the EventLoopGroup, which will process any pending events and tasks, and then release all active threads. This is what calling the EventLoopGroup.shutdownGracefully() method does.

This method call will return a Future that will be notified when shutdown is complete. It should be noted that the shutdownGracefully() method is also an asynchronous operation, so you need to block and wait until it completes, or register a listener with the returned Future to be notified when the shutdown completes.

Graceful shutdown:

EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap. group(group)
.channel(NioSocketChannel.class);
...
//shutdownGracefully() method will release all resources and close all channels currently in use
Future<?> future = group. shutdownGracefully();
// block until the group has shutdown
future.syncUninterruptibly();

Alternatively, you can explicitly call Channel.close() on all active Channels before calling EventLoopGroup.shutdownGracefully(). But in any case, remember to close the EventLoopGroup itself.