本篇文章将带你一起学习如何配置一个Netty的服务端和客户端的简单程序,并了解其部分配置属性
Netty 服务端简单案例
要启动一个Netty服务端,必须要指定三类属性,分别是线程模型、IO 模型、连接读写处理逻辑,有了这三者,之后在调用bind(8000)
,我们就可以在本地绑定一个 8000 端口启动起来
分类 | Bootstrap | ServerBootstrap |
---|---|---|
网络功能 | 连接到远程主机和端口 | 绑定本地端口 |
需要注意的是:服务端启动的引导类是ServerBootstrap
java
public class NettyServer {
private final int port = 8090; // 服务端监听的端口号
/**
* 启动Netty服务端
*/
public void start() throws Exception {
// 创建EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建ServerBootstrap并配置Channel
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
// 添加自定义的ChannelHandler
ch.pipeline().addLast("decoder", new StringDecoder());
ch.pipeline().addLast("encoder", new StringEncoder());
ch.pipeline().addLast(new NettyServerHandler());
}
});
// 绑定端口,并启动Netty服务端
ChannelFuture f = b.bind(port).sync();
System.out.println("Netty server started on port " + port);
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 关闭EventLoopGroup
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
Netty服务端的启动过程
Netty 服务端的启动过程大致分为三个步骤:
- 配置线程池;
- Channel 初始化(IO 模型);
- 端口绑定;
配置线程池
单线程模式
Reactor 单线程模型所有 I/O 操作都由一个线程完成,所以只需要启动一个 EventLoopGroup 即可。
java
EventLoopGroup group = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group)
多线程模式
Reactor 单线程模型有非常严重的性能瓶颈,因此 Reactor 多线程模型出现了。在 Netty 中使用 Reactor 多线程模型与单线程模型非常相似,区别是 NioEventLoopGroup 可以不需要任何参数,它默认会启动 2 倍 CPU 核数的线程。当然,你也可以自己手动设置固定的线程数。
java
EventLoopGroup group = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(group)
主从多线程模式
在大多数场景下,我们采用的都是主从多线程 Reactor 模型。Boss 是主 Reactor,Worker 是从 Reactor。它们分别使用不同的 NioEventLoopGroup,主 Reactor 负责处理 Accept,然后把 Channel 注册到从 Reactor 上,从 Reactor 主要负责 Channel 生命周期内的所有 I/O 事件。
java
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
Channel 初始化
我们通过.channel(NioServerSocketChannel.class)
来指定 IO 模型,推荐 Netty 服务端采用NioServerSocketChannel
作为Channel的类型,客户端采用 NioSocketChannel
。
注册channelHandler
在 Netty 中可以通过 ChannelPipeline 去注册多个 ChannelHandler,使用childHandler方法
java
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(xxxx)
}
})
ChannelInitializer 是实现了 ChannelHandler接口的匿名类
自动绑定递增端口
serverBootstrap.bind(8000)
这个方法呢,它是一个异步的方法,调用之后是立即返回的,他的返回值是一个ChannelFuture
,我们可以给这个ChannelFuture
添加一个监听器GenericFutureListener
- 我们还可以通过
sync()
方法,不过它会阻塞,直至整个启动过程完成
java
private static void bind(final ServerBootstrap serverBootstrap, final int port) {
serverBootstrap.bind(port).addListener(new GenericFutureListener<Future<? super Void>>() {
public void operationComplete(Future<? super Void> future) {
if (future.isSuccess()) {
System.out.println("端口[" + port + "]绑定成功!");
} else {
System.err.println("端口[" + port + "]绑定失败!");
bind(serverBootstrap, port + 1);
}
}
});
}
// 当然我们也可以阻塞绑定
ChannelFuture f = serverBootstrap.bind().sync();
ServerBootstrap服务端启动其他方法
带child的是针对连接的,不带的是针对服务器的
handler() 方法
java
serverBootstrap.handler(new ChannelInitializer<NioServerSocketChannel>() {
protected void initChannel(NioServerSocketChannel ch) {
System.out.println("服务端启动中");
}
})
childHandler()
用于指定处理新连接数据的读写处理逻辑,handler()
用于指定在服务端启动过程中的一些逻辑
attr() 方法
java
serverBootstrap.attr(AttributeKey.newInstance("serverName"), "nettyServer")
attr()
方法可以给服务端的channel,也就是NioServerSocketChannel
指定一些自定义属性,然后我们可以通过channel.attr()
取出这个属性。
比如,上面的代码我们指定我们服务端channel
的一个serverName
属性,属性值为nettyServer
,其实说白了就是给NioServerSocketChannel
维护一个map而已。
childAttr() 方法
java
serverBootstrap.childAttr(AttributeKey.newInstance("clientKey"), "clientValue")
上面的childAttr
可以给每一条连接指定自定义属性,然后后续我们可以通过channel.attr()
取出该属性。
客户端启动案例
创建一个引导类,然后给他指定线程模型,IO 模型,连接读写处理逻辑,连接上特定主机和端口,客户端就启动起来了。
需要注意的是:客户端启动的引导类是Bootstrap
java
public class NettyClient {
public static void main(String[] args) {
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap
// 1.指定线程模型
.group(workerGroup)
// 2.指定 IO 类型为 NIO
.channel(NioSocketChannel.class)
// 3.IO 处理逻辑
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
}
});
// 4.建立连接
bootstrap.connect("juejin.cn", 80).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else {
System.err.println("连接失败!");
}
});
}
}
失败重连
在网络情况差的情况下,客户端第一次连接可能会连接失败,这个时候我们可能会尝试重新连接,我们把建立连接的逻辑先抽取出来,然后在重连失败的时候,递归调用自身。
通常情况下,连接建立失败不会立即重新连接,而是会通过一个指数退避的方式,比如每隔 1 秒、2 秒、4 秒、8 秒,以 2 的幂次来建立连接,然后到达一定次数之后就放弃连接。
java
connect(bootstrap, "juejin.cn", 80, MAX_RETRY);
private static void connect(Bootstrap bootstrap, String host, int port, int retry) {
bootstrap.connect(host, port).addListener(future -> {
if (future.isSuccess()) {
System.out.println("连接成功!");
} else if (retry == 0) {
System.err.println("重试次数已用完,放弃连接!");
} else {
// 第几次重连
int order = (MAX_RETRY - retry) + 1;
// 本次重连的间隔
int delay = 1 << order;
System.err.println(new Date() + ": 连接失败,第" + order + "次重连......");
bootstrap.config().group().schedule(() -> connect(bootstrap, host, port, retry - 1), delay, TimeUnit
.SECONDS);
}
});
}
在上面的代码中,我们看到,我们定时任务是调用bootstrap.config().group().schedule()
, 其中bootstrap.config()
这个方法返回的是 BootstrapConfig,他是对Bootstrap配置参数的抽象,然后bootstrap.config().group()
返回的就是我们在一开始的时候配置的线程模型 workerGroup,调workerGroup的schedule
方法即可实现定时任务逻辑。
Bootstrap客户端启动其他方法
handler() 方法
客户端相关的数据读写逻辑是通过handler()
方法指定
attr() 方法
这个跟服务端的attr方法是一样的,只不过它是给客户端的channel配置属性
ChannelOption 参数
参数 | 含义 |
---|---|
SO_KEEPALIVE | 设置为 true 代表启用了 TCP SO_KEEPALIVE 属性,TCP 会主动探测连接状态,即连接保活 |
SO_BACKLOG | 已完成三次握手的请求队列最大长度,同一时刻服务端可能会处理多个连接,在高并发海量连接的场景下,该参数应适当调大 |
TCP_NODELAY | Netty 默认是 true,表示立即发送数据。如果设置为 false 表示启用 Nagle 算法,该算法会将 TCP 网络数据包累积到一定量才会发送,虽然可以减少报文发送的数量,但是会造成一定的数据延迟。Netty 为了最小化数据传输的延迟,默认禁用了 Nagle 算法 |
SO_SNDBUF | TCP 数据发送缓冲区大小 |
SO_RCVBUF | TCP数据接收缓冲区大小,TCP数据接收缓冲区大小 |
SO_LINGER | 设置延迟关闭的时间,等待缓冲区中的数据发送完成 |
CONNECT_TIMEOUT_MILLIS | 建立连接的超时时间 |
WRITE_BUFFER_HIGH_WATER_MARK 和 WRITE_BUFFER_LOW_WATER_MARK | 表示发送缓冲区的高水位线和低水位线。当发送缓冲区的字节数超过高水位线时,Channel的isWritable()方法将返回false;当发送缓冲区的字节数低于低水位线时,Channel的isWritable()方法将返回true。默认值为64KB和32KB。 |
ALLOCATOR | 表示使用的ByteBuf内存分配器。Netty提供了多种内存分配器,如UnpooledByteBufAllocator和PooledByteBufAllocator。默认值为PooledByteBufAllocator。 |
这些选项可以通过ServerBootstrap
和Bootstrap
类的option()
方法和childOption()
方法进行设置。
其中,option()
方法用于设置服务端Channel
,而childOption()
方法用于设置接受客户端连接的子Channel,即SocketChannel
。
childOption和option的区别
在网络编程中,服务端和客户端是相对的概念。服务端通常是一个主动等待客户端连接的程序,而客户端通常是一个主动发起连接的程序。
在Netty中,ServerSocketChannel
是服务端的 Channel
,它用于监听端口并接受客户端的连接请求 。当客户端发起连接请求时,ServerSocketChannel
会创建一个新的SocketChannel
,并将其交给业务逻辑处理。这个SocketChannel
就是用于收客户端连接的子 Channel
。
简单来说,服务端Channel
的作用是监听端口并接受客户端连接请求,而子Channel的作用是处理客户端的具体数据传输。
因为服务端 Channel
和 SocketChannel
有不同的行为,所以需要分别设置它们的 ChannelOption
,使用 option()
和 childOption()
方法来进行区分设置。
childOption() 方法
java
serverBootstrap
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
childOption()
可以给每条连接设置一些TCP底层相关的属性,比如上面,我们设置了两种TCP属性,其中
ChannelOption.SO_KEEPALIVE
表示是否开启TCP底层心跳机制,true为开启ChannelOption.TCP_NODELAY
表示是否开启Nagle算法,true表示关闭,false表示开启
-
- 通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。
option() 方法
除了给每个连接设置这一系列属性之外,我们还可以给服务端channel设置一些属性,最常见的就是so_backlog
java
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
ChannelOption.SO_BACKLOG
表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数ChannelOption.CONNECT_TIMEOUT_MILLIS
表示连接的超时时间,超过这个时间还是建立不上的话则代表连接失败
总结
通过上述内容,我们可以知道 Netty 服务端和客户端的启动过程可以分为三个主要部分:线程模型的配置、IO模型的选择(NioServerSocketChannel或NioSocketChannel)、以及连接读写处理逻辑。
服务端使用ServerBootstrap引导类,客户端则使用Bootstrap引导类。对每条连接,可以设置TCP底层相关属性如心跳机制和Nagle算法。
同时,还可以给服务端Channel设置属性,如连接队列长度和连接超时时间。此外,Netty还提供了ChannelPipeline来注册多个ChannelHandler,加强数据处理逻辑。