Netty_浅看ServerBootstrap配置
2025-01-22 08:19:30    2.6k 字   
This post is also available in English and alternative languages.

了解 ServerBootstrap 的一些方法。


  • 网上很多资料都将创建的 NioEventLoopGroup 对象称之为: bossGroup(boss) 和 workGroup(work),这里OP不会这么称呼,而是采用源码中变量的命名,即: parentGroupchildGroup

  • 本文 Netty 源码解析基于 4.1.86.Final 版本。


1. 继承关系

ServerBootstrap 的继承实现关系比较简单,向上只继承了 AbstractBootstrap。

ServerBootstrap继承关系

2. group方法

group 方法用于设置 NioEventLoopGroup ,NioEventloopGroup 本质是线程池, 它的核心作用是接收 I/O 请求,并分配 NioEventLoop 执行处理;

如果调用 group(EventLoopGroup group) 方法,对于 Netty 来说,parentGroup 和 childGroup 是同一个 NioEventLoopGroup,对照的就是 单Reactor模式,但至于是 单reactor-单线程模型 还是 单reactor-多线程模型 下面再说。

而如果像 示例代码(点击跳转) 一样,调用 group(EventLoopGroup parentGroup, EventLoopGroup childGroup) 方法,parentGroup 和 childGroup 分别是不同的 NioEventLoopGroup ,对照的就是 主从reactor-多线程模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.netty.bootstrap.ServerBootstrap

@Override
public ServerBootstrap group(EventLoopGroup group) {
// 如果只使用一个线程池,即 parentGroup 和 childGroup 是同一个线程池
return group(group, group);
}

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
super.group(parentGroup);
if (this.childGroup != null) {
throw new IllegalStateException("childGroup set already");
}
this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
return this;
}
// ...

父类AbstractBootstrap中的group方法

1
2
3
4
5
6
7
8
9
10
// io.netty.bootstrap.AbstractBootstrap#group(io.netty.channel.EventLoopGroup)

public B group(EventLoopGroup group) {
ObjectUtil.checkNotNull(group, "group");
if (this.group != null) {
throw new IllegalStateException("group set already");
}
this.group = group;
return self();
}

parentGroup 保存在父类 AbstractBootstrap 中,而 childGroup 保存在子类 ServerBootstrap 中。

2.1. 如何配置三种模型

Netty 在配置 ServerBootstrap 时通过 group 方法传递不同的参数,就可以支持 Reactor 的三种线程模型:

2.1.1. 单reactor-单线程模型

1
2
3
4
// 示例代码
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstra.group(eventGroup);

此处需要显式的构建一个线程池数量为1 的 NioEventLoopGroup,parentGroup 和 childGroup 使用同一个 EventLoopGroup;如果使用 NioEventLoopGroup 默认的构造方法,NioEventLoopGroup 会创建 CPU * 2 个线程(NioEventLoop)。


2.1.2. 单reactor-多线程模型

1
2
3
4
// 示例代码
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstra.group(eventGroup);

此处用默认构造方法创建 NioEventLoopGroup 即可,NioEventLoopGroup 默认构造方法中会创建 CPU * 2 个线程(NioEventLoop),同时 parentGroup 和 childGroup 同样使用同一个 NioEventLoopGroup。


2.1.3. 主从reactor-多线程模型

1
2
3
4
5
// 示例代码
EventLoopGroup parentNioEventLoopGroup = new NioEventLoopGroup();
EventLoopGroup childNioEventLoopGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstra.group(parentNioEventLoopGroup, childNioEventLoopGroup );

此处需要显式创建两个 NioEventLoopGroup 对象,parentNioEventLoopGroup 对照 主Reactor,childNioEventLoopGroup 对照 从Reactor


3. channel方法

Netty 中两种常用的 Channel 类型:

  • 一种是服务端用于监听绑定端口地址并负责与客户端建立连接的 NioServerSocketChannel, 是对 java.nio.channels.ServerSocketChannel 的封装, parentGroup 中的 Channel 就是 NioServerSocketChannel。
  • 一种是与客户端通信的 NioSocketChannel, 是对 java.nio.channels.SocketChannel 的封装, childGroup 中的 Channel 就是 NioSocketChannel。

