整理与讲解Netty常用ChannelHandler(1)-http与基本类

439 阅读7分钟

引言

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

image.png

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))

image.png

FixedLengthFrameDecoder

该类可在netty层面处理TCP粘包与拆包。FixedLengthFrameDecoder 是固定长度解码器,能够对固定长度的消息进行自动解码,利用 FixedLengthFrameDecoder,无论多少数据,都会按照构造函数中设置的固定长度进行解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包到达后进行拼包,直到读取到一个完整的包。

image.png

image.png

// 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),以换行符为结束标志的解码器,同识支持最大长度。

image.png

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  // 要跳过的开头的字节数
));

相对的更灵活,适用于绝大多数协议,许多具有定长头部,变长负载,且头部存在指定字段指示负载长度的协议都可以使用此类去解析。更多例子可以查看源码上的注释;

长度调节值比较难理解,首先要明确下面两点

  • 此类会认为长度字段后即为负载。
  • 此类在读取完长度字段后,会再去读取长度字段所指定的数据长度。

由以上两点可知,如果

  1. 长度字段后为负载 且 长度字段只包含负载长度;则数值选择为0
  2. 长度字段后为负载 且 长度字段为头+负载长度;则数值需要减去头的长度
  3. 长度字段后不为负载 且 长度字段只包含负载长度;则数值需要加上长度字段后负载前的长度
  4. 长度字段后不为负载 且 长度字段头+负载长度;则数值需要先减去头的长度再加上长度字段后负载前的长度(或减去从0到长度字段结尾的长度)

StringEncoder

将字符流转化为字节流,将String转化为Bytebuf

StringDecoder

将字节流转化为字符流,将Bytebuf转化为String,通常搭配LineBasedFrameDecoder使用

需要自己实现或继承

ByteToMessageDecoder

可自己继承ByteToMessageDecoder将ByteBuf解析为任意 Object,不需要显式使用ctx.fireChannelRead(msg)向后传递;用于自定义解码器

image.png

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()向前传递

image.png

MessageToMessageEncoder<I>

将一种Object转化为另一种Object

image.png

MessageToMessageDecoder<I>

和上面作用一样,不过上面是输出,这边是输入

image.png

ReplayingDecoder<S>

ReplayingDecoder继承自ByteToMessageDecoder同样用于解码数据

image.png

@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.");
        }
    }
}

在协议比较复杂时,可以将协议解析划分为多个步骤,例如解析头部状态和解析负载状态

ReplayingDecoder ByteToMessageDecoder 异同
相同点是两种都不需要显式传递数据
不同点

  1. 前者遇到数据长度不足的情况,无需手动判断,可自动回滚指针,并且使用checkpoint()状态管理,回滚到指定位置
  2. 后者遇到数据长度不足的情况,需手动判断并手动回滚指针,否则报错,且无状态管理机制

ChannelInboundHandlerAdapter

基础的处理输入的Handler,在继承此类时,要手动ctx.fireChannelRead(msg);

SimpleChannelInboundHandler

是对ChannelInboundHandlerAdapter的一个简化,无需手动传递数据;如果此类可以处理此条数据,则去处理并自动回收空间;否则自动传递数据。

image.png

image.png

ChannelOutboundHandlerAdapter

基础的处理输出的Handler,在继承此类时,要手动ctx.write(msg, promise);

image.png

附录

协议层面处理TCP拆包/粘包

  1. 开发协议时,固定字段长度(例:TCP header)
  2. 使用固定字符表示结束(例:Http header)
  3. 使用长度字段告知数据长度(例:payload)