【Netty】Netty权威指南- 第4章 TCP粘包/拆包问题的解决方案
一. TCP 粘包/拆包概念
TCP是一个“流”,协议,所谓流,就是没有界限的一串数据,大家可以想想河里的流水,它们是连成一片的,其间并没有分界线,TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包的问题
二. TCP粘包/拆包发生的原因
问题产生的原因主要有三个,分别如下
  1.应用程序write写入的字节大小大于套接字接口发送缓冲区大小
  2.进行MSS大小的TCP分段
  3.以太网帧的payload大于MTU进行IP分片

三. TCP粘包问题的解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界主流的协议的解决方案,可以归纳如下:
  1.消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格
  2.在包尾增加回车换行符进行分割,例如FTP协议
  3.将消息分为消息头和消息提,消息头中包含消息总长度(或者消息体长度)的字节,通常设计思路为消息的第一个字段使用int32来表示消息的总长度
  4.更复杂的应用层协议
四. Netty的半包解码器解决TCP粘包/拆包问题
以下以LineBasedFrameDecoder解码器为例说明Netty解决粘包问题
TimeServer
package com.demo.chapter4; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; /** * @author Sam.yang * @since 2023/5/15 01:26 */ public class TimeServer { public void bind(int port) { //配置服务端的NIO 线程组 实际上它们就是Reactor线程组 //创建两个的原因是: //一个用于服务端接受客户端的连接 //另一个用于进行SocketChannel的网络读写 EventLoopGroup bossGroups = new NioEventLoopGroup(); EventLoopGroup workGroups = new NioEventLoopGroup(); try { ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroups, workGroups) .channel(NioServerSocketChannel.class) /** TCP backlog是指TCP服务器在处理客户端连接请求时,可以排队等待的最大连接数。当服务器的连接请求队列已满时,新的连接请求将被拒绝。TCP backlog的大小可以通过操作系统的参数进行配置,通常默认值为128。 当服务器的并发连接数较高时,TCP backlog的大小需要适当调整,以避免连接请求被拒绝。如果TCP backlog设置过小,可能会导致连接请求被拒绝,从而影响服务器的可用性。 如果TCP backlog设置过大,可能会占用过多的系统资源,从而影响服务器的性能。 在Netty中,可以通过ServerBootstrap的option方法和childOption方法设置TCP backlog的大小。例如: ServerBootstrap的option方法设置了TCP backlog的大小为1024。这意味着服务器可以排队等待1024个连接请求。如果连接请求队列已满,新的连接请求将被拒绝。 */ .option(ChannelOption.SO_BACKLOG, 1024) /** * 绑定IO处理事件childHandler 主要用于处理网络I/O事件 例如记录日志,对消息进行编码等等 */ .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel channel) throws Exception { //Netty提供了多种编码器和解码器 //解决粘包的问题 channel.pipeline().addLast(new LineBasedFrameDecoder(1024)); channel.pipeline().addLast(new StringDecoder()); channel.pipeline().addLast(new TimeServerHandler3()); } }) ; //绑定端口 ,同步等待成功 ChannelFuture future = bootstrap.bind(port).sync(); //等待服务端监听端口关闭 future.channel().closeFuture().sync(); } catch (InterruptedException e) { System.out.println("异常" + e); } finally { //优雅推出 释放线程池资源 bossGroups.shutdownGracefully(); workGroups.shutdownGracefully(); } } }
TimeServerHandler
package com.demo.chapter4; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import java.util.Date; /** * @author Sam.yang * @since 2023/5/18 00:05 */ public class TimeServerHandler3 extends ChannelInboundHandlerAdapter { private 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 = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length()); String body = ((String) msg); System.out.println("The time server receive order:" + body + "; the counter is:" + ++counter); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER"; currentTime = currentTime + System.getProperty("line.separator"); ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.writeAndFlush(resp); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("断开链接:{}" + ctx); System.out.println("异常信息" + cause); } }
TimeClient
package com.demo.chapter4; import io.netty.bootstrap.Bootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; /** * @author Sam.yang * @since 2023/5/15 01:26 */ public class TimeClient { public void bind(int port) { //配置客户端NIO线程组 EventLoopGroup group = new NioEventLoopGroup(); Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024)); socketChannel.pipeline().addLast(new StringDecoder()); socketChannel.pipeline().addLast(new TimeClientHandler3()); } }); ChannelFuture future = null; try { //发起异步连接操作 future = bootstrap.connect("127.0.0.1", port).sync(); //等待客户端链路关闭 future.channel().closeFuture().sync(); } catch (InterruptedException e) { System.out.println("发生了异常" + e); } finally { group.shutdownGracefully(); } } }
TimeClientHandler3
package com.demo.chapter4; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * 模拟TCP粘包的行为 * * @author Sam.yang * @since 2023/5/18 00:05 */ public class TimeClientHandler3 extends ChannelInboundHandlerAdapter { private int counter; private byte[] req; public TimeClientHandler3() { req = ("QUERY TIME ORDER" + System.getProperty("line.separator")).getBytes(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //super.channelActive(ctx); ByteBuf message; for (int i = 0; i < 100; i++) { message = Unpooled.buffer(req.length); message.writeBytes(req); ctx.writeAndFlush(message); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //super.channelRead(ctx, msg); // ByteBuf buf = (ByteBuf) msg; // byte[] req = new byte[buf.readableBytes()]; // buf.readBytes(req); // String body = new String(req, "UTF-8"); String body = ((String) msg); System.out.println("Now is:" + body + "; the counter is:" + ++counter); } }
五. Netty中LineBasedFrameDecoder 和 StringDecoder的原理分析
LineBasedFrameDecoder 的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n” 或者“\r\n” 如果有,就以此为结束位置,从可读索引到结束位置区间的字节就组成了一行,它是以换行符为结束标记的解码器,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度,如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前都熬的异常码流
StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的Handler
LineBasedFrameDecoder + StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包
除此之外,Netty还提供了多种支持TCP粘包/拆包的解码器,用来满足用户的不同诉求
六. 问题
  1. UDP协议下的包传输会有粘包问题吗?
  - UDP协议本身不会出现粘包问题,因为UDP是无连接的、不可靠的传输协议,每个UDP数据包都是独立的,没有顺序和关联性。每个UDP数据包都有自己的头部信息,包括源端口、目的端口、长度和校验和等,这些信息可以帮助接收方正确地接收和处理每个UDP数据包。
但是,当UDP数据包在传输过程中经过网络设备(如路由器、交换机等)时,这些设备可能会将多个UDP数据包合并成一个数据包进行传输(提高网络传输的效率和带宽利用率),从而导致粘包问题的出现。此外,由于UDP没有提供流量控制和拥塞控制等机制,发送方发送数据的速度可能会超过接收方处理数据的速度,也会导致粘包问题的出现。
  2.TCP的流式协议和TCP Flood攻击的关系
  - TCP的流式协议和TCP Flood攻击是有关系的。TCP协议是一种流式协议,它将数据分割成小的数据包并在网络上传输,以实现数据传输。TCP协议的流式特性使得数据可以按照顺序传输,并且可以在传输过程中进行错误检测和纠正。
     TCP Flood攻击是一种利用TCP协议的漏洞来进行的攻击方式。攻击者通过发送大量的TCP连接请求来占用目标服务器的资源,从而使其无法正常工作。由于TCP协议的流式特性,攻击者可以通过发送大量的TCP连接请求来占用服务器的资源,从而导致服务器无法正常处理其他合法用户的请求。
      因此,TCP Flood攻击利用了TCP协议的流式特性来实现攻击目的。为了防止TCP Flood攻击,网络管理员可以采取一些措施,例如限制来自外部网络的TCP连接请求、使用专门的防御工具来检测和阻止TCP Flood攻击、对网络进行定期的漏洞扫描和安全评估等。
  3.TCP Half Flood 攻击
  - Half Flood攻击是一种DDoS攻击的形式,它是一种基于TCP半连接的攻击方式。攻击者通过发送大量的TCP半连接请求来占用目标服务器的资源,从而使其无法正常工作。
   Half Flood攻击的原理是攻击者向目标服务器发送大量的TCP SYN请求,但是在建立连接的第二个步骤中,攻击者不会发送ACK确认包,而是直接关闭连接。这样就会导致目标服务器在等待ACK确认包的过程中浪费大量的资源,从而影响其正常的服务。
     Half Flood攻击的特点是攻击流量大,但是攻击者的IP地址很难被追踪,因为攻击者只发送了半个TCP连接请求,而没有完成TCP连接。此外,Half Flood攻击也可以绕过一些基于TCP连接数的防御措施。
    为了防止Half Flood攻击,网络管理员可以采取以下措施:
    1.配置防火墙,限制来自外部网络的TCP连接请求。
    2.使用专门的防御工具,如DDoS防护设备,来检测和阻止Half Flood攻击。
    3.对网络进行定期的漏洞扫描和安全评估,及时发现并修复可能存在的漏洞  
    4.配置TCP连接数限制,限制每个IP地址可以建立的TCP连接数,从而防止Half Flood攻击。

 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号