每种 Channel 类型实例都会对应一个 PipeLine 用于编排对应 Channel 实例上的 I/O 事件处理逻辑,而 PipeLine 中组织的就是ChannelHandler,其用于编写特定的IO处理逻辑。


channel 方法继承自父类 AbstractBootstrap,channel 方法接收的是一个 Class 对象,用于实例化创建 channel 的工厂。

结合 示例代码(点击跳转) 这里接收的就是 NioServerSocketChannel 的 Class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.netty.bootstrap.AbstractBootstrap

public B channel(Class<? extends C> channelClass) {
return channelFactory(new ReflectiveChannelFactory<C>(
ObjectUtil.checkNotNull(channelClass, "channelClass")
));
}

public B channelFactory(io.netty.channel.ChannelFactory<? extends C> channelFactory) {
return channelFactory((ChannelFactory<C>) channelFactory);
}

public B channelFactory(ChannelFactory<? extends C> channelFactory) {
ObjectUtil.checkNotNull(channelFactory, "channelFactory");
if (this.channelFactory != null) {
throw new IllegalStateException("channelFactory set already");
}

this.channelFactory = channelFactory;
return self();
}

ReflectiveChannelFactory 这个类比较简单,接收到 NioServerSocketChannel 的 Class 后,调用 getConstructor 方法,然后将返回内容存储到 constructor 属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ReflectiveChannelFactory<T extends Channel> implements ChannelFactory<T> {

private final Constructor<? extends T> constructor;

public ReflectiveChannelFactory(Class<? extends T> clazz) {
ObjectUtil.checkNotNull(clazz, "clazz");
try {
this.constructor = clazz.getConstructor();
} catch (NoSuchMethodException e) {...}
}

@Override
public T newChannel() {
try {
return constructor.newInstance();
} catch (Throwable t) {...}
}

@Override
public String toString() {...}
}

java.lang.Class#getConstructor(Class<?>... parameterTypes) 方法用于获取 对应参数类型 的构造函数,再通过调用 java.lang.reflect.Constructor#newInstance(Object ... initargs) 方法时传入构造方法参数值,就可以构造一个实例。

此处并没有将 NioServerSocketChannel 进行实例化,后续 ServerBootstrap 调用 bind 方法时会通过调用 newChannel 方法创建 NioServerSocketChannel 对象。


4. option方法

option 方法来自父类 AbstractBootstrap,用于设置 NioServerSocketChannel 底层 java.nio.channels.ServerSocketChannel 的TCP参数。

这里 AbstractBootstrap 只是将这些配置值保存起来,后续初始化 Channel 时才会使用它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();

public <T> B option(ChannelOption<T> option, T value) {
ObjectUtil.checkNotNull(option, "option");
synchronized (options) {
if (value == null) {
options.remove(option);
} else {
options.put(option, value);
}
}
return self();
}

4.1. 常用的ChannelOption

  • SO_RCVBUF / SO_SNDBUF
    TCP参数。每个TCP socket(套接字)在内核中都有一个发送缓冲区和一个接收缓冲区,这两个选项就是用来设置TCP连接的这两个缓冲区大小的。TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的缓冲区及其填充的状态。

  • TCP_NODELAY
    TCP参数。表示是否立即发送数据,默认值为True(Netty默认为true,而操作系统默认为false)。该值用于设置是否关闭Nagle算法,该算法将小的碎片数据连接成更大的报文(或数据包)来最小化所发送报文的数量,如果需要发送一些较小的报文,则需要禁用该算法。Netty默认禁用该算法,从而最小化报文传输的延时。

  • SO_KEEPALIVE
    TCP参数。表示底层TCP协议的心跳机制。true为连接保持心跳,默认值为false。启用该功能时,TCP会主动探测空闲连接的有效性。可以将此功能视为TCP的心跳机制,需要注意的是:默认的心跳间隔是7200s,即2小时。Netty默认关闭该功能。

  • SO_REUSEADDR
    此为TCP参数。设置为true时表示地址复用,默认值为false。有四种情况需要用到这个参数设置:

    1. 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而我们希望启动的程序的socket2要占用该地址和端口。例如在重启服务且保持先前端口时;
    2. 有多块网卡或用IP Alias技术的机器在同一端口启动多个进程,但每个进程绑定的本地IP地址不能相同;
    3. 单个进程绑定相同的端口到多个socket(套接字)上,但每个socket绑定的IP地址不同;
    4. 完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

  • SO_LINGER
    此为TCP参数。表示关闭socket的延迟时间,默认值为-1,表示禁用该功能:

    • -1表示socket.close()方法立即返回,但操作系统底层会将发送缓冲区全部发送到对端;
    • 0表示socket.close()方法立即返回,操作系统放弃发送缓冲区的数据,直接向对端发送RST包,对端收到复位错误。
    • 正整数值表示调用socket.close()方法的线程被阻塞,直到延迟时间到来、发送缓冲区中的数据发送完毕,若超时,则对端会收到复位错误。

  • SO_BACKLOG
    此为TCP参数。表示服务器端接收连接的队列长度,如果队列已满,客户端连接将被拒绝。Linux中默认为128。如果连接建立频繁,服务器处理新连接较慢,可以适当调大这个参数。

  • SO_BROADCAST
    此为TCP参数。表示设置广播模式。


