Netty 系列(3) — Netty 处理TCP 粘包/拆包

2,106 阅读10分钟

TCP 粘包/拆包

问题分析

TCP 是个“流”协议,所谓流,就是没有界限的一串数据。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分。所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的 TCP 粘包和拆包问题。

image.png

例如在上一篇文章的Demo程序中,客户端向服务端发送了两条数据,服务端也向客户端响应了两条数据。

// 服务端处理器
static class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        // 读数据并处理请求
        ByteBuf reqBuf = (ByteBuf) msg;
        byte[] reqBytes = new byte[reqBuf.readableBytes()];
        reqBuf.readBytes(reqBytes);
        System.out.println("Request data: " + new String(reqBytes, StandardCharsets.UTF_8));

        // 响应客户端请求
        System.out.println("channel writing...");
        ctx.channel().write(Unpooled.copiedBuffer("Hello Netty Client!", StandardCharsets.UTF_8));
        ctx.channel().write(Unpooled.copiedBuffer("Hello World!!!", StandardCharsets.UTF_8));
        ctx.channel().flush();
    }
}

// 客户端处理器
static class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel active...");

        ctx.channel().write(Unpooled.copiedBuffer("Hello Netty Server!", StandardCharsets.UTF_8));
        ctx.channel().write(Unpooled.copiedBuffer("Hello World!!!", StandardCharsets.UTF_8));
        ctx.channel().flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");
        ByteBuf resBuf = (ByteBuf) msg;
        byte[] resBytes = new byte[resBuf.readableBytes()];
        resBuf.readBytes(resBytes);
        System.out.println("Response data: " + new String(resBytes, StandardCharsets.UTF_8));

        // 关闭通道
        ctx.channel().close();
    }
}

从输出结果来看,客户端和服务端接收到的数据输出只有一行,说明两条数据包粘在一起了,这样我们就没法区分一个独立完整的数据。

// 服务端
channel read...
Request data: Hello Netty Server!Hello World!!!

// 客户端
channel read...
Response data: Hello Netty Client!Hello World!!!

发生原因

发生 TCP 粘包/拆包的常见原因有如下几点:

  • 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。

  • 待发送数据大于 MSS(最大报文长度),TCP在传输前将根据 MSS 大小进行拆包分段发送。

  • 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。

  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

解决方案

由于底层的 TCP 无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。

要解决粘包和拆包的问题,关键点在于读取方需要知道一个完整的数据包,它是如何开始,如何结束的。根据业界主流协议的解决方案,可以归纳如下。

  • 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;

  • 在包尾增加回车换行符作为结束标志,例如FTP协议,这种方式在文本协议中应用比较广泛;

  • 将消息分为消息头和消息体,消息头中包含表示消息总长度的字段,通常消息头的第一个字段使用 int32 来表示消息的总长度;

  • 使用更复杂的应用层协议。

例如下面的程序,基于在消息头添加一个固定的 int32 来表示消息的总长度的方案:

  • 在写入数据时,首先写入消息的字节长度,再写入完整的数据。
  • 在读取时,则先读取一个 int 值,这个值就表示消息的长度,然后再读取对应长度的字节就是一条完整的消息。
  • 如果发生拆包,那字节长度就会小于头部指定的 int 值,此时就跳过读取,等待后续数据接收完整再读。
  • 如果发生粘包,就需要多次解码,直到数据读完或数据长度小于 int 值。
public class RequestUtil {
    /**
     * 写入通道
     */
    public static void writeAndFlush(String data, ChannelHandlerContext ctx) {
        ByteBuf buf = Unpooled.buffer();
        byte[] bytes = data.getBytes(StandardCharsets.UTF_8);
        // 先写入数据的长度
        buf.writeInt(bytes.length);
        // 再写入完整的数据
        buf.writeBytes(bytes);
        // 写入通道
        ctx.channel().writeAndFlush(buf);
    }

    /**
     * 对消息解码,可能有多块数据粘包,所以解码每块消息后放入 out 集合中
     */
    public static void decode(Object msg, List<String> out) {
        ByteBuf buf = (ByteBuf) msg;

        // 校验消息长度,必须达到4个字节
        if (buf.readableBytes() < 4) {
            return;
        }

        // 标记当前可读位置,便于后面重新读取
        buf.markReaderIndex();

        // 先读取4个字节的int,代表消息的bytes长度
        int len = buf.readInt();
        // 检查是否有拆包,小于数据长度,说明数据不完整,等待后续的数据进来
        if (buf.readableBytes() < len) {
            // 还原读索引
            buf.resetReaderIndex();
            return;
        }
        // 读取指定长度的字节,多余的不读取,避免粘包问题
        byte[] bytes = new byte[len];
        buf.readBytes(bytes);

        out.add(new String(bytes, StandardCharsets.UTF_8));

        // 如果还有数据就继续读
        if (buf.isReadable()) {
            decode(msg, out);
        }
    }
}

