引言
Netty自带了许多ChannelHandler来简化开发,又由于Netty采用责任链的设计模式,所以为了数据能在链内正确传递,了解每个handler的输入与输出就非常重要了;下面是一些常用的Handler的汇总及其讲解。
预设
HttpServerCodec
该类用于服务器解析HTTP请求,其内部封装了 HttpRequestDecoder 与 HttpReponseEncoder
HttpRequestDecoder:会将 ByteBuf 解析为 HttpRequest 以及 HttpContent
in: ByteBuf
out: HttpRequest and HttpContent
HttpReponseEncoder:
in: HttpResponse and HttpContent
out: ByteBuf
HttpClientCodec
该类在内部封装了,HttpRequestEncoder 与 HttpReponseDecoder, 效果与 HttpServerCodec 相反,主要用于客户端。其类图与 HttpClientCodec 相同
HttpObjectAggregator
将多个消息聚合成一个完整的 HttpRequest 或 HttpResponse,解决消息分片问题。这是因为 HTTP 消息可能会被分割成多个chunked在网络上传输,每个chunker只包含消息的一部分,该类的任务就是把这些片段重新组合起来,以便于后续的处理器可以处理完整的 HTTP 消息。
// 输入时
// in: HttpMessage(HttpRequest的父类), HttpContent<br>
// out: FullHttpRequest<br>
// 输出时
// in: HttpMessage(HttpRequest的父类), HttpContent<br>
// out: FullHttpResponse<br>
ch.pipeline()
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(1024 * 1024))
FixedLengthFrameDecoder
该类可在netty层面处理TCP粘包与拆包。FixedLengthFrameDecoder 是固定长度解码器,能够对固定长度的消息进行自动解码,利用 FixedLengthFrameDecoder,无论多少数据,都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包到达后进行拼包,直到读取到一个完整的包。
// client
private void start0(EventLoopGroup workerGroup) throws InterruptedException {
// 创建Bootstrap实例
Bootstrap sb = new Bootstrap();
sb.group(workerGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 添加处理器
ch.pipeline()
.addLast(new MessageToByteEncoder<String>() {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
out.writeBytes(msg.getBytes());
}
});
}
});
// 绑定端口,同步等待成功
ChannelFuture f = sb.connect("127.0.0.1", port).sync();
log.info("ClientBootstrap connect success on port:{}", port);
Channel channel = f.channel();
Scanner scanner = new Scanner(System.in);
f.channel().closeFuture().addListener(future -> {
log.info("ClientBootstrap close...");
});
// 手动模拟发消息
while (true) {
String line = scanner.nextLine();
if ("exit".equals(line)) {
workerGroup.shutdownGracefully();
}
// line += "\r\n";
channel.writeAndFlush(line);
}
}
// server
private void start0(EventLoopGroup bossGroup, EventLoopGroup workerGroup) throws InterruptedException {
// 创建ServerBootstrap实例
ServerBootstrap sb = new ServerBootstrap();
sb.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 添加处理器
ch.pipeline()
.addLast(new FixedLengthFrameDecoder(3))
.addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
System.out.println("receive: " + new String(bytes));
}
});
}
});
// 绑定端口,同步等待成功
ChannelFuture f = sb.bind(port).sync();
log.info("ServiceBootstrap start success on port:{}", port);
f.channel().closeFuture().addListener((ChannelFutureListener) future -> {
log.info("ServiceBootstrap close...");
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
});
}
可以看到,该解码器会按照长度将字节进行切分,只有接收到足够长度的字节,才向下传递数据
// hello
// receive: hel
// world
// receive: low
// receive: orl
// !!
// receive: d!!
优点:使用简单
缺点:
- 不够灵活,只适合固定长度的协议;
- 非固定长度的协议单条消息需要严格控制长度
- 不足指定长度的消息需要补充空字符
LineBasedFramedDecoder
该类可在netty层面处理TCP粘包与拆包。按行进行解析,依次遍历 ByeBuf 中可读字节,判断是否有\n,\r\n,如果有,就当前位置为结束位置,从可读索引到结束位置区间的字节就组装成一行(frame),以换行符为结束标志的解码器,同识支持最大长度。
123
receive: 123
hello
receive: hello
!
receive: !
GET /index.html
receive: GET /index.html
优点:使用简单
缺点:
- 协议结尾必须为指定字符
- 多见于文本型协议(例如:http协议),如果数据中包含
\n字符,可能会导致误判协议结尾。
DelimiterBasedFrameDecoder
该类可在 netty 层面处理TCP粘包与拆包。自定义结尾符的行解析器,如果设置\n,\r\n为结尾,则退化为 LineBasedFramedDecoder,适用于存在分隔符的文本型协议。
LengthFieldBasedFrameDecoder
该解码器用于处理带有长度字段的协议,最简单的处理的协议为
0 4 4+length
+--------+-----------+
| length | ..... |
+--------+-----------+
pipeline.addLast(new LengthFieldBasedFrameDecoder(
1024, // 最大帧长度
0, // 长度字段的偏移量(0就是从消息开始)
4, // 长度字段的字节数
0, // 长度调整值,如果长度字段只包含消息体的长度则取0
4 // 要跳过的开头的字节数
));
相对的更灵活,适用于绝大多数协议,许多具有定长头部,变长负载,且头部存在指定字段指示负载长度的协议都可以使用此类去解析。更多例子可以查看源码上的注释;
长度调节值比较难理解,首先要明确下面两点
- 此类会认为长度字段后即为负载。
- 此类在读取完长度字段后,会再去读取长度字段所指定的数据长度。
由以上两点可知,如果
- 长度字段后为负载 且 长度字段只包含负载长度;则数值选择为0
- 长度字段后为负载 且 长度字段为头+负载长度;则数值需要减去头的长度
- 长度字段后不为负载 且 长度字段只包含负载长度;则数值需要加上长度字段后负载前的长度
- 长度字段后不为负载 且 长度字段头+负载长度;则数值需要先减去头的长度再加上长度字段后负载前的长度(或减去从0到长度字段结尾的长度)
StringEncoder
将字符流转化为字节流,将String转化为Bytebuf
StringDecoder
将字节流转化为字符流,将Bytebuf转化为String,通常搭配LineBasedFrameDecoder使用
需要自己实现或继承
ByteToMessageDecoder
可自己继承ByteToMessageDecoder将ByteBuf解析为任意 Object,不需要显式使用ctx.fireChannelRead(msg)向后传递;用于自定义解码器
new ByteToMessageDecoder() {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int length = 0;
if (in.readableBytes() < 4) {
return;
}
length = in.readInt();
if (in.readableBytes() < length) {
in.resetReaderIndex(); // 重置到mark的位置,默认原始位置
return;
}
byte[] content = new byte[length];
in.readBytes(content);
log.info("receive content:{}", new String(content));
}
}
MessageToByteEncoder<O>
用于将对应 Object 转化为 ByteBuf,以用于将数据传输到互联网,不需要显式使用ctx.write()向前传递
MessageToMessageEncoder<I>
将一种Object转化为另一种Object
MessageToMessageDecoder<I>
和上面作用一样,不过上面是输出,这边是输入
ReplayingDecoder<S>
ReplayingDecoder继承自ByteToMessageDecoder同样用于解码数据
@Slf4j
public class CustomReplayingDecoder extends ReplayingDecoder<CustomReplayingDecoder.State> {
// 定义解码状态
public enum State { READ_LENGTH, READ_DATA }
public CustomReplayingDecoder() {
super(State.READ_LENGTH); // 初始状态为 READ_LENGTH
}
int length;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
State state = state();
switch (state) {
case READ_LENGTH:
length = in.readInt(); // 读取4个字节作为消息长度
log.info("length: {}", length);
checkpoint(State.READ_DATA); // 切换到 READ_DATA 状态
break;
case READ_DATA: // 读取消息内容
byte[] bytes = new byte[length];
in.readBytes(bytes);
String message = new String(bytes, StandardCharsets.UTF_8);
log.info("message: {}", message);
// 重置状态为 READ_LENGTH,准备处理下一个消息
checkpoint(State.READ_LENGTH);
break;
default:
throw new Error("Shouldn't reach here.");
}
}
}
在协议比较复杂时,可以将协议解析划分为多个步骤,例如解析头部状态和解析负载状态
ReplayingDecoderByteToMessageDecoder异同
相同点是两种都不需要显式传递数据
不同点
- 前者遇到数据长度不足的情况,无需手动判断,可自动回滚指针,并且使用checkpoint()状态管理,回滚到指定位置
- 后者遇到数据长度不足的情况,需手动判断并手动回滚指针,否则报错,且无状态管理机制
ChannelInboundHandlerAdapter
基础的处理输入的Handler,在继承此类时,要手动ctx.fireChannelRead(msg);
SimpleChannelInboundHandler
是对ChannelInboundHandlerAdapter的一个简化,无需手动传递数据;如果此类可以处理此条数据,则去处理并自动回收空间;否则自动传递数据。
ChannelOutboundHandlerAdapter
基础的处理输出的Handler,在继承此类时,要手动ctx.write(msg, promise);
附录
协议层面处理TCP拆包/粘包
- 开发协议时,固定字段长度(例:TCP header)
- 使用固定字符表示结束(例:Http header)
- 使用长度字段告知数据长度(例:payload)