5. attr方法

attr 方法来自父类 AbstractBootstrap,用于配置 NioServerSocketChannel 的自定义属性,attr 方法配置的属性,后续可以在Pipineline中使用。

这里 AbstractBootstrap 只是将这些配置值保存起来,后续初始化 Channel 时才会使用它们。

1
2
3
4
5
6
7
8
9
10
11
private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();

public <T> B attr(AttributeKey<T> key, T value) {
ObjectUtil.checkNotNull(key, "key");
if (value == null) {
attrs.remove(key);
} else {
attrs.put(key, value);
}
return self();
}

6. childHandler方法

ServerBootstrap 启动类方法带有 child 前缀的均是设置 NioSocketChannel 属性的。

childHandler 方法是 ServerBootstrap 中的,用于设置 NioSocketChannel PipeLine 中的 ChannelHandler,即: 用来处理 已经连接客户端 Channel 的 I/O 读写事件,如编解码器就设置在此处。

注意:childHandler 添加的 ChannelHandler 是在 client 连接成功、触发IO事件后才开始执行的。

1
2
3
4
public ServerBootstrap childHandler(ChannelHandler childHandler) {
this.childHandler = ObjectUtil.checkNotNull(childHandler, "childHandler");
return this;
}

7. handler方法

handler 方法来自父类 AbstractBootstrap,用于设置 NioServerSocketChannel PipeLine 中的 ChannelHandler,如 示例代码(点击跳转) 中添加了 LoggingHandler。

1
2
3
4
public B handler(ChannelHandler handler) {
this.handler = ObjectUtil.checkNotNull(handler, "handler");
return self();
}

在服务端初始化 NioServerSocketChannel 过程中,会将 handler 添加到 NioServerSocketChannel 的 pipeline 中,源码位置: ServerBootstrap#init

在实际项目使用的过程中,并不会向 Netty 服务端 NioServerSocketChannel 添加额外的ChannelHandler,NioServerSocketChannel 只需专心做好最重要的本职工作 接收客户端连接 即可。示例代码中添加 LoggingHandler 只是为了展示 ServerBootstrap 的配置方法。


8. bind方法

bind 方法会校验相关参数配置,然后绑定端口;bind 方法内逻辑很多,后续再分析其流程逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
public ChannelFuture bind() {
// 检查、校验
validate();

// 重要配置不存在,快速中断
SocketAddress localAddress = this.localAddress;
if (localAddress == null) {
throw new IllegalStateException("localAddress not set");
}

// 绑定
return doBind(localAddress);
}

9. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
public class NettyEchoServer {
public static void main(String[] args) {
final AttributeKey<String> key = AttributeKey.valueOf(UUID.randomUUID().toString());
EventLoopGroup parentNioEventLoopGroup = new NioEventLoopGroup();
EventLoopGroup childNioEventLoopGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(parentNioEventLoopGroup, childNioEventLoopGroup)
.handler(new LoggingHandler(LogLevel.INFO))
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.attr(key, "value")
.localAddress(SERVER_PORT)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new NettyEchoServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind().sync();
log.info("Echo 服务端准备就绪...");
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {...} finally { ... }
}
}

如觉得示例代码不合胃口,可以食用官方源码中的Test:io.netty.bootstrap.ServerBootstrapTest#testHandlerRegister


10. Reference