03 What Does the Bootstrapper Do, Both Client and Server Startup Require Some Operations

03 What does the Bootstrapper do, both client and server startup require some operations #

Hello, I am Ruodi. In the last class, we introduced the roles of the core components in Netty and how they work together. Starting from this class, we will analyze each core component of Netty in depth. I will explain how to use Netty for basic program development using relevant code examples to help you get started with Netty quickly.

When using Netty to write network applications, we always start with the Bootstrap. As the entry point of the entire Netty client and server, the Bootstrap assembles the core components of Netty like building blocks. In this class, starting from the Bootstrap of Netty, I will teach you how to use Netty for basic program development.

Starting with a simple HTTP server #

HTTP server is one of the most commonly used tools. Just like the traditional web container Tomcat and Jetty, Netty can also be used to develop an HTTP server easily. Starting with a simple HTTP server, I will use code examples to show you how Netty programs are configured and started, and how the Bootstrap establishes connections with the core components.

It is very complex to implement a high-performance, fully functional, and robust HTTP server. This article only focuses on helping you understand the basic process of developing Netty network applications. Therefore, only the most basic request-response process is implemented:

  1. Set up an HTTP server, configure relevant parameters, and start it.
  2. Send an HTTP request from a browser or terminal.
  3. Successfully receive the response from the server.

Netty’s modular design is very elegant, and the startup process for both the client and the server is fairly consistent. As a developer, you only need to follow a similar pattern. In most cases, you only need to implement a series of ChannelHandlers related to your business logic, and add the HTTP-related encoders and decoders provided by Netty to quickly build the server framework. Therefore, we only need two classes to complete the simplest HTTP server. These classes are the server startup class and the business logic processing class. I will explain them separately by showing the complete code implementation.

Server startup class #

The following code structure can be used for developing all Netty server startup classes. Let’s break down the process: First, create a bootstrap; then configure the thread model, bind the business logic handler through the bootstrap, and configure some network parameters; finally, bind the port to complete the server startup.

public class HttpServer {

    public void start(int port) throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup();

        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap b = new ServerBootstrap();

            b.group(bossGroup, workerGroup)

                    .channel(NioServerSocketChannel.class)

                    .localAddress(new InetSocketAddress(port))

                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override

                        public void initChannel(SocketChannel ch) {

                            ch.pipeline()

                                    .addLast("codec", new HttpServerCodec())                  // HTTP encoding and decoding

                                    .addLast("compressor", new HttpContentCompressor())       // HttpContent compression

                                    .addLast("aggregator", new HttpObjectAggregator(65536))   // HTTP message aggregation

                                    .addLast("handler", new HttpServerHandler());             // Custom business logic handler

                        }

                    })

                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = b.bind().sync();

            System.out.println("Http Server started, Listening on " + port);

            f.channel().closeFuture().sync();

        } finally {

            workerGroup.shutdownGracefully();

            bossGroup.shutdownGracefully();

        }

    }

    public static void main(String[] args) throws Exception {

        new HttpServer().start(8088);

    }

}

Business logic processing class for the server #

As shown in the code below, HttpServerHandler is a custom business logic processing class. It is an inbound ChannelInboundHandler type processor responsible for receiving decoded HTTP request data and writing the processing results back to the client.

public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {

        String content = String.format("Receive http request, uri: %s, method: %s, content: %s%n", msg.uri(), msg.method(), msg.content().toString(CharsetUtil.UTF_8));

        FullHttpResponse response = new DefaultFullHttpResponse(
                HttpVersion.HTTP_1_1,
                HttpResponseStatus.OK,
                Unpooled.wrappedBuffer(content.getBytes()));

        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);

    }

}

Through the above two classes, we can complete the basic request-response process of the HTTP server. The testing steps are as follows:

  1. Start the main function of HttpServer.
  2. Send an HTTP request from the terminal or browser.

The test result is as follows:

$ curl http://localhost:8088/abc

$ Receive http request, uri: /abc, method: GET, content:

Of course, you can also use Netty to implement an HTTP client yourself. The client and server startup class codes are very similar. I have provided an implementation code for HTTPClient in the appendix for your reference.

Through the above example of a simple HTTP server, we are now familiar with the programming model of Netty. Now, I will provide a detailed introduction to the bootstrapper based on this example.

