Netty combat (eight)

1. Guidance

1.1 What is bootstrap

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 classes

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 committed 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 Channel with no parent 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 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 client and connectionless protocol

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 describe
Bootstrap group(EventLoopGroup) Set the EventLoopGroup used to handle all events of the Channel
Bootstrap channel(Class<? extends C>) Bootstrap channelFactory(ChannelFactory<? extends C>) The channel() method specifies the implementation class of 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. Alternatively, localAddress can also be specified through the bind() or connect() method
Bootstrap option(ChannelOption option,T value) Sets the ChannelOption that will be applied to the ChannelConfig of 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 ChannelOption depends on the type of Channel used.
Bootstrap attr(Attribute key, T value) Specifies the attribute values ​​for 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) Sets 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() Connects to a remote node and returns a ChannelFuture that will be notified when the connection operation completes
ChannelFuture bind() Binds a Channel and returns a ChannelFuture that will be notified after the binding operation is complete, after which the Channel.connect() method must be called to establish a 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 Booting the 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:

insert image description here

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 引导了NIO 的 Netty 客户端代码
 */
public class client {
    
    

    public static void main(String[] args) {
    
    
        EventLoopGroup group = new NioEventLoopGroup();
        //创建一个Bootstrap类的实例以创建和连接新的客户端Channe
        Bootstrap bootstrap = new Bootstrap();
        //设置 EventLoopGroup,提供用于处理 Channel事件的 EventLoop
        bootstrap.group(group)
                //指定channel实现
                .channel(NioSocketChannel.class)
                //设置用于 Channel 事件和数据的ChannelInboundHandle
                .handler(new SimpleChannelInboundHandler<ByteBuf>() {
    
    
                    @Override
                    protected void channelRead0(
                            ChannelHandlerContext channelHandlerContext,
                            ByteBuf byteBuf) throws Exception {
    
    
                        System.out.println("Received data");
                    }
                } );
        //链接到远程主机
        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 an overview view of the ServerBootstrap API.

2.1 ServerBootstrap class

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

name describe
group Sets the EventLoopGroup to be used by ServerBootstrap. This EventLoopGroup will be used for 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 by the bind() method
option Specifies the ChannelOption to apply 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 ChannelOption depends on the type of Channel used.
childOption Specifies the ChannelOption to apply to the ChannelConfig of the sub-Channel when the sub-Channel is accepted. The supported ChannelOption depends on the type of Channel used.
attr Specifies the properties on the ServerChannel, which will be set to the Channel via the bind() method. Changing them after calling the bind() method will have no effect
childAttr Set properties to sub-Channels that have 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 that 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 completes

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

2.2 Boot 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:

insert image description here

In code, its boot process should look like this:

NioEventLoopGroup group = new NioEventLoopGroup();
//创建ServerBootstrap 
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
//设置channel
.channel(NioServerSocketChannel.class)
//设置用于处理已被接受的子Channel的I/O及数据的 ChannelInboundHandler
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
    
    

@Override
protected void channelRead0(ChannelHandlerContext ctx,
ByteBuf byteBuf) throws Exception {
    
    
System.out.println("Received data");
}
} );
//通过配置好的ServerBootstrap的实例绑定该Channel
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. Guide the client from the 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 direct a client Channel from already accepted sub-Channels may arise. 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 situation, 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:

//创建 ServerBootstrap 以创建ServerSocketChannel,并绑定它
ServerBootstrap bootstrap = new ServerBootstrap();
//设置 EventLoopGroup,其将提供用以处理 Channel 事件的 EventLoop
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
//指定要使用的Channel 实现
.channel(NioServerSocketChannel.class)
//设置用于处理已被接受的子 Channel 的 I/O 和数据的ChannelInboundHandler
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
    
    
ChannelFuture connectFuture;
@Override
public void channelActive(ChannelHandlerContext ctx)
throws Exception {
    
    
Bootstrap bootstrap = new Bootstrap();
//指定 Channel的实现
bootstrap.channel(NioSocketChannel.class).handler(
//为入站 I/O 设置ChannelInboundHandler
new SimpleChannelInboundHandler<ByteBuf>() {
    
    
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in)
throws Exception {
    
    
System.out.println("Received data");
}
} );
//使用与分配给已被接受的子channel相同的EventLoop
bootstrap.group(ctx.channel().eventLoop());
//创建一个 Bootstrap类的实例以连接到远程主机
connectFuture = bootstrap.connect(
new InetSocketAddress("www.manning.com", 80));
}
@Override
protected void channelRead0(
ChannelHandlerContext channelHandlerContext,
ByteBuf byteBuf) throws Exception {
    
    
//当连接完成时,执行一些数据操作(如代理)
if (connectFuture.isDone()) {
    
    
// do something with the data
}
}
} );
//通过配置好的ServerBootstrap绑定该 ServerSocketChannel
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).

