Loading

Netty编码解码器

TCP粘包拆包问题

我没什么网络编程经验,但在之前在开发VPad的时候也发现了这个问题。

VPad基于流式的、BIO的SocketAPI进行通信,我需要在客户端和服务器之间发送一些代表MIDI或控制信息的消息,就像这样:

其中,头一个字节中的2代表这是一个midi消息,第二个字节note代表按下的音符,第三个字节代表音符的力度,第四个代表音符的状态,这四个字节组成了一个midi控制消息。

我在服务器端正常的接收,然后我发现,有的时候,当我一起按下两个音符的时候,服务器端会接到这样的消息:

02 60 90 1 02 62 90 1

这其实是两个MIDI信息,但它们在同一个TCP数据包中到达

02 60 90 1  音符60以90的力度按下
02 62 90 1  音符62以90的力度按下

这就是粘包,TCP无法理解上层应用传递数据的语义,它没法分析出你要发送的其实是两个消息,它有时会选择将你的多次发送打包放在一个TCP包中传输。

而拆包就是当你要发送的数据太大,TCP有可能将它拆成若干个数据包。

我当时的解决办法是,每个消息的头两个字节代表消息的长度,也就是说一个消息最长是65535-2。然后接收者接到数据时首先会读取两个字节,取出消息的长度L,然后再向后读取L个字节,这个消息就被读完了。这样就能应付拆包或粘包了。

对于一些应用,也可以选择如下方法:

  1. 固定消息长度
  2. 在包尾添加自定义的分隔符(比如换行)
  3. 定义一个应用层协议由双方遵守

TimeServer和TimeClient发生粘包的案例

下面是使用Netty编写的TimeServer和TimeClient,功能如下:

  1. TimeClient向TimeServer发送QUERY TIME ORDER\r\n
  2. TimeServer解析TimeClient的命令,如果是QUERY TIME ORDER\r\n,就向TimeClient发送当前时间,否则向TimeClient发送BAD ORDER
public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    // 记录服务器的服务客户端请求的次数
    private volatile int counter;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] req = new byte[buf.readableBytes()];
        buf.readBytes(req);

        // 获取请求数据并减去换行符的长度得到发送方的原始数据
        String body = StringUtils.removeSeparator(new String(req, "UTF-8"));

        System.out.println("Timeserver received order : " + body + ", the counter is " + ++counter);

        String result = StringUtils.addSeparator(
                body.equalsIgnoreCase("QUERY TIME ORDER") ?
                new Date(System.currentTimeMillis()).toString() : "BAD ORDER"
        );

        ByteBuf respBuf = Unpooled.copiedBuffer(result.getBytes());
        ctx.write(respBuf);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

很简单,然后我们来创建一个客户端,并且让客户端一次性发送100条查询来测试:

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    byte[] reqData = StringUtils.addSeparator("QUERY TIME ORDER").getBytes(StandardCharsets.UTF_8);
    private volatile int counter;
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf buf = null;
        for (int i = 0; i < 100; i++) {
            buf = Unpooled.copiedBuffer(reqData);
            ctx.writeAndFlush(buf);
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        byte[] bytes = new byte[buf.readableBytes()];
        buf.readBytes(bytes);
        String result = StringUtils.removeSeparator(
                new String(bytes, "UTF-8")
        );
        System.out.println("Now is : " + result + ", the counter is " + ++counter);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

下面是StringUtils

public class StringUtils {
    private static final String lineSeparator = System.getProperty("line.separator");

    public static String addSeparator(String original) {
        return original + lineSeparator;
    }

    public static String removeSeparator(String original) {
        return original.substring(0, original.length() - lineSeparator.length());
    }
}

这个代码在压力很小的情况下不会出现问题,包括我之前的VPad也是,但是在同时发送100条数据时,就会发生粘包。我们预期的效果是客户端发起100个请求数据包,服务端返回100个结果,客户端和服务端的counter都是100,然而运行时结果却不是这样。

server端结果
...omit some line...
QUERY TIME ORDER
QUERY TIME ORDER
QUERY TIME ORDER
QUERY TIME ORDER
QUERY TIME ORDER
QUERY TIME ORDER, the counter is 1

client端结果
Now is : BAD ORDER, the counter is 1

造成该结果是因为发生了粘包,我们的100次请求被打在了一个数据包中发送给服务器,服务器认为客户端发送的东西不合法,所以返回了一个BAD ORDER

使用LineBasedFrameDecoder解决TCP粘包问题

在服务器和客户端的启动器Bootstrap上添加handler,就可以自动解决粘包问题。

// Server
ServerBootstrap bootstrap = new ServerBootstrap()
        /*... omit some code ...*/
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                // 添加LineBasedFrameDecoder
                socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024));
                // 添加StringDecoder
                socketChannel.pipeline().addLast(new StringDecoder(StandardCharsets.UTF_8));
                socketChannel.pipeline().addLast(new TimeServerHandler());
            }
        });

LineBasedFrameDecoder的作用是将接收到的包按行分割,StringDecoder将ByteBuf形式的数据转换成字符串。所以,我们的TimeServerHandler的channelRead可以写成如下形式:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    String body = (String) msg;

    System.out.println("Timeserver received order : " + body + ", the counter is " + ++counter);

    String result = StringUtils.addSeparator(
            body.equalsIgnoreCase("QUERY TIME ORDER") ?
            new Date(System.currentTimeMillis()).toString() : "BAD ORDER"
    );

    ByteBuf respBuf = Unpooled.copiedBuffer(result.getBytes());
    ctx.write(respBuf);
}

发送过来的数据包会经过LineBasedFrameDecoder自动被分成若干行,对于每行的ByteBuf,会被StringDecoder用对应的字符编码解码成字符串,然后对于每一个这样的字符串(也就是客户端请求的每一行),channelRead都会被调用一次。我们不需要再手动进行ByteBuf到字符串的转换,不需要去末尾的换行符,而且由于LineBasedFrameDecoder会把数据按换行符分割成若干个Frame,粘包问题也被解决了。

客户端的代码更加简单:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    String result = (String) msg;
    System.out.println("Now is : " + result + ", the counter is " + ++counter);
}

别忘了客户端的bootstrap也需要添加这两个Decoder。

server结果
... omit some line ...
Timeserver received order : QUERY TIME ORDER, the counter is 97
Timeserver received order : QUERY TIME ORDER, the counter is 98
Timeserver received order : QUERY TIME ORDER, the counter is 99
Timeserver received order : QUERY TIME ORDER, the counter is 100



client结果
... omit some line ...
Now is : Thu Mar 24 12:31:27 CST 2022, the counter is 97
Now is : Thu Mar 24 12:31:27 CST 2022, the counter is 98
Now is : Thu Mar 24 12:31:27 CST 2022, the counter is 99
Now is : Thu Mar 24 12:31:27 CST 2022, the counter is 100

其它Decoder

  1. DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(";".getBytes(StandardCharsets.UTF_8)))
    指定分隔符
  2. FixedLengthFrameDecoder(length)
    指定固定长度
posted @ 2022-03-24 14:37  yudoge  阅读(52)  评论(0编辑  收藏  举报