服务端、客户端读取和写入改造:

// 服务端
static class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        // 读数据并处理请求
        List<String> dataList = new ArrayList<>();
        RequestUtil.decode(msg, dataList);
        // 处理数据
        for (String data : dataList) {
            handleMsg(ctx, data);
        }
    }

    private void handleMsg(ChannelHandlerContext ctx, String msg) {
        System.out.println("Request data: " + msg);
        // 响应客户端请求
        System.out.println("channel writing...");
        RequestUtil.writeAndFlush("Hello Netty Client!", ctx);
        RequestUtil.writeAndFlush("Hello World!!!", ctx);
    }
}

// 客户端
static class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel active...");

        RequestUtil.writeAndFlush("Hello Netty Server!", ctx);
        RequestUtil.writeAndFlush("Hello World!!!", ctx);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        List<String> dataList = new ArrayList<>();
        RequestUtil.decode(msg, dataList);
        // 处理数据
        for (String data : dataList) {
            handleMsg(ctx, data);
        }
    }

    private void handleMsg(ChannelHandlerContext ctx, String msg) {
        System.out.println("Response data: " + msg);
    }
}

服务端、客户端输出,通过输出可以看到这时处理的数据就是一个完整的数据了。

// 服务端
channel read...
Request data: Hello Netty Server!
Request data: Hello World!!!

// 客户端
channel read...
Response data: Hello Netty Client!
Response data: Hello World!!!
Response data: Hello Netty Client!
Response data: Hello World!!!

编码器和解码器

为了解决 TCP 粘包/拆包导致的半包读写问题,Netty 默认提供了多种编解码器用于处理半包,使用起来也非常方便。

LineBasedFrameDecoder

LineBasedFrameDecoder 行解码器 支持按换行符 /n/r/n 来读取字节数组,我们在写入数据的时候需要在一条数据的最后加上换行符,读取的时候 LineBasedFrameDecoder 已经自动去掉了换行符,无需再处理。

构造 LineBasedFrameDecoder 必须传入 maxLength 参数,表示最大长度,如果遍历超过这个长度的字节都没有找到分隔符就会报错。

public LineBasedFrameDecoder(final int maxLength) {
    this(maxLength, true, false);
}

/**
 * @param maxLength 最大长度
 * @param stripDelimiter 去除分隔符
 * @param failFast 超过长度时是否直接报错
 */
public LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast) {
    this.maxLength = maxLength;
    this.failFast = failFast;
    this.stripDelimiter = stripDelimiter;
}

只需要在服务端和客户端添加这个处理器即可:

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        // 添加一些处理器
        channelPipeline
                // 行解码器
                .addLast(new LineBasedFrameDecoder(1024))
                // 自定义的服务端处理器
                .addLast(new NettyServerHandler());
    }
})

再看此时的服务端和客户端数据交互代码,可以看到写入数据的时候需要加上换行符,读数据的时候跟以前是一样的方式,但此时读出来的数据不会粘包在一起了。

static final String SEPARATOR = System.getProperty("line.separator");

// 服务端
static class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        // 读数据并处理请求
        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        System.out.println("Request data:" + new String(bytes, StandardCharsets.UTF_8));

        // 响应请求
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello Netty Client!" + SEPARATOR, StandardCharsets.UTF_8));
        ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World!!!" + SEPARATOR, StandardCharsets.UTF_8));
    }
}

// 客户端
static class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel active...");

        // 写数据时加上分隔符
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty Server!" + SEPARATOR, StandardCharsets.UTF_8));
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello World!!!" + SEPARATOR, StandardCharsets.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        System.out.println("Response data:" + new String(bytes, StandardCharsets.UTF_8));
    }
}

通过 LineBasedFrameDecoder 的继承体系可以知道,它是一个通道输入处理器,用于处理输入数据。父类 ByteToMessageDecoder,从字面意思可以知道它是将字节转换为具体消息的解码器。

image.png

通过 ByteToMessageDecoder 的 channelRead 方法进去可以看到,它最终会调用子类的 decode() 方法来解码消息,并将消息放入 out 集合中。解析完后就会遍历每条数据触发通道的读事件,这样在后续的处理器中得到的 msg 就是一个完整的消息了。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        // 解析结果
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 解码
            decode(ctx, msg, out);
        } catch (Exception e) {
            throw new DecoderException(e);
        } finally {
            try {
                // 触发通道读事件
                fireChannelRead(ctx, out, out.size());
            } finally {
                out.recycle();
            }
        }
    } else {
        ctx.fireChannelRead(msg);
    }
}

