什么是粘包拆包

本文来说下什么是粘包拆包


概述

有过tcp编程的伙伴可能都知道,无论是服务端还是客户端,发送或读取消息的时候,都需要考虑粘包/拆包问题。本篇文章笔者就介绍下,tcp粘包拆包的一些基础知识,以及解决方案和示例。


什么是粘包/拆包

TCP是个“流”式的协议,所谓流,就像河里的水,中间没有边界。TCP传输的数据,在网络上就是一连串的数据,没有分界线。TCP协议的底层,并不了解上层业务的具体定义,它会根据TCP缓冲区的实际情况进行包的划分。在业务层面认为一个完整的包,可能会被TCP拆分成多个小包进行发送,也可能把多个小的包封装成一个大的数据包进行发送,这就是所谓的TCP粘包拆包问题。


粘包/拆包可能发生的情况

TCP粘包/拆包,可能发生4种情况,如图1所示:

在这里插入图片描述
客户端发送了两个数据包P1和P2给服务端,服务端一次读取到的字节数是不确定的,可能存在以下4种情况:

(1)服务端分两次读取到了两个独立的数据包P1和P2,没有发送粘包和拆包;
(2)服务端一次读到了两个数据包,P1和P2粘在一起,这就是TCP粘包情况;
(3)服务端分两次读取到了两个数据包,第一次读取了完整的P1包和P2包的一部分,第二次读取到了P2包的剩余部分,这被称为TCP拆包;
(4)服务端分两次读取了两个数据包,第一次读取了P1包的一部分,第二次读取到了P1包的剩余部分,这也是TCP拆包;


TCP粘包/拆包发生的原因

TCP数据流最终发到目的地,需要通过以太网协议封装成一个个的以太网帧发送出去,以太网数据帧大小最小64字节,最大1518字节,除去header部分,其数据payload为46到1500字节。所以如果以太网帧的payload大于MTU(默认1500字节)就需要进行拆包。


粘包拆包问题解决方法

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

(1)消息定长,发送方和接收方规定固定大小的消息长度,例如每个报文大小固定为200字节,如果不够,空位补空格;
(2)在包围增加特殊字符进行分割,例如FTP协议;
(3)自定义协议,将消息分为消息头和消息体,消息头中包含消息总长度,这样服务端就可以知道每个数据包的具体长度了,知道了发送数据包的具体边界后,就可以解决粘包和拆包问题了;


为什么UDP不会发生粘包/拆包问题

UDP是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法(TCP使用了Nagle算法)。UDP支持的是一对多的模式,所以接收端的缓冲区采用了链式结构来缓存每一个到达的数据包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。即面向消息的通信是有消息边界的。所以udp根本不会粘包,但是会丢数据,不可靠。

笔者使用jdk的nio,写了个简单的发送文件的客户端,每次发送文件协议格式:数据包总长度|文件名长度|文件名|文件内容长度|文件内容,时间有限,服务端程序还未写完,稍后会继续完成的。

public static void sendFile(String hostname, int nioPort, byte[] file, String filename, long fileSize) {
    SocketChannel channel = null;
    Selector selector = null;
    try {
        channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress(hostname, nioPort));
        selector = Selector.open();
        channel.register(selector, SelectionKey.OP_CONNECT);
        while (true) {
            selector.select();
            Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
            while (keysIterator.hasNext()) {
                SelectionKey key = keysIterator.next();
                keysIterator.remove();
                if (key.isConnectable()) {
                    channel = (SocketChannel) key.channel();
                    if (channel.isConnectionPending()) {
                        channel.finishConnect();
                        byte[] filenameBytes = filename.getBytes();
                        long totalLen = 4 + filenameBytes.length + 8 + fileSize;
                        ByteBuffer buffer = ByteBuffer.allocate((int) fileSize * 2 + filenameBytes.length);
                        buffer.putLong(totalLen);
                        buffer.putInt(filenameBytes.length);
                        buffer.put(filenameBytes);
                        buffer.putLong(fileSize);
                        buffer.put(file);
                        buffer.flip();
                        int sentData = channel.write(buffer);
                        System.out.println("已经发送了" + sentData + "字节的数据到" + hostname);
                        channel.register(selector, SelectionKey.OP_READ);
                    }
                }
                else if (key.isReadable()) {
                    channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = channel.read(buffer);
                    if (len > 0) {
                        System.out.println("收到服务端的响应:" + new String(buffer.array(), 0, len));
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (channel != null) {
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

netty是如何解决粘包拆包问题的

基于jdk原生的socket或者nio编程,解决粘包拆包问题毕竟麻烦,作为一款非常强大的网络通信框架,netty提供了多种编码器用于解决粘包拆包问题,只要掌握这些类库的使用,你就不用关心如何解决粘包拆包问题了。

常见编码器:

  • LineBasedFrameDecoder,基于行的解码器,遇到 “\n”、"\r\n"会被作为行分隔符;
  • FixedLengthFrameDecoder,基于固定长度的解码器;
  • DelimiterBasedFrameDecoder,基于分隔符的振解码器;

本文小结

本文详细介绍了什么是粘包拆包以及粘包拆包产生的原因。

  • 9
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值