Fourth, add multiple ChannelHandlers during the boot process

In all the code examples we have shown, we have called the handler() or childHandler() method to add a single ChannelHandler during the bootstrap process. 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 we need 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 instance of ChannelInitializer will remove itself from the ChannelPipeline.

Here's a code example of it:

//创建 ServerBootstrap 以创建和绑定新的 Channel
ServerBootstrap bootstrap = new ServerBootstrap();
//设置 EventLoopGroup,其将提供用以处理 Channel 事件的 EventLoop
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
//指定 Channel 的实现
.channel(NioServerSocketChannel.class)
//注册一个 ChannelInitializerImpl 的实例来设置 ChannelPipeline
.childHandler(new ChannelInitializerImpl());
//绑定到地址
ChannelFuture future = bootstrap.bind(new InetSocketAddress(8080));
future.sync();

final class ChannelInitializerImpl extends ChannelInitializer<Channel> {
    
    
//用以设置 ChannelPipeline 的自定义ChannelInitializerImpl 实现
@Override
protected void initChannel(Channel ch) throws Exception {
    
    
ChannelPipeline pipeline = ch.pipeline();
//将所需的ChannelHandler添加到ChannelPipeline
pipeline.addLast(new HttpClientCodec());
pipeline.addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
}
}

5. Use 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 attributes and buffer settings.

How to use ChannelOption to configure Channel:

//创建一个 AttributeKey以标识该属性
final AttributeKey<Integer> id = new AttributeKey<Integer>("ID");
//创建一个 Bootstrap 类的实例以创建客户端 Channel 并连接它们
Bootstrap bootstrap = new Bootstrap();
//设置 EventLoopGroup,其提供了用以处理 Channel事件的 EventLoop
bootstrap.group(new NioEventLoopGroup())
//指定Channel的实现
.channel(NioSocketChannel.class)
//设置用以处理 Channel 的I/O 以及数据的 ChannelInboundHandler
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
    
    
@Override
public void channelRegistered(ChannelHandlerContext ctx)throws Exception {
    
    
//使用 AttributeKey 检索属性以及它的值
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)
//设置 ChannelOption,其将在 connect()或者bind()方法被调用时被设置到已经创建的Channel 上
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
//存储该id 属性
bootstrap.attr(id, 123456);
//使用配置好的 Bootstrap实例连接到远程主机
ChannelFuture future = bootstrap.connect(
new InetSocketAddress("www.manning.com", 80));
future.syncUninterruptibly();

6. Guide DatagramChannel

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

Using Bootstrap and DatagramChannel:

//创建一个 Bootstrap 的实例以创建和绑定新的数据报 Channel
Bootstrap bootstrap = new Bootstrap();
//设置 EventLoopGroup,其提供了用以处理 Channel 事件的 EventLoop
bootstrap.group(new OioEventLoopGroup()).channel(
//设置用以处理 Channel 的I/O 以及数据的 ChannelInboundHandlerOioDatagramChannel.class).handler(
new SimpleChannelInboundHandler<DatagramPacket>(){
    
    
@Override
public void channelRead0(ChannelHandlerContext ctx,
DatagramPacket msg) throws Exception {
    
    
// Do something with the packet
}
}
);
//调用 bind()方法,因为该协议是无连接的
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 take care of everything when it exits, 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()方法将释放所有的资源,并且关闭所有的当前正在使用中的 Channe
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.

Guess you like

Origin blog.csdn.net/qq_35241329/article/details/130847210