Bootstrapper Practical Guide #

The startup process of the Netty server can roughly be divided into three steps:

  1. Configure the thread pool.
  2. Initialize the channel.
  3. Bind the port.

Next, I will introduce in detail what needs to be done in each step.

Configure the thread pool #

Netty is developed using the Reactor model, and it can easily switch between three Reactor patterns: single-threaded mode, multi-threaded mode, and master-slave multi-threaded mode.

Single-threaded mode #

In the Reactor single-threaded model, all I/O operations are handled by a single thread, so only one EventLoopGroup needs to be started.

EventLoopGroup group = new NioEventLoopGroup(1);

ServerBootstrap b = new ServerBootstrap();

b.group(group)

Multi-threaded mode #

The Reactor single-threaded model has serious performance bottlenecks, so the Reactor multi-threaded model was introduced. In Netty, using the Reactor multi-threaded model is very similar to using the single-threaded model. The only difference is that NioEventLoopGroup can be created without any parameters, and it will start double the number of CPU cores as the number of threads by default. Of course, you can also manually set a fixed number of threads.

EventLoopGroup group = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();

b.group(group)

Master-slave multi-threaded mode #

In most scenarios, we use the master-slave multi-threaded Reactor model. The boss is the master reactor, and the workers are the slave reactors. They use different NioEventLoopGroups. The master reactor is responsible for handling Accept operations and then registering the channels to the slave reactors. The slave reactors are mainly responsible for all I/O events during the lifetime of the channels.

EventLoopGroup bossGroup = new NioEventLoopGroup();

EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)

The configuration methods for the three Reactor thread models mentioned above show that Netty’s thread model is highly customizable. It only requires simple configuration of different parameters to enable different Reactor thread models, without the need to change other code, greatly reducing the development and debugging costs for users.

Channel Initialization #

Setting Channel Types #

NIO model is the most mature and widely used model in Netty. Therefore, it is recommended to use NioServerSocketChannel as the channel type for Netty server, and NioSocketChannel for client. The setting method is as follows:

b.channel(NioServerSocketChannel.class);

Of course, Netty provides multiple types of Channel implementations, and you can switch them as needed, such as OioServerSocketChannel, EpollServerSocketChannel, etc.

Registering ChannelHandlers #

In Netty, you can register multiple ChannelHandlers through ChannelPipeline. Each ChannelHandler has its own responsibilities, which maximizes code reuse and fully reflects the elegance of Netty’s design. So how do we add multiple ChannelHandlers through the bootstrapper? In fact, it’s very simple. Let’s take a look at the HTTP server code example:

b.childHandler(new ChannelInitializer<SocketChannel>() {

    @Override

    public void initChannel(SocketChannel ch) {

        ch.pipeline()

                .addLast("codec", new HttpServerCodec())

                .addLast("compressor", new HttpContentCompressor())

                .addLast("aggregator", new HttpObjectAggregator(65536)) 

                .addLast("handler", new HttpServerHandler());

    }

})

ServerBootstrap’s childHandler() method needs to register a ChannelHandler. ChannelInitializer is an anonymous class that implements the ChannelHandler interface, and is instantiated as a parameter of ServerBootstrap.

Each Channel is initialized with a Pipeline, which is primarily used for service orchestration. The Pipeline manages multiple ChannelHandlers. I/O events are propagated through the ChannelHandlers in sequence, and the ChannelHandlers are responsible for handling business logic. The above HTTP server example uses a chain-like method to load multiple ChannelHandlers, including HTTP codec handler, HTTP content compression handler, HTTP message aggregation handler, and custom business logic handler.

In previous chapters, we introduced the concepts of inbound ChannelHandlers and outbound ChannelHandlers in the ChannelPipeline. Here, combined with the scenario of HTTP request-response, let’s analyze the flow of data in the ChannelPipeline. When the server receives an HTTP request, it will be sequentially processed by the HTTP codec handler, HTTP content compression handler, HTTP message aggregation handler, and custom business logic handler, and then sent back to the client through the HTTP content compression handler and HTTP codec handler.

Setting Channel Parameters #

