详解什么是 TCP 粘包和拆包现象并演示 Netty 是如何解决的( 三 )

  1. 还用上面的例子 , 将客户端通道处理类:NettyClientHandler 中的 channelActive 方法修改成如下的方式
// 重写 channelActive, 当客户端启动的时候 自动发送数据给服务端@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// 使用客户端分10次发送 , 每次数据量少// tcp 会等客户端多次生产后 , 一次性进行发送for (int i = 0; i < 10; i++) {ByteBuf buffer = Unpooled.copiedBuffer("ckjava" + i + " ", StandardCharsets.UTF_8);ctx.writeAndFlush(buffer);}}
  1. 分别先启动服务器端后 , 再启动客户端 , 服务器端的输出如下
17:12:27.239 [nioEventLoopGroup-2-1] INFOcom.ckjava.test.server.NettyServer - 服务器端启动成功 , 开放端口:8080服务器收到的数据=ckjava0 ckjava1 ckjava2 ckjava3 ckjava4 ckjava5 ckjava6 ckjava7 ckjava8 ckjava9 服务器收到的数据次数=1
  1. 客户端的输出如下
17:12:36.917 [nioEventLoopGroup-2-1] INFOcom.ckjava.test.client.NettyClient - 连接服务器端:127.0.0.1:8080 成功!17:12:36.961 [nioEventLoopGroup-2-1] INFOc.c.test.client.NettyClientHandler - 客户端接收到消息:[31b25c25-bd32-4ff1-b390-0c31b2558d12]17:12:36.962 [nioEventLoopGroup-2-1] INFOc.c.test.client.NettyClientHandler - 客户端接收到消息的次数:117:12:36.962 [nioEventLoopGroup-2-1] INFOc.c.test.client.NettyClientHandler - ---------------------------------------------------
  1. 从服务器端和客户端的输出结果来看:客户端只发送了 10 次数据 , 但是服务器端却收到了 1 次数据 , 说明 tcp 在客户端粘包后一次性发送了全部的数据 。
Netty 解决 TCP 粘包和拆包现象带来的问题TCP 粘包和拆包现象带来的问题从上面的案例可以发现当出现 TCP 粘包和拆包现象后会出现下面的问题:
  1. tcp 在粘包的时候 , 数据混合后 , 接收方不能正确区分数据的头尾 , 如果是文件类型的数据 , 会导致文件破坏 。
  2. tcp 在拆包的时候 , 数据拆分后 , 接收方不能正确区分数据的头尾 , 导致收到的消息错乱 , 影响语义 。
如何解决 TCP 粘包和拆包现象带来的问题由于 TCP 粘包和拆包现象会导致不能正确区分数据的头尾 , 那么解决的办法也挺简单的 , 通过 特殊字符串 来分隔消息体或者使用 定长消息 就能够正确区分数据的头尾 。
目前的主流解决方式有以下几种:
  1. 使用定长消息 , Client 和 Server 双方约定报文长度 , Server 端接受到报文后 , 按指定长度解析;
  2. 使用特定分隔符 , 比如在消息尾部增加分隔符 。Server 端接收到报文后 , 按照特定的分割符分割消息后 , 再解析;
  3. 将消息分割为消息头和消息体两部分 , 消息头中指定消息或者消息体的长度 , 通常设计中使用消息头第一个字段 int32 表示消息体的总长度;
Netty 中也提供了基于分隔符实现的半包解码器和定长的半包解码器:
  1. LineBasedFrameDecoder 使用"n"和"rn"作为分割符的解码器
  2. DelimiterBasedFrameDecoder 使用自定义的分割符的解码器
  3. FixedLengthFrameDecoder 定长解码器
通过 Netty 的 DelimiterBasedFrameDecoder 解码器 来解决 TCP 粘包和拆包现象带来的问题使用
DelimiterBasedFrameDecoder 可以确保收到的数据会自动通过 自定义的分隔符 进行分隔 。发送的时候消息的后面只需要增加上 自定义的分隔符 即可 。
  1. 基于上面的例子 , 服务器端 NettyServer 改动如下
public void start() {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap sbs = new ServerBootstrap().group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(port)).childHandler(new ChannelInitializer<SocketChannel>() {protected void initChannel(SocketChannel ch) {// 使用分隔符"$_"的半包解码器ByteBuf byteBuf = Unpooled.copiedBuffer(DELIMITER.getBytes());ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, byteBuf));ch.pipeline().addLast(new NettyServerHandler());}}).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);// 绑定端口 , 开始接收进来的连接sbs.bind(port).addListener(future -> {log.info(String.format("服务器端启动成功 , 开放端口:%s", port));});} catch (Exception e) {log.error("启动服务器端出现异常", e);}}


推荐阅读