【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攻击。

 

posted @ 2023-05-20 19:23  听风是雨  阅读(1722)  评论(0)    收藏  举报
/* 看板娘 */