// 抽象方法,消息解码
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out);

static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {
    for (int i = 0; i < numElements; i ++) {
        // 传入每条消息,触发读事件
        ctx.fireChannelRead(msgs.getUnsafe(i));
    }
}

接着看 LineBasedFrameDecoder 实现的父类 decode 方法,它首先会基于内存地址遍历字节,找到第一个分隔符的位置,然后读取对应长度的数据,默认跳过分隔符。

@Override
protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    Object decoded = decode(ctx, in);
    if (decoded != null) {
        out.add(decoded);
    }
}

protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception {
    // 从缓冲区查找一行的结束位置,即分隔符的位置
    final int eol = findEndOfLine(buffer);
    if (eol >= 0) {
        final ByteBuf frame;
        // 结束符位置 - 当前位置 = 数据长度
        final int length = eol - buffer.readerIndex();
        // 分隔符长度
        final int delimLength = buffer.getByte(eol) == '\r'? 2 : 1;
        
        // 超过最大长度则报错
        if (length > maxLength) {
            buffer.readerIndex(eol + delimLength);
            fail(ctx, length);
            return null;
        }

        // 默认去除分隔符
        if (stripDelimiter) {
            // 读取指定长度的数据
            frame = buffer.readRetainedSlice(length);
            // 跳过分隔符
            buffer.skipBytes(delimLength);
        } else {
            // 读取数据+分隔符
            frame = buffer.readRetainedSlice(length + delimLength);
        }
        return frame;
    } else {
        //...
    }
}

private int findEndOfLine(final ByteBuf buffer) {
    int totalLength = buffer.readableBytes();
    // 基于内存地址遍历字节 寻找分隔符
    int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
    //...
    return i;
}

通过简单的分析源码可以了解到,LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有\n 或者 \r\n, 如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以换行符为结束标志的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。

StringDecoder & StringEncoder

前面在写入数据时,都是先将字符串写入一个 ByteBuf 缓冲区再写入 Channel;读取数据时都是将 msg 强转成 ByteBuf,再读取字节转换成字符串。可以看到这样转来转去很麻烦,我们可以添加内置的 StringDecoder 和 StringEncoder 来避免这种转换。

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // SocketChannel 的处理管道
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        // 添加一些处理器
        channelPipeline
                // 行解码器
                .addLast(new LineBasedFrameDecoder(1024))
                // 字符串解码器
                .addLast(new StringDecoder())
                // 字符串编码器
                .addLast(new StringEncoder())
                // 自定义的服务端处理器
                .addLast(new NettyServerHandler());
    }
})

再看此时的服务端和客户端数据交互代码,可以看到此时就是直接写入字符串,读取的时候直接将 msg 转成字符串来处理。这样就可以直接写入和读取字符串数据了,避免通过 ByteBuf 来读取。

static class NettyServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        // 读数据并处理请求
        String data = (String) msg;
        System.out.println("Request data:" + data);

        // 响应请求
        ctx.channel().writeAndFlush("Hello Netty Client!" + SEPARATOR);
        ctx.channel().writeAndFlush("Hello World!!!" + SEPARATOR);
    }
}

static class NettyClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("channel active...");

        // 写数据时加上分隔符
        ctx.writeAndFlush("Hello Netty Server!" + SEPARATOR);
        ctx.writeAndFlush("Hello World!!!" + SEPARATOR);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("channel read...");

        String data = (String) msg;
        System.out.println("Response data:" + data);
    }
}

简单分析它的源码可知,写入数据时编码,就是将字符串写入一个 ByteBuf 中;读取数据时解码,读取 ByteBuf 中的字节数组再转成字符串。

protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
    out.add(msg.toString(charset));
}

protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
    if (msg.length() == 0) {
        return;
    }

    out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset));
}

DelimiterBasedFrameDecoder

DelimiterBasedFrameDecoder 可以自动完成以分隔符做结束标志的消息的解码。

同样的方式,只需要在 ChannelPipeline 中添加 DelimiterBasedFrameDecoder 解码器即可,在构造 DelimiterBasedFrameDecoder 时需传入特定的分隔符标志。

static final String DELIMITER = "$$";

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // SocketChannel 的处理管道
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        // 添加一些处理器
        channelPipeline
                // 分隔符解码器
                .addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(DELIMITER, UTF_8)))
                // 字符串解码器
                .addLast(new StringDecoder())
                // 字符串编码器
                .addLast(new StringEncoder())
                // 自定义的服务端处理器
                .addLast(new NettyServerHandler());
    }
})