Netty provides a very convenient method for setting Channel parameters. There are a large number of parameters for the Channel, and it would be very cumbersome to set each parameter individually. Fortunately, Netty provides default parameter settings, which already meet our needs in practical scenarios. We only need to modify the parameters we care about.

b.option(ChannelOption.SO_KEEPALIVE, true);

ServerBootstrap has two methods for setting Channel attributes: option and childOption. option is mainly used to set parameters for the Boss thread group, while childOption is for the Worker thread group.

Here are some commonly used parameter meanings, which you can set as needed based on your business scenario.

Parameter Meaning
SO_KEEPALIVE Setting it to true means that the TCP SO_KEEPALIVE attribute is enabled, and TCP actively detects the connection status, that is, keeping the connection alive.
SO_BACKLOG The maximum length of the fully completed handshake request queue. At the same time, the server may handle multiple connections. In scenarios with high concurrency and massive connections, this parameter should be appropriately increased.
TCP_NODELAY Netty defaults to true, which means that data is sent immediately. If set to false, it means that the Nagle algorithm is enabled. This algorithm accumulates TCP network packets to a certain amount before sending them. Although it can reduce the number of packets sent, it will cause some data delay. In order to minimize data transmission delay, Netty disables Nagle algorithm by default.
SO_SNDBUF TCP data send buffer size.
SO_RCVBUF TCP data receive buffer size.
SO_LINGER Set the time of delaying the close, waiting for the data in the buffer to be sent.
CONNECT_TIMEOUT_MILLIS Connection timeout for establishing a connection.

Binding Ports #

After completing the above configuration of Netty, the bind() method will trigger the actual startup, and the sync() method will block until the entire startup process is completed. The specific usage is as follows:

ChannelFuture f = b.bind().sync();

There are many details involved in the bind() method, which we will analyze in detail in the “Source Code: Analyzing the Server Startup Process from Linux” course. We won’t go into detail here.

With regards to how to use the bootstrapper to develop a Netty network application, we have finished introducing it. The server startup process cannot be separated from configuring the thread pool, initializing the Channel, and binding the port. In the process of Channel initialization, the most important thing is to bind the custom business logic implemented by the user. Isn’t it simple? You can refer to the examples in this section and try to develop a simple program yourself.

Summary #

In this section, we focused on Netty’s bootstrapper and learned how to develop the most basic network application program. The bootstrapper connects all the core components of Netty, and using the bootstrapper as a starting point for learning Netty helps us get started quickly. As a very convenient tool, Netty’s bootstrapper avoids the need to manually create and configure Channels, and there are many knowledge points to explore. We will explore its implementation principles together in the subsequent source code chapters.

Appendix #

HTTP Client Class #

public class HttpClient {

    public void connect(String host, int port) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();

try {

    Bootstrap b = new Bootstrap();

    b.group(group);

    b.channel(NioSocketChannel.class);

    b.option(ChannelOption.SO_KEEPALIVE, true);

    b.handler(new ChannelInitializer<SocketChannel>() {

        @Override

        public void initChannel(SocketChannel ch) {

            ch.pipeline().addLast(new HttpResponseDecoder());

            ch.pipeline().addLast(new HttpRequestEncoder());

            ch.pipeline().addLast(new HttpClientHandler());

        }

    });

    ChannelFuture f = b.connect(host, port).sync();

    URI uri = new URI("http://127.0.0.1:8088");

    String content = "hello world";

    DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,

            uri.toASCIIString(), Unpooled.wrappedBuffer(content.getBytes(StandardCharsets.UTF_8)));

    request.headers().set(HttpHeaderNames.HOST, host);

    request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);

    request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());

    f.channel().write(request);

    f.channel().flush();

    f.channel().closeFuture().sync();

} finally {

    group.shutdownGracefully();

}

}

public static void main(String[] args) throws Exception {

HttpClient client = new HttpClient();

client.connect("127.0.0.1", 8088);

}


}


#### HTTP client business handler class


public class HttpClientHandler extends ChannelInboundHandlerAdapter {

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg) {

if (msg instanceof HttpContent) {

HttpContent content = (HttpContent) msg;

ByteBuf buf = content.content();

System.out.println(buf.toString(io.netty.util.CharsetUtil.UTF_8));

buf.release();

}

}

}