V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
javaCoder
V2EX  ›  程序员

TCP 粘包问题浅析及其解决方案

  •  
  •   javaCoder · 2018-08-10 14:27:32 +08:00 · 21044 次点击
    这是一个创建于 2327 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文地址: haifeiWu 的博客
    博客地址:www.hchstudio.cn
    欢迎转载,转载请注明作者及出处,谢谢!

    最近一直在做中间件相关的东西,所以接触到的各种协议比较多,总的来说有 TCP,UDP,HTTP 等各种网络传输协议,因此楼主想先从协议最基本的 TCP 粘包问题搞起,把计算机网络这部分基础夯实一下。

    TCP 协议的简单介绍

    TCP 是面向连接的运输层协议

    简单来说,在使用 TCP 协议之前,必须先建立 TCP 连接,就是我们常说的三次握手。在数据传输完毕之后,必须是释放已经建立的 TCP 连接,否则会发生不可预知的问题,造成服务的不可用状态。

    每一条 TCP 连接都是可靠连接,且只有两个端点

    TCP 连接是从 Server 端到 Client 端的点对点的,通过 TCP 传输数据,无差错,不重复不丢失。

    TCP 协议的通信是全双工的

    TCP 协议允许通信双方的应用程序在任何时候都能发送数据。TCP 连接的两端都设有发送缓冲区和接收缓冲区,用来临时存放双向通信的数据。发送数据时,应用程序把数据传送给 TCP 的缓冲后,就可以做自己的事情,而 TCP 在合适的时候将数据发送出去。在接收的时候,TCP 把收到的数据放入接收缓冲区,上层应用在合适的时候读取数据。

    TCP 协议是面向字节流的

    TCP 中的流是指流入进程或者从进程中流出的字节序列。所以向 Java,golang 等高级语言在进行 TCP 通信是都需要将相应的实体序列化才能进行传输。还有就是在我们使用 Redis 做缓存的时候,都需要将放入 Redis 的数据序列化才可以,原因就是 Redis 底层就是实现的 TCP 协议。

    **TCP 并不知道所传输的字节流的含义,TCP 并不能保证接收方应用程序和发送方应用程序所发出的数据块具有对应大小的关系(这就是 TCP 传输过程中产生的粘包问题)。**但是应用程序接收方最终受到的字节流与发送方发送的字节流是一定相同的。因此,我们在使用 TCP 协议的时候应该制定合理的粘包拆包策略。

    下图是 TCP 的协议传输的整个过程:

    TCP 面向字节流

    下面这个图是从老钱的博客里面取到的,非常生动 TCP 传输动图

    TCP 粘包问题复现

    理论推敲

    如下图所示,出现的粘包问题一共有三种情况

    TCP 粘包问题

    第一种情况: 如上图中的第一根bar所示,服务端一共读到两个数据包,每个数据包都是完成的,并没有发生粘包的问题,这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,每次服务端读取到的消息都是完成的,并不会出现数据不正确的情况。

    第二种情况: 服务端仅收到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于第一种情况的逻辑实现的服务端就蒙了,因为服务端并不能很好的处理这个数据包,甚至不能处理,这种情况其实就是 TCP 的粘包问题。

    第三种情况: 服务端收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了 TCP 拆包问题,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。

    为什么会发生 TCP 粘包、拆包

    1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。

    2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。

    3. 进行 MSS (最大报文长度)大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包。

    4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包。

    如何处理粘包、拆包

    通常会有以下一些常用的方法:

    1. 使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。

    2. 设置定长消息,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符。

    3. 设置消息边界,服务端从网络流中按消息编辑分离出消息内容,一般使用‘\n ’。

    4. 更为复杂的协议,例如楼主最近接触比较多的车联网协议 808,809 协议。

    TCP 粘包拆包的代码实践

    下面代码楼主主要演示了使用规定消息头,消息体的方式来解决 TCP 的粘包,拆包问题。

    server 端代码: server 端代码的主要逻辑是接收客户端发送过来的消息,重新组装出消息,并打印出来。

    
    import java.io.*;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    /**
     * @author wuhf
     * @Date 2018/7/16 15:50
     **/
    public class TestSocketServer {
        public static void main(String args[]) {
            ServerSocket serverSocket;
            try {
                serverSocket = new ServerSocket();
                serverSocket.bind(new InetSocketAddress(8089));
                while (true) {
                    Socket socket = serverSocket.accept();
                    new ReceiveThread(socket).start();
    
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    
        static class ReceiveThread extends Thread {
            public static final int PACKET_HEAD_LENGTH = 2;//包头长度
            private Socket socket;
            private volatile byte[] bytes = new byte[0];
    
            public ReceiveThread(Socket socket) {
                this.socket = socket;
            }
    
            public byte[] mergebyte(byte[] a, byte[] b, int begin, int end) {
                byte[] add = new byte[a.length + end - begin];
                int i = 0;
                for (i = 0; i < a.length; i++) {
                    add[i] = a[i];
                }
                for (int k = begin; k < end; k++, i++) {
                    add[i] = b[k];
                }
                return add;
            }
    
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    try {
                        InputStream reader = socket.getInputStream();
                        if (bytes.length < PACKET_HEAD_LENGTH) {
                            byte[] head = new byte[PACKET_HEAD_LENGTH - bytes.length];
                            int couter = reader.read(head);
                            if (couter < 0) {
                                continue;
                            }
                            bytes = mergebyte(bytes, head, 0, couter);
                            if (couter < PACKET_HEAD_LENGTH) {
                                continue;
                            }
                        }
                        // 下面这个值请注意,一定要取 2 长度的字节子数组作为报文长度,你懂得
                        byte[] temp = new byte[0];
                        temp = mergebyte(temp, bytes, 0, PACKET_HEAD_LENGTH);
                        String templength = new String(temp);
                        int bodylength = Integer.parseInt(templength);//包体长度
                        if (bytes.length - PACKET_HEAD_LENGTH < bodylength) {//不够一个包
                            byte[] body = new byte[bodylength + PACKET_HEAD_LENGTH - bytes.length];//剩下应该读的字节(凑一个包)
                            int couter = reader.read(body);
                            if (couter < 0) {
                                continue;
                            }
                            bytes = mergebyte(bytes, body, 0, couter);
                            if (couter < body.length) {
                                continue;
                            }
                        }
                        byte[] body = new byte[0];
                        body = mergebyte(body, bytes, PACKET_HEAD_LENGTH, bytes.length);
                        count++;
                        System.out.println("server receive body:  " + count + new String(body));
                        bytes = new byte[0];
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
    }
    
    

    **client 端代码:**客户端代码主要逻辑是组装要发送的消息,确定消息头,消息体,然后发送到服务端。

    
    import java.io.*;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    
    /**
     * @author wuhf
     * @Date 2018/7/16 15:45
     **/
    public class TestSocketClient {
        public static void main(String args[]) throws IOException {
            Socket clientSocket = new Socket();
            clientSocket.connect(new InetSocketAddress(8089));
            new SendThread(clientSocket).start();
    
        }
    
        static class SendThread extends Thread {
            Socket socket;
            PrintWriter printWriter = null;
    
            public SendThread(Socket socket) {
                this.socket = socket;
                try {
                    printWriter = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
    
            @Override
            public void run() {
                String reqMessage = "HelloWorld ! from clientsocket this is test half packages!";
                for (int i = 0; i < 100; i++) {
                    sendPacket(reqMessage);
                }
                if (socket != null) {
                    try {
                        socket.close();
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
    
            }
    
            public void sendPacket(String message) {
                try {
                    OutputStream writer = socket.getOutputStream();
                    writer.write(message.getBytes());
                    writer.flush();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    
    }
    
    
    

    小结

    最近一直在写一些框架性的博客,专门针对某些问题进行原理性的技术探讨的博客还比较少,所以楼主想着怎样能在自己学到东西的同时也可以给一同在技术这条野路子上奋斗的小伙伴们一些启发,是楼主一直努力的方向。

    参考文章

    137 条回复    2023-10-10 20:05:37 +08:00
    1  2  
    mhycy
        1
    mhycy  
       2018-08-10 14:46:22 +08:00   ❤️ 34
    提醒:TCP 是流传输协议

    > 传输控制协议(英语:Transmission Control Protocol,缩写为 TCP )
    > 是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。
    > (中文维基百科 TCP 词条 第一句)

    理解了这个本质,那么**粘包**就不是一个问题
    因为本来就没有**包**的概念,自然没有**粘**的可能性,更没有**粘包**这一说法

    本质问题:如何在流传输协议上构造具有**可靠**分段特性的(包 /报文 /数据段)传输协议
    catror
        2
    catror  
       2018-08-10 14:49:54 +08:00 via Android   ❤️ 5
    TCP 没有包的概念,不要再普及错误的知识了。这里所谓的包是构建在 TCP 之上的另一种协议的概念。
    chinawrj
        3
    chinawrj  
       2018-08-10 14:54:22 +08:00   ❤️ 2
    TCP 是流。。。。
    另外粘包定义好奇葩。哈哈
    wqyyy
        4
    wqyyy  
       2018-08-10 14:55:34 +08:00
    >**TCP 并不知道所传输的字节流的含义,TCP 并不能保证接收方应用程序和发送方应用程序所发出的数据块具有对应大小的关系(这就是 TCP 传输过程中产生的粘包问题)。**但是应用程序接收方最终受到的字节流与发送方发送的字节流是一定相同的。因此,我们在使用 TCP 协议的时候应该制定合理的粘包拆包策略。

    没毛病,人家本来就知道知道了 TCP 是面向流的协议,也确实在讲#1 的分段问题,只是很多人不喜欢“粘包”这个词。
    hahastudio
        5
    hahastudio  
       2018-08-10 14:55:39 +08:00   ❤️ 13
    你猜为什么“ TCP 粘包”只有中文资料?
    alamaya
        6
    alamaya  
       2018-08-10 14:55:41 +08:00
    我还以为是来蹭热点的想不到内容这么一本正经
    mhycy
        7
    mhycy  
       2018-08-10 14:58:05 +08:00   ❤️ 1
    @wqyyy
    还是有毛病...
    这个解答显然这是面向 Socket API 学习 TCP 协议
    正常情况下对方接收的时候根本无法预测能一次性的从缓冲区读取到多少数据
    无法预测能读取到多少数据自然不可能有包的效果更不可能有后续的解释
    misaka19000
        8
    misaka19000  
       2018-08-10 15:00:15 +08:00   ❤️ 1
    TCP 是流,你想想看水流会产生粘在一起的情况吗?
    jedihy
        9
    jedihy  
       2018-08-10 15:03:27 +08:00 via iPhone
    这不叫粘包拆包。粘包特指 nagle 解决的问题。
    waruqi
        10
    waruqi  
       2018-08-10 15:13:43 +08:00
    看标题,一猜就知道是 java 系的。
    youxiachai
        11
    youxiachai  
       2018-08-10 15:24:53 +08:00   ❤️ 1
    看到粘包..就知道什么水平了...
    cholerae
        12
    cholerae  
       2018-08-10 16:18:13 +08:00   ❤️ 1
    都 8102 年了还有人讨论粘包
    qk3z
        13
    qk3z  
       2018-08-10 16:20:21 +08:00   ❤️ 1
    “ TCP 是一个“流”是一个协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连城一片的,其间并没有分界线。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包进行发送,这就是所谓的 TCP 粘包和拆包问题。” --- 《 netty 权威指南》

    楼上一堆嘲讽的,应该不是搞 java 的吧,心疼楼主。
    skinny
        14
    skinny  
       2018-08-10 16:24:34 +08:00   ❤️ 1
    @qk3z 然而你“粘”的这段话来自一个有“包”的中国人写的……
    skinny
        15
    skinny  
       2018-08-10 16:27:01 +08:00   ❤️ 1
    我猜有人会说我群嘲地图炮,然而我真的只在中文社区里看到过中国人说“粘包”这个词。
    qk3z
        16
    qk3z  
       2018-08-10 16:28:58 +08:00
    @skinny #15 可能都被这本书荼毒了吧 2333
    eastlhu
        17
    eastlhu  
       2018-08-10 16:30:23 +08:00 via iPhone   ❤️ 1
    TCP 所谓的粘包和拆包问题,是技术圈里最奇葩的问题之一。
    彻底解决 TCP 粘包和拆包问题的代码架构如下:

    char tmp[];Buffer buffer;// 网络循环:必须在一个循环中读取网络,因为网络数据是源源不断的。
    while(1){
    // 从 TCP 流中读取不定长度的一段流数据,不能保证读到的数据是你期望的长度
    tcp.read(tmp);
    // 将这段流数据和之前收到的流数据拼接到一起 buffer.append(tmp);
    // 解析循环:必须在一个循环中解析报文,避免所谓的粘包
    while(1){
    // 尝试解析报文
    msg = parse(buffer);
    if(!msg){
    // 报文还没有准备好,糟糕,我们遇到拆包了!跳出解析循环,继续读网络。
    break;
    } // 将解析过的报文对应的流数据清除 buffer.remove(msg.length); // 业务处理 process(msg); }}

    这段代码是终极地解决 TCP 粘包和拆包问题的代码!
    以上摘自某位大佬的博客
    eastlhu
        18
    eastlhu  
       2018-08-10 16:32:09 +08:00 via iPhone
    wysnylc
        19
    wysnylc  
       2018-08-10 16:43:55 +08:00
    @eastlhu #17 水流也是由水滴组成,水流只是水的运动形式本质上是水滴或者更小的水元素(麻瓜不懂魔法)
    那么把 TCP 比作水流(stream)自然也是由水滴(单个包)组成,上面说鄙视嘲笑粘包的脸肿不
    fgodt
        20
    fgodt  
       2018-08-10 16:44:57 +08:00
    写了几年 rtmp 了,第一次听说粘包
    怕不是我用了假的 tcp
    zjp
        21
    zjp  
       2018-08-10 16:51:05 +08:00 via Android
    有一点一直没明白,
    哪怕 TCP 没有缓冲机制,客户端发送多少,服务器就立马收到多少,也不能假设客户端将数据一次性全部发送。还是得处理数据被分段的问题。所以他们到底在介意 TCP 什么?
    Applenice
        22
    Applenice  
       2018-08-10 16:51:57 +08:00   ❤️ 1
    真让人孩怕,TCP 什么时候有包的概念了。。。
    mhycy
        23
    mhycy  
       2018-08-10 16:52:51 +08:00
    @wysnylc
    应用层无法简单获知 TCP 底层的包状态
    事实上在系统内核做拥塞控制的时候就已经解包到字节流缓冲区了
    既然给应用层的是字节流,那么自然不存在包的概念,更不可能粘起来。

    (注意:极端情况下一次 socket read 调用会只获得一个字节的数据返回)
    mhycy
        24
    mhycy  
       2018-08-10 17:03:14 +08:00
    @zjp
    面向 Socket API 学习 TCP 协议了解下...
    说白了还是基础不扎实...

    看了下那本《 Netty 权威指南》粘包相关部分的网络知识同样如此
    另: 这篇文章其实就是相关章节的重写
    petelin
        25
    petelin  
       2018-08-10 17:05:01 +08:00
    整个程序设计都有流的概念, 通过流, 我们有 map, reduce 这种处理大数据的基石, 我们有统一的访问网络, 本地文件, 甚至一切数据的范式, 你说的这种东西根本不是流的缺点, 这么简单的代码都写不好. 别当程序员了. 什么狗屁粘包, 你读取命令行数据也有命令行数据解析粘包问题?
    momocraft
        26
    momocraft  
       2018-08-10 17:05:27 +08:00   ❤️ 15
    - TCP 叫什么
    传输控制协议

    - TCP 保证什么
    一端写入的字节在被对方读到时是同样顺序

    - TCP 不保证什么
    一端写入的字节一定到达另一端
    到达另一端的字节一定有人读
    另一端读了这一端一定知道

    - TCP 需要理解上层协议吗
    不需要

    - 一个应用层协议的消息可能对应几个 TCP 包?
    不定

    - 一次 recv()读到的字节对应几个 TCP 包?
    不定

    - 如果以上几点我们可以达成共识,那对 TCP 及其 API 做不必要的假设,并发明奇特概念的人应该叫什么
    _____
    glacer
        27
    glacer  
       2018-08-10 17:05:42 +08:00
    @wysnylc TCP 本身就是要被切割成段才能封装成 ip 包发送,这里的粘包应该值得是业务表示的数据包,而 TCP 层并不关心业务数据如何组织,它的任务只是完整地交付数据罢了。所以这里对粘包拆包的处理应该属于应用层协议。
    Applenice
        28
    Applenice  
       2018-08-10 17:07:38 +08:00
    @mhycy 去看了下参考文章。。都是直接进了博客。。。
    zjp
        29
    zjp  
       2018-08-10 17:11:27 +08:00 via Android
    @mhycy 我的理解和你上面的说法一样…不觉得我的说法有错误,可以细说吗?
    mhycy
        30
    mhycy  
       2018-08-10 17:12:29 +08:00
    @zjp
    显然我的想法和你是一样的...
    mhycy
        31
    mhycy  
       2018-08-10 17:13:25 +08:00
    @zjp
    只是我把你不愿意说的话说出来了而已...
    zjp
        32
    zjp  
       2018-08-10 17:14:44 +08:00 via Android
    @mhycy 补充前面的表达"还是得处理数据被分段的问题。" > "还是得在应用层处理数据被分段的问题。"
    zjp
        33
    zjp  
       2018-08-10 17:17:43 +08:00 via Android
    @mhycy emmm …好吧。我并没有想这么说😂
    qk3z
        34
    qk3z  
       2018-08-10 17:18:40 +08:00   ❤️ 1
    @mhycy #24 我觉得作者还是比较谨慎的,所以书里说的是 “所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送”。然而楼上人一看 tcp 粘包就高潮了...
    xiao17174
        35
    xiao17174  
       2018-08-10 17:22:59 +08:00   ❤️ 7
    楼主不要被楼上阵容整齐的的嘲讽给吓到了.其实很多人都吃过这个亏的.
    楼主能够一本正经的整理出这么一大堆资料,也是一个认真的人.
    相信你能在别的事情上成功的.
    解释一下粘包,TCP 确实是没有粘包这个说法.之所以会有这个错觉产生,其实是由于自我脑补.
    很多人第一次入门网络编程,往往是发一个"helloworld"到另一端,对端大多数时候一次回调或者读取就收到了整个的"helloworld".自然地就以为发了一次,收了一次.就是一个数据包的传输完成了.
    这中间发生了什么呢?TCP 底层收到命令到要发送一块内存数据("helloworld"),那么发送端就一个一个字节(只是比喻,实际是 IP 数据帧)地向接收端发送.接收端也开始一个字节一个字节的接受.在发送前,两端没有任何的通信来约定这一次传输一共有多少长的数据.也就是说接收端在收到第一个字节的时候,并不知道接下来它还会收到剩余多少数据.
    那么问题就来了,接收端通知你有数据可读时,是以什么为依据呢?
    它的逻辑概括一下是(好久好久前的东西了,并不准确):1.收到了一些数据,并且在随后若干毫秒内就没有新数据了;2.数据缓存区满了;3.收到了其它的指令(重置,断开等).以上任意条件触发,那么就通知上层(你的程序)有 TCP 数据到来了.但是它本身并没有你所以为的另外一层含义----这是一个完整的数据包的到来.
    只要清楚 TCP 的这一本质,那么你在写代码时就要知道,当你被通知有数据可以接收时,此刻能读取的这些数据本身并没有任何意义,它可能是某个完整数据包的一部分,也可能就是一个完整的数据包.只有你自己亲自去拆开(解析)它后才能判断.
    mhycy
        36
    mhycy  
       2018-08-10 17:26:39 +08:00
    @qk3z
    前提:业务无法获知 TCP 的底层分包情况

    既然无法获知,那么这句“业务上认为”是不存在的,因为业务不能对流式协议的底层做任何假设

    而且书上认为 TCP 最终发包的时候会被如何分片的估计情况并不完整
    毕竟 TCP 作为一个流式传输协议,在网络传递过程中被中间设备如何重组数据包都是不确定的
    vy0b0x
        37
    vy0b0x  
       2018-08-10 17:27:33 +08:00
    安利一个 c#的 System.IO.Pipelines 在这方面贼好用
    lolizeppelin
        38
    lolizeppelin  
       2018-08-10 17:29:55 +08:00 via Android   ❤️ 1
    中文造了个容易理解的名词而已
    能解决问题就好
    非要上纲上线干啥
    xiao17174
        39
    xiao17174  
       2018-08-10 17:29:57 +08:00   ❤️ 1
    另外补充一下,楼主图中的三种粘包情况的示意图,显然是一个半吊子人画的,1 和 2 还能理解是站在应用层看待 tcp 传输的,而情况 3 是不可能在应用层发生的.
    情况 3 的确是 tcp 传输过程中会遇到的,但如果要讨论,这情况就多了去了:丢包,重包,乱序包.tcp 本身都已经把这些都处理掉了,所以在应用层,要么能接收到正确的数据,要么就是读不到数据.不会有情况 3 出现的.
    zhujinliang
        40
    zhujinliang  
       2018-08-10 17:57:39 +08:00
    @wysnylc 水滴对应字节,所谓的包,可以理解为每连续 n 滴水滴。
    或者说,先滴 1 滴染成红色的水,后滴 1 滴染成蓝色的水,你在水管另一端能恰好地分隔开红色液体和蓝色液体吗
    dbw9580
        41
    dbw9580  
       2018-08-10 18:06:18 +08:00 via Android
    我觉得这只是看待问题的角度不同。发明“粘包”这个词的人,站在应用开发的角度,对网络通信的心理模型是一端发送一次,另一端就接受一次,并且完整地接受所有发送的内容,这一批数据就是“包”。这在处理高层业务逻辑的时候是很合理也自然的模型。然而底层的 TCP 的行为并不符合这个模型,所以用这个模型的话来说就是发生了“粘包”。

    换个例子,做操作系统的人让进程间内存隔离,做应用的人不明白隔离的用意,就会把“进程间通信”讲成“打破隔离”之类的了。
    dbw9580
        42
    dbw9580  
       2018-08-10 18:14:39 +08:00 via Android   ❤️ 1
    “粘包”这个词错就错在重用了“包”这个词,而“包”已经用于描述 IP 数据包了。并非不能重用,而是这样重用同一个领域(计算机网络)甚至同一个话题(进程间通信)下的名词,会导致不必要的语义含混。
    e8c47a0d
        43
    e8c47a0d  
       2018-08-10 18:29:15 +08:00   ❤️ 2
    最近很火的“碰包”了解一下
    DOLLOR
        44
    DOLLOR  
       2018-08-10 18:38:06 +08:00 via Android
    “粘包”有对应的英文概念么?
    gamexg
        45
    gamexg  
       2018-08-10 18:41:26 +08:00
    社区氛围有些不好。
    老人都知道这个怎么处理,但是新人的确会碰到这个问题,有人愿意介绍是挺好的事情。
    inpro
        46
    inpro  
       2018-08-10 19:28:59 +08:00 via iPhone
    如果粘包是伪概念,那处理粘包的代码在工作时起到的是什么作用呢?如果没有它们,那些之前用粘包代码的程序可以正常运行吗?
    kernel
        47
    kernel  
       2018-08-10 19:44:39 +08:00
    @gamexg 什么叫新人的确会碰到这个问题,用 TCP 编程本来就是这么写的,你还有别的写法吗,还专门给这个起个名词,简直了
    palfortime
        48
    palfortime  
       2018-08-10 19:57:53 +08:00 via Android
    这不是楼主自己定义了应用层协议吗?和 tcp 根本无关,粘也是应用层粘在一起而已。
    huskar
        49
    huskar  
       2018-08-10 20:11:57 +08:00
    @gamexg 提出“粘包”这种词就是在误导新人。tcp 流本身是非常容易理解的概念,新人也不可能理解不了。“粘包”这种说法反倒容易让新手产生混淆。
    zjp
        50
    zjp  
       2018-08-10 20:18:40 +08:00 via Android
    @e8c47a0d 😂这自黑我给满分
    realpg
        51
    realpg  
       2018-08-10 21:11:54 +08:00
    强行用自己方便理解的名词重新定义了 TCP 编程……
    jianpanxia
        52
    jianpanxia  
       2018-08-10 23:46:57 +08:00
    @qk3z #13 捧着一本现时都不是经典(更别说经历时间检验)的书当圣经?
    zado
        53
    zado  
       2018-08-10 23:51:15 +08:00   ❤️ 1
    "粘包"是 TCP 编程经常遇到的问题,专门取个名词也没什么不好,通俗形象,英文造词恐怕是没这么方便,所有就没有专门的词来特指这个问题。
    d18
        54
    d18  
       2018-08-10 23:54:28 +08:00   ❤️ 1
    这论坛戾气可真重,恐怖。
    tsui
        55
    tsui  
       2018-08-11 00:20:32 +08:00 via iPhone
    昨天发明一个“ TCP 碰包”,今天来了个详解“粘包”
    该开个民科网络 tag 了
    zhujinliang
        56
    zhujinliang  
       2018-08-11 00:44:01 +08:00 via iPhone
    @inpro 是建立在 TCP 之上的上层协议。肯定要添加一些信息以区分每条消息的边界,且需要做缓冲区缓存不够一条完整消息的部分
    你说的“如果没有它们”,不知是指什么。“避免粘包“代码必定服务端和客户端同时使用,事实上已经是一个自创协议了,一端使用而另一端不使用则无法通信
    iceheart
        57
    iceheart  
       2018-08-11 00:47:33 +08:00 via Android
    "粘包","惊群" 都是二把刀搞出来的名词。根源在于还没搞清楚特定技术的打开姿势,就自以为懂了,出状况了又搞不清原因,就自己给起个名字。然后被其他的二把刀传播...
    PhxNirvana
        58
    PhxNirvana  
       2018-08-11 01:24:57 +08:00
    点了一下计算机网络,连接居然是博客首页。。是想表达这个博客是谢希仁的么。。
    Tyanboot
        59
    Tyanboot  
       2018-08-11 01:33:03 +08:00   ❤️ 1
    @iceheart 忍不住搜了下什么是"惊群", 看了下真是惊了, 这帮人怕不是不知道 pthread_cond_* 和 condition_variable.
    luozic
        60
    luozic  
       2018-08-11 01:33:35 +08:00 via iPhone
    应用层数据使用 HTTP 协议走 tcp/ip 分包传输过程中出现了需要使用缓冲区或者异步轮询的方式才能完整反解出来。 你上层的事情为啥要管下层的事? 尼玛用数据库的时候是不是还要骂为啥连接资源就那么点,为啥不能开个几十万个?
    缓冲和缓存本来就是处理时间有差异的不同层级东西的架构最优选择。
    bombless
        61
    bombless  
       2018-08-11 02:24:57 +08:00 via Android
    @zjp 一楼已经说了。其实就是把字节流拆成一段段的,可以理解成是自己在设计应用层协议(或者更一般的说,设计一个在字节流服务上跑的协议)
    binarylu
        62
    binarylu  
       2018-08-11 08:57:23 +08:00
    看来大家讨论那么多,感觉问题主要是:
    1. 楼主问题描述有问题,TCP 包确实是错误概念,楼主想表达的应该是 走 TCP 协议的应用层包
    2. 大家纠结在 TCP 包这个错误概念上,没有理解楼主的真实想法。
    个人观点:
    这个问题还是要处理的,TCP 并不知道应用层包的边界,recv 到的数据还是有可能包含一部分上一个(应用层)包尾和一部分下一个(应用层)包头。

    附上一些国外网友的讨论:

    "If you want to send and receive "packets" with TCP, you have to implement packet start markers, length markers and so on. How about using a message oriented protocol like UDP instead? UDP guarantees one send() call translates to one sent datagram and to one recv() call!"
    ref: https://gamedev.stackexchange.com/questions/96945/what-is-better-lots-of-small-tcp-packets-or-one-long-one

    "If you stick with SOCK_STREAM, then your receiving code needs to be prepared for the fact that a single call to recv may not retrieve a whole message as sent by the sender. recv will return the number of bytes actually received, or -1 if there is an error. If the messages are fixed length then you can repeatedly call it until you have the whole message."
    ref: https://stackoverflow.com/questions/24051965/maximum-limit-on-size-of-data-in-ipc-using-sockets-in-unix
    urmyfaith
        64
    urmyfaith  
       2018-08-11 10:45:55 +08:00   ❤️ 1
    有啥可喷的?

    编程的遇到的问题,命名了一个新词而已。

    就比如外国人第一次用筷子吃饺子,他发明一个新词,叫 “筷饺现象”,然后在他们国家内讨论的火热。

    这个时候,你看到中国会讨论这个现象吗 ?

    只是遇到问题,总结一下前人的经验。上纲上线,好好讨论问题不行,有人基础差,或者说你水平高,你耐心评论一下,多指导下不就得了,何必冷嘲热讽?
    bombless
        65
    bombless  
       2018-08-11 11:10:30 +08:00 via Android
    @urmyfaith 主要是看到不懂的人出来传教确实挺尴尬的。可以看出来 po 主没理解 tcp/ip 的多层设计到底是怎么回事。

    说起来最近小火了的一个 golang 程序员对归档文件和 jar 的理解也是错的,然后还
    bombless
        66
    bombless  
       2018-08-11 11:10:49 +08:00 via Android
    跑出来教人
    pangliang
        67
    pangliang  
       2018-08-11 11:32:30 +08:00
    要说本质问题, 本质问题是 tcp 一个流协议却给了一个"包"式的接口:
    实际长度=read(缓冲区, 最大长度)
    newtype0092
        68
    newtype0092  
       2018-08-11 11:33:42 +08:00
    @eastlhu 上大学学的计算机网络,TCP 接数据就这么写的,毕业后接触到粘包的概念,总以为自己没搞懂 TCP,感叹自己没好好学。。。
    zn
        69
    zn  
       2018-08-11 11:48:08 +08:00   ❤️ 1
    先搞清楚什么叫 TCP 吧,还神他妈沾包。

    其实就是自己基于 TCP 传输的自定义协议的解析问题。

    *是*你*自*己*协*议*的*解*析*问*题*,懂了吗?

    跟 TCP 一根毛的关系都没有。
    pangliang
        70
    pangliang  
       2018-08-11 11:51:34 +08:00   ❤️ 2
    实际长度=read(缓冲区, 最大长度) 底层就这个接口,
    在这段代码层面, 每次 read 完了, "缓冲区" 就是这一段代码这个层面的"包"
    这个层面的"包" 跟应用层面的 "包" 不一一对应, 所以出现 两个层面的"包" 的拆和 粘的问题

    就算 tcp 是个流, 你有一堆高级特性的 api, 你就骄傲了?
    永远还是会遇到 2 个层面的包无法一一对应的问题
    硬件缓冲区是不是"包" ? 现在硬件缓冲区能当"流"对待了? 总线不也还是一次按总线位数取"一块"?

    拿着一堆高级特性 api 在嘲笑底层特性的 api 用法, 你们到底是在骄傲个啥?
    crayygy
        71
    crayygy  
       2018-08-11 11:51:37 +08:00 via iPhone
    @newtype0092 多看英文书...所谓的博客有很多都是臆测,甚至抄袭,所以有些错误的表述就这样被广泛传播
    yingtl
        72
    yingtl  
       2018-08-11 12:11:32 +08:00
    @binarylu 所以说应用层使用 TCP 判应用层数据是否完整只有以下几个办法
    1. HTTP 1.1 类似的, 用 \r\n\r\n 特定标示表示结尾, 但处理起来略麻烦, HTTP 2.0 就改进了
    2. 加包头
    2.1 接收时先收个固定长度包头,包头指明的包体长度再接收包体, 这样会多一次调用, 高性能场合会顾及这个性能损失
    2.2 直接接收一大块数据, 然后再根据里面的包头信息进行解析, 处理起来有点麻烦
    zhicheng
        73
    zhicheng  
       2018-08-11 12:31:54 +08:00
    本来不想回复的,但看到有人误人子弟误得更深了,回复一下:

    如果你觉得 read(缓冲区,长度)这个读的是一个 “包” , 那么请问我要传送一个 1TB 的数据包,应该怎么做?发送端和接收端各初始化一个 1TB 大小的缓冲区吗?另外接收端怎么知道对方要发一个 1TB 的数据包?还有我的机器是 32 位的怎么办?

    经常写代码的工程师,即使没接触过流式协议也会很快想到这个问题,而这个问题的答案,随手就能找到。你们讨论的 “新手” 和 “老手” 其实是 “完全不懂网络编程的” 和 “懂网络编程的” 或者 “完全不懂编程硬要装着自己懂的” 和 “这个问题我不懂要看下书” 的两种人。
    timothyqiu
        74
    timothyqiu  
       2018-08-11 12:43:26 +08:00
    我一般把一本正经讨论「粘包问题」的情况理解为不肯好好看文档、面向直觉编程。
    pangliang
        75
    pangliang  
       2018-08-11 12:43:36 +08:00
    @zhicheng 你到底拿上层高特性的 api 在较什么劲?
    底层能跑的开这种方式收数据吗? 动不动流式协议? 你硬件缓冲区是流式的吗? 你硬件缓冲区有 1TB 吗?

    拿着一堆流式协议高级的 api, 然后说"不存在拆和粘的问题, 因为我是流式的" ?
    zhicheng
        76
    zhicheng  
       2018-08-11 12:46:21 +08:00 via iPhone
    @pangliang 你回错贴子了,这个贴子讨论的是 TCP 协议,TCP 协议就是流式协议。
    pangliang
        77
    pangliang  
       2018-08-11 12:49:32 +08:00
    @zhicheng 楼主的题目是: (用)tcp(做业务会有)粘包问题

    然后一堆人在教楼主 tcp 是流式协议...
    zhicheng
        78
    zhicheng  
       2018-08-11 12:52:09 +08:00 via iPhone
    @pangliang 因为 TCP 是流式协议,所以没有包的概念,更没有粘包的概念。你到底要表达的是什么?
    pangliang
        79
    pangliang  
       2018-08-11 12:57:03 +08:00
    @zhicheng
    楼主说的是: (用)tcp(这种流协议)(实现业务层的包协议) 会有粘(业务层的)包问题.

    所以你们一再强调 tcp 没有包是为啥? 楼主什么时候说 tcp 有包了?

    楼主帖子里, "为什么会发生 TCP 粘包、拆包" 说的清清楚楚, 哪一句说 tcp 包了?
    zhicheng
        80
    zhicheng  
       2018-08-11 13:07:59 +08:00 via iPhone
    @pangliang 既然没有包,那你又拆什么和粘什么?加深别人的错误印象?
    pangliang
        81
    pangliang  
       2018-08-11 13:26:40 +08:00
    @zhicheng

    (用)tcp(缓冲区读取的使用方式)(实现业务层的包协议) 会有(被缓冲区)粘(业务层的)包问题.
    这样够确切了吗?
    理解楼主要表达的意思就好了, 拿一些不相关的东西喷楼主是为啥呢?
    pangliang
        82
    pangliang  
       2018-08-11 13:31:49 +08:00
    @zhicheng
    我同意 "tcp 这种流协议就应该用流式 api"
    我也同意 "2018 年了不应该还在讨论粘包"

    但是我不同意 "tcp 不存在这个问题"
    因为一开始根本没有 流式 api

    所以不要拿着 2018 年的 api 去嘲笑 2000 年的 api
    bombless
        83
    bombless  
       2018-08-11 13:33:02 +08:00 via Android
    @pangliang 你没看到 po 主的图吗,你没发现这几张图就是瞎扯吗
    zhicheng
        84
    zhicheng  
       2018-08-11 13:41:18 +08:00 via iPhone
    @pangliang 明明就是解释一下为什么

    一次 send 可能要多次 recv 才能完成



    多次 send 一次 recv 也可能能完成

    的简单问题。却“发明”一个新手不懂老手也不懂的名词,别人指出来了还要强辩。
    zhicheng
        85
    zhicheng  
       2018-08-11 13:45:42 +08:00 via iPhone
    @pangliang 说真的我不知道你到底在讨论什么,2000 年的时候 TCP 或者说 socket 接口也是这样的,如果你硬要强辩,那我告诉你最底层的数据是流式的,那个东西叫做 “电流” 。
    zhicheng
        86
    zhicheng  
       2018-08-11 13:46:53 +08:00 via iPhone
    @pangliang 突然意识到你可能是写 Java 的,算了,不解释了。
    bombless
        87
    bombless  
       2018-08-11 13:48:34 +08:00 via Android
    @zhicheng 跟 tcp 本身也没太大关系,设计文件格式的时候也是一回事……

    而且作者怎么没想到如果他想的没错,两个包为什么不会调换顺序,哈哈哈
    pangliang
        88
    pangliang  
       2018-08-11 13:51:19 +08:00   ❤️ 1
    @bombless
    第三张图?
    Data 理解成是一个业务层定义的包 没问题啊
    tcp 是流式, 但是它也不完全是像水一样的,
    水的最小流动单位是 "一个原子??"
    tcp 的"流动"最小单位是 "报文", 也还是"一块"

    Data 跟 报文的大小不一致, 那么报文到达缓冲区之后
    某个特定时刻 缓冲区内部 Data 的个数和完整性是不确定的
    所以你如果按缓冲区 "整个" 来读, 那 Data 就肯定不完整

    这个根本不是 tcp 是不是流式的问题, 而是 缓冲区把 流 "块" 化了

    所以关键是使用缓冲区这个东西的方式
    按"块"来用 "缓冲区" 就有问题
    如果用流式来用缓冲区就没问题
    bombless
        89
    bombless  
       2018-08-11 14:01:17 +08:00 via Android
    @pangliang 看来你跟作者观点一样……我简单解释一下吧,你只是在读一个字节流,根本不应该关注你每次读了多少字节。操作系统就可以把邻接的一大串数据一次发给你,也可以每次发你一个字节让你慢慢读。因此你的 recv 每次读多少数据跟网络传输情况并没用关联,一般来说你也不应该关注,更重要的是不能因此影响你正确解析你自己设计的应用层协议。

    从另一个方向解释,比如说你从 unix 套接字 recv,这时候是单机的,这时你从套接字读数据就完全是操作系统和往里写数据那一方决定了。

    你仔细想想,你读写大文件的时候是不是也是一次不一定读完可能要读多次。
    iwtbauh
        90
    iwtbauh  
       2018-08-11 14:03:19 +08:00 via Android
    @pangliang #67

    你这个说法完全是扯谈

    什么时候有过这个接口了?
    实际长度=read(缓冲区, 最大长度)
    socket 接口可是这样的
    读取到的长度=read(文件描述符, 缓冲区, 缓冲区长度)

    文件描述符就是一个流的引用,你想从这个流里面读多少字节,就设置多大的缓冲区长度。

    否则流的底层接口你说怎么设计啊。

    现在明白 Unix 的最终设计思想“一切都是文件”的含义了吧:“一切都是文件”就是在暗示“一切都是字节流”
    pangliang
        91
    pangliang  
       2018-08-11 14:07:06 +08:00
    @iwtbauh 你说的接口跟我的没区别, 我的最大长度说的就是 缓冲区长度, 实际长度 就是 read 到的"实际"长度
    pangliang
        92
    pangliang  
       2018-08-11 14:08:39 +08:00
    @iwtbauh 那你有没有想过, 为啥 这个接口 是
    读取到的长度=read(文件描述符, 缓冲区, 缓冲区长度)
    它为何有个 读取到的长度 ?
    而不是
    read(文件描述符, 缓冲区, 我就要这么长不读够不返回) ?
    pangliang
        93
    pangliang  
       2018-08-11 14:10:09 +08:00
    @bombless 我为啥不关注啊?
    读取到的长度=read(文件描述符, 缓冲区, 缓冲区长度)
    "读取到的长度" 跟 我想要的长度不一定一样啊
    bombless
        94
    bombless  
       2018-08-11 14:10:12 +08:00 via Android
    @pangliang 不返回占用的是操作系统自己的内存,这个对操作系统稳定性是个伤害
    bombless
        95
    bombless  
       2018-08-11 14:11:32 +08:00 via Android
    @pangliang 那是为了最终拼接起来,你只应该关注拼接到什么进度了,不应该关注拼接了几次。这就是我说的你不应该关注每次调用返回了多少数据
    iwtbauh
        96
    iwtbauh  
       2018-08-11 14:11:46 +08:00 via Android
    @pangliang #91

    你的#67 原话:“本质问题是 tcp 一个流协议却给了一个"包"式的接口”

    我的意思:你在故意模糊接口参数的含义(最大长度和缓冲区大小看起来区别不大,但是表达的是不同的思想),故意隐藏掉“文件描述符”,来试图说明 read 这个接口“是包式接口“

    但其实是恰恰相反
    pangliang
        97
    pangliang  
       2018-08-11 14:11:53 +08:00
    @bombless 我已经说了, 问题不是 tcp, 而是缓冲区
    pangliang
        98
    pangliang  
       2018-08-11 14:13:21 +08:00
    @bombless 你自己都说要拼接了, 为啥要拼接? 拆了才要接啊
    bombless
        99
    bombless  
       2018-08-11 14:14:15 +08:00 via Android
    @pangliang 看来你懂了,那你明白我为什么说 po 主放的图是错的了吧。他对整个问题都误解了
    bombless
        100
    bombless  
       2018-08-11 14:15:23 +08:00 via Android
    @pangliang 是字节流啊,说了多少次,不是拆了,是字节流只能这么设计接口,要不你怎么设计。怎么就拆了呢
    1  2  
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5861 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 02:37 · PVG 10:37 · LAX 18:37 · JFK 21:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.