在写数据时,需在末尾添加分隔符标志。

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    System.out.println("channel active...");

    // 写数据时加上分隔符
    ctx.writeAndFlush("Hello Netty Server!" + DELIMITER);
    ctx.writeAndFlush("Hello World!!!" + DELIMITER);
}

FixedLengthFrameDecoder

FixedLengthFrameDecoder 是固定长度解码器,能够按照指定的长度对消息进行自动解码。

static final String DELIMITER = "$$";

.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // SocketChannel 的处理管道
        ChannelPipeline channelPipeline = socketChannel.pipeline();
        // 添加一些处理器
        channelPipeline
                // 分隔符解码器
                .addLast(new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(DELIMITER, UTF_8)))
                // 字符串解码器
                .addLast(new StringDecoder())
                // 字符串编码器
                .addLast(new StringEncoder())
                // 自定义的服务端处理器
                .addLast(new NettyServerHandler());
    }
})

应用层协议

Netty 提供了多种协议支持,例如 HTTP、WebSocket、SSL 等等,我们可以通过它提供的组件快速开发出对应协议的服务器,而不用担心 TCP 粘包/拆包等网络问题。

HTTP 协议

HTTP 是建立在 TCP 传输协议之上的应用层协议,下面这段程序是基于 Netty 开发的一个 HTTP 服务器。

package com.lyyzoo.netty.http;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.util.ArrayList;
import java.util.List;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedWriteHandler;

public class HttpServer {
    private final int port;

    public HttpServer(int port) {
        this.port = port;
    }

    /**
     * 启动 Netty Server
     */
    public void start() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(32);

        try {
            // Netty网络服务器(服务端启动类)
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap
                    .group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            // 添加一些处理器
                            socketChannel.pipeline()
                                    // HTTP 请求消息解码器
                                    .addLast("http-decoder", new HttpRequestDecoder())
                                    // 聚合解码器
                                    .addLast("http-aggregator", new HttpObjectAggregator(65536))
                                    // HTTP 响应编码器
                                    .addLast("http-encoder", new HttpResponseEncoder())
                                    // 支持异步发送大的码流
                                    .addLast("http-chunked", new ChunkedWriteHandler())
                                    // 自定义的HTTP处理器
                                    .addLast("http-server-handler", new HttpServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
            ;

            // 绑定要监听的端口
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            // 对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    // HTTP 处理器
    static class HttpServerHandler extends SimpleChannelInboundHandler<HttpRequest> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) throws Exception {
            String method = request.method().name();
            String uri = request.uri();
            System.out.println("HTTP Request: " + method + " " + uri);

            // 响应
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
            ByteBuf buffer = Unpooled.buffer();

            // 处理请求
            if (HttpMethod.GET.name().equals(method) && uri.equals("/index")) {
                String html = "<html><body>Welcome to Netty!!!</body></html>";
                buffer.writeBytes(html.getBytes(UTF_8));
                response.headers().set("Content-Type", "text/html;charset=UTF-8");
                response.content().writeBytes(buffer);
            }
            else if (HttpMethod.GET.name().equals(method) && uri.equals("/list")) {
                List<String> dataList = new ArrayList<>();
                dataList.add("Netty");
                dataList.add("RocketMQ");

                buffer.writeBytes(dataList.toString().getBytes(UTF_8));

                response.headers().set("Content-Type", "application/json;charset=UTF-8");
                response.content().writeBytes(buffer);
            } else {
                response.setStatus(HttpResponseStatus.NOT_FOUND);
                buffer.writeBytes("Resource Not Found.".getBytes(UTF_8));
                response.content().writeBytes(buffer);
            }

            buffer.release();
            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        }
    }

    public static void main(String[] args) {
        // 启动 Netty Server
        HttpServer nettyServer = new HttpServer(9000);
        nettyServer.start();
    }
}

可以看到主要就是添加如下几个处理器来支持HTTP协议:

  • HttpRequestDecoder:HTTP 请求消息解码器,对 HTTP 请求行、请求头、请求体进行解析。

  • HttpObjectAggregator:聚合解码器,用于将多个消息转换为单一的 FullHttpRequest 或 FullHttpResponse,因为 HTTP 解码器会将每个 HTTP 消息解码生成多个消息对象。

  • HttpResponseEncoder:HTTP 响应消息编码器,对 HTTP 状态行、响应头、响应体 进行编码。

  • ChunkedWriteHandler:支持异步发送大的码流,例如大的文件传输,但不会占用过多的内存,防止内存溢出。

  • 最后是添加自定义的业务处理器,处理 HTTP 请求和响应。

从 Netty 的源码可以了解到,Netty 提供了 http、http2、redis、mqtt、smtp、xml 等纵多主流协议的支持,基于 Netty 我们可以快速开发出相应的服务器和客户端程序,而不用担心网络问题的处理。

image.png