Netty实践
1 Netty 版本选择
Netty 请选择使用4.x版本,不要用5.x ,5.x已经废弃
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.113.Final</version>
</dependency>
2 Netty 模版代码
2.1 Server
new ServerBootstrap()
//.group(new NioEventLoopGroup())
.group(new NioEventLoopGroup(1), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("ChannelInboundHandler1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//流转到下一个InHandler
// super.channelRead(ctx, msg) 也是调用ctx.fireChannelRead(msg)
ctx.fireChannelRead(msg);
}
});
}
}).bind(5989).sync();
}
- ServerBootstrap, Server服务启动器
- group, 创建 NioEventLoopGroup,可以简单理解为 `线程池 + Selector,如果两个参数,第一个为parentGroup 可以理解为监听客户端的连接事件,第二个childGroup,负责处理客户端NioSocketChannel 的读写。
- channel, 选择 Scoket 实现类,其中 NioServerSocketChannel 表示基于 NIO 的服务器端ServerSocketChannel的实现。
- childHandler,是接下来添加的处理器都是给 SocketChannel 用的,而不是给 ServerSocketChannel。ChannelInitializer 处理器(每个客户端接入时 执行且只执行一次),它的作用是待客户端 SocketChannel 建立连接后,执行 initChannel 以便添加更多的处理器。
- ch.pipeline().addLast,添加消息处理器,入站的消息 按Handler在pipeline的里的顺序从头到尾依次处理。出站的消息从尾(也可能是当前Handler)到头依次处理
- bind ServerSocketChannel 绑定的监听端口
2.2 Client
Channel channel = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
}).connect("127.0.0.1", 8080)
.sync()
.channel();
- group 创建 NioEventLoopGroup
- channel, 选择 Scoket 实现类,NioSocketChannel 表示基于 NIO 的客户端实现
- handler 添加 SocketChannel 的处理器,ChannelInitializer 处理器(仅执行一次),它的作用是待客户端 SocketChannel 建立连接后,执行 initChannel 以便添加更多的处理器
- ch.pipeline().addLast,添加消息处理器,入站消息 按Handler在pipeline的里的顺序从头到尾依次处理。出站的消息从尾(也可能是当前Handler)到头依次处理
- connect 指定要连接的服务器和端口,connect 是异步的,方法返回一个ChannelFuture,
- 调用ChannelFuture 的 sync 方法同步等待 connect 建立连接完毕
- 调用ChannelFuture 的addListener 方法 ,连接完成执行异步回调
- sync Netty 中很多方法都是异步的,如 connect,这时需要使用 sync 方法等待 connect 建立连接完毕
- 获取 channel 对象,它即为通道抽象,可以进行数据读写操作
3 组件
3.1 EventLoop、EventLoopGroup
3.1.1 EventLoop
EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 io 事件。
继承关系
- 继承自 j.u.c.ScheduledExecutorService 因此包含了线程池中所有的方法
- 另一条线是继承自 netty 自己的 OrderedEventExecutor,
- 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
- 提供了 parent 方法来看看自己属于哪个 EventLoopGroup
3.1.2 EventLoopGroup
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)
继承关系
- 继承自 netty 自己的 EventExecutorGroup ,next 方法获取集合中下一个 EventLoop
- 实现了 Iterable 接口提供遍历 EventLoop 的能力
3.2 Channel
channel 的主要方法
- close() 可以用来关闭 channel
- closeFuture() 用来处理 channel 的关闭
- sync 方法作用是同步等待 channel 关闭
- addListener 方法是 channel 关闭后执行回调
- pipeline() 方法添加处理器
- write() 方法将数据写入
- writeAndFlush() 方法将数据写入并刷出
3.2.1 ChannelFuture
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8080); // 1
channelFuture.sync().channel().writeAndFlush(new Date() + ": hello world!");
1 处返回的是 ChannelFuture 对象
注意 connect 方法是异步的,意味着不等连接建立,方法执行就返回了。因此 channelFuture 对象中不能【立刻】获得到正确的 Channel 对象
ChannelFuture 的两个重要方法
- channelFuture.sync() ,同步等待连接完成
- channelFuture.addListener 连接建立后异步回调
channelFuture.addListener((ChannelFutureListener) future -> {
System.out.println(future.channel());
});
3.2.2 CloseFuture
// 获取 CloseFuture 对象, 1) 同步处理关闭, 2) 异步处理关闭
ChannelFuture closeFuture = channel.closeFuture();
//1) 同步处理关闭
closeFuture.sync();
// 2) 异步处理关闭
closeFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
log.debug("处理关闭之后的操作");
//group.shutdownGracefully();
}
});
3.3 ChannelHandler
ChannelHandler 用来处理 Channel 上的各种事件,分为入站、出站两种。所有 ChannelHandler 被连成一串,就是 Pipeline
- 入站处理器通常是 ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据,写回结果
- 出站处理器通常是 ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
Handler的执行顺序
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(1);
ctx.fireChannelRead(msg); // 1
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(2);
ctx.fireChannelRead(msg); // 2
}
});
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(3);
ctx.channel().write(msg); // 3
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(4);
ctx.write(msg, promise); // 4
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(5);
ctx.write(msg, promise); // 5
}
});
ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg,
ChannelPromise promise) {
System.out.println(6);
ctx.write(msg, promise); // 6
}
});
}
})
.bind(8080);
服务器端打印:
1
2
3
6
5
4
可以看到,ChannelInboundHandlerAdapter 是按照 addLast 的顺序执行的,而 ChannelOutboundHandlerAdapter 是按照 addLast 的逆序执行的。ChannelPipeline 的实现是一个 ChannelHandlerContext(包装了 ChannelHandler) 组成的双向链表
- 入站处理器中,ctx.fireChannelRead(msg) 是 调用下一个入站处理器
- 如果注释掉 1 处代码,则仅会打印 1
- 如果注释掉 2 处代码,则仅会打印 1 2
- 3 处的 ctx.channel().write(msg) 会 从尾部开始触发 后续出站处理器的执行
- 如果注释掉 3 处代码,则仅会打印 1 2 3
- 类似的,出站处理器中,ctx.write(msg, promise) 的调用也会 触发上一个出站处理器
- 如果注释掉 6 处代码,则仅会打印 1 2 3 6
- ctx.channel().write(msg) vs ctx.write(msg)
- 都是触发出站处理器的执行
- ctx.channel().write(msg) 从尾部开始查找出站处理器
- ctx.write(msg) 是从当前节点找上一个出站处理器
- 3 处的 ctx.channel().write(msg) 如果改为 ctx.write(msg) 仅会打印 1 2 3,因为节点3 之前没有其它出站处理器了
- 6 处的 ctx.write(msg, promise) 如果改为 ctx.channel().write(msg) 会打印 1 2 3 6 6 6… 因为 ctx.channel().write() 是从尾部开始查找,结果又是节点6 自己
- 服务端 pipeline 触发的原始流程,图中数字代表了处理步骤的先后次序
3.2.1 常用的 ChannelInboundHandlerAdapter
3.2.1.1 LineBasedFrameDecoder
以 换行分隔符( \n 或 \r\n )为结尾进行分割
- 构造函数
/**
* @param maxLength 解码帧的最大长度。如果帧长度超过此值,则抛出异常
*/
public LineBasedFrameDecoder(final int maxLength) {
this(maxLength, true, false);
}
//更多构造函数见源码
-
输入 ByteBuf
-
输出 ByteBuf ,每遇到换行符 就将换行符之前的数据 单独调用ctx.fireChannelRead 传递给下一个ChannelHandler,多行数据会调用多次ctx.fireChannelRead
-
使用方法
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
3.2.1.2 DelimiterBasedFrameDecoder
以 指定的分隔符 为结尾进行分割
- 构造函数
/**
* @param maxFrameLength 解码帧的最大长度。如果帧长度超过此值,则抛出异常
* @param delimiter 分隔符
*/
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
this(maxFrameLength, true, delimiter);
}
- 输入 ByteBuf
- 输出 ByteBuf, 每遇到指定的分隔符 就将分隔符之前的数据 单独调用ctx.fireChannelRead 传递给下一个ChannelHandler,多条数据会调用多次ctx.fireChannelRead
- 使用方法
//以 F0F1 为分隔符
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, ByteBufAllocator.DEFAULT.buffer().writeBytes("F0F1".getBytes(StandardCharsets.UTF_8))));
3.2.1.3 LengthFieldBasedFrameDecoder
是一个根据在接收到的 ByteBuf 消息中 长度值 对这个消息动态的切分的解码器。在需要解码二进制消息并且在消息头中包含代表这条消息的整体长度或消息体长度的字段的情况下非常有用。
- 配置参数
类拥有许多配置参数,所以它可以用来解析任何包含长度字段的消息,包含长度字段的消息在自定义的客户端-服务端协议之间非常常见。常用的配置参数有:-
ByteOrder byteOrder : 这个二进制消息是大端模式[默认]/小端模式,如果和实际数据不同,length 字段数值解
-