原文地址: haifeiWu 的博客
博客地址:www.hchstudio.cn
欢迎转载,转载请注明作者及出处,谢谢!
最近一直在做中间件相关的东西,所以接触到的各种协议比较多,总的来说有 TCP,UDP,HTTP 等各种网络传输协议,因此楼主想先从协议最基本的 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 的协议传输的整个过程:
下面这个图是从老钱的博客里面取到的,非常生动
如下图所示,出现的粘包问题一共有三种情况
第一种情况: 如上图中的第一根bar所示,服务端一共读到两个数据包,每个数据包都是完成的,并没有发生粘包的问题,这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,每次服务端读取到的消息都是完成的,并不会出现数据不正确的情况。
第二种情况: 服务端仅收到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于第一种情况的逻辑实现的服务端就蒙了,因为服务端并不能很好的处理这个数据包,甚至不能处理,这种情况其实就是 TCP 的粘包问题。
第三种情况: 服务端收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了 TCP 拆包问题,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。
为什么会发生 TCP 粘包、拆包
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包。
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包。
进行 MSS (最大报文长度)大小的 TCP 分段,当 TCP 报文长度-TCP 头部长度>MSS 的时候将发生拆包。
接收方法不及时读取套接字缓冲区数据,这将发生粘包。
如何处理粘包、拆包
通常会有以下一些常用的方法:
使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
设置定长消息,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符。
设置消息边界,服务端从网络流中按消息编辑分离出消息内容,一般使用‘\n ’。
更为复杂的协议,例如楼主最近接触比较多的车联网协议 808,809 协议。
下面代码楼主主要演示了使用规定消息头,消息体的方式来解决 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();
}
}
}
}
最近一直在写一些框架性的博客,专门针对某些问题进行原理性的技术探讨的博客还比较少,所以楼主想着怎样能在自己学到东西的同时也可以给一同在技术这条野路子上奋斗的小伙伴们一些启发,是楼主一直努力的方向。
1
mhycy 2018-08-10 14:46:22 +08:00 34
提醒:TCP 是流传输协议
> 传输控制协议(英语:Transmission Control Protocol,缩写为 TCP ) > 是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。 > (中文维基百科 TCP 词条 第一句) 理解了这个本质,那么**粘包**就不是一个问题 因为本来就没有**包**的概念,自然没有**粘**的可能性,更没有**粘包**这一说法 本质问题:如何在流传输协议上构造具有**可靠**分段特性的(包 /报文 /数据段)传输协议 |
2
catror 2018-08-10 14:49:54 +08:00 via Android 5
TCP 没有包的概念,不要再普及错误的知识了。这里所谓的包是构建在 TCP 之上的另一种协议的概念。
|
3
chinawrj 2018-08-10 14:54:22 +08:00 2
TCP 是流。。。。
另外粘包定义好奇葩。哈哈 |
4
wqyyy 2018-08-10 14:55:34 +08:00
>**TCP 并不知道所传输的字节流的含义,TCP 并不能保证接收方应用程序和发送方应用程序所发出的数据块具有对应大小的关系(这就是 TCP 传输过程中产生的粘包问题)。**但是应用程序接收方最终受到的字节流与发送方发送的字节流是一定相同的。因此,我们在使用 TCP 协议的时候应该制定合理的粘包拆包策略。
没毛病,人家本来就知道知道了 TCP 是面向流的协议,也确实在讲#1 的分段问题,只是很多人不喜欢“粘包”这个词。 |
5
hahastudio 2018-08-10 14:55:39 +08:00 13
你猜为什么“ TCP 粘包”只有中文资料?
|
6
alamaya 2018-08-10 14:55:41 +08:00
我还以为是来蹭热点的想不到内容这么一本正经
|
7
mhycy 2018-08-10 14:58:05 +08:00 1
@wqyyy
还是有毛病... 这个解答显然这是面向 Socket API 学习 TCP 协议 正常情况下对方接收的时候根本无法预测能一次性的从缓冲区读取到多少数据 无法预测能读取到多少数据自然不可能有包的效果更不可能有后续的解释 |
8
misaka19000 2018-08-10 15:00:15 +08:00 1
TCP 是流,你想想看水流会产生粘在一起的情况吗?
|
9
jedihy 2018-08-10 15:03:27 +08:00 via iPhone
这不叫粘包拆包。粘包特指 nagle 解决的问题。
|
10
waruqi 2018-08-10 15:13:43 +08:00
看标题,一猜就知道是 java 系的。
|
11
youxiachai 2018-08-10 15:24:53 +08:00 1
看到粘包..就知道什么水平了...
|
12
cholerae 2018-08-10 16:18:13 +08:00 1
都 8102 年了还有人讨论粘包
|
13
qk3z 2018-08-10 16:20:21 +08:00 1
“ TCP 是一个“流”是一个协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连城一片的,其间并没有分界线。TCP 底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包进行发送,这就是所谓的 TCP 粘包和拆包问题。” --- 《 netty 权威指南》
楼上一堆嘲讽的,应该不是搞 java 的吧,心疼楼主。 |
15
skinny 2018-08-10 16:27:01 +08:00 1
我猜有人会说我群嘲地图炮,然而我真的只在中文社区里看到过中国人说“粘包”这个词。
|
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 粘包和拆包问题的代码! 以上摘自某位大佬的博客 |
18
eastlhu 2018-08-10 16:32:09 +08:00 via iPhone
|
19
wysnylc 2018-08-10 16:43:55 +08:00
@eastlhu #17 水流也是由水滴组成,水流只是水的运动形式本质上是水滴或者更小的水元素(麻瓜不懂魔法)
那么把 TCP 比作水流(stream)自然也是由水滴(单个包)组成,上面说鄙视嘲笑粘包的脸肿不 |
20
fgodt 2018-08-10 16:44:57 +08:00
写了几年 rtmp 了,第一次听说粘包
怕不是我用了假的 tcp |
21
zjp 2018-08-10 16:51:05 +08:00 via Android
有一点一直没明白,
哪怕 TCP 没有缓冲机制,客户端发送多少,服务器就立马收到多少,也不能假设客户端将数据一次性全部发送。还是得处理数据被分段的问题。所以他们到底在介意 TCP 什么? |
22
Applenice 2018-08-10 16:51:57 +08:00 1
真让人孩怕,TCP 什么时候有包的概念了。。。
|
23
mhycy 2018-08-10 16:52:51 +08:00
@wysnylc
应用层无法简单获知 TCP 底层的包状态 事实上在系统内核做拥塞控制的时候就已经解包到字节流缓冲区了 既然给应用层的是字节流,那么自然不存在包的概念,更不可能粘起来。 (注意:极端情况下一次 socket read 调用会只获得一个字节的数据返回) |
24
mhycy 2018-08-10 17:03:14 +08:00
|
25
petelin 2018-08-10 17:05:01 +08:00
整个程序设计都有流的概念, 通过流, 我们有 map, reduce 这种处理大数据的基石, 我们有统一的访问网络, 本地文件, 甚至一切数据的范式, 你说的这种东西根本不是流的缺点, 这么简单的代码都写不好. 别当程序员了. 什么狗屁粘包, 你读取命令行数据也有命令行数据解析粘包问题?
|
26
momocraft 2018-08-10 17:05:27 +08:00 15
- TCP 叫什么
传输控制协议 - TCP 保证什么 一端写入的字节在被对方读到时是同样顺序 - TCP 不保证什么 一端写入的字节一定到达另一端 到达另一端的字节一定有人读 另一端读了这一端一定知道 - TCP 需要理解上层协议吗 不需要 - 一个应用层协议的消息可能对应几个 TCP 包? 不定 - 一次 recv()读到的字节对应几个 TCP 包? 不定 - 如果以上几点我们可以达成共识,那对 TCP 及其 API 做不必要的假设,并发明奇特概念的人应该叫什么 _____ |
27
glacer 2018-08-10 17:05:42 +08:00
@wysnylc TCP 本身就是要被切割成段才能封装成 ip 包发送,这里的粘包应该值得是业务表示的数据包,而 TCP 层并不关心业务数据如何组织,它的任务只是完整地交付数据罢了。所以这里对粘包拆包的处理应该属于应用层协议。
|
34
qk3z 2018-08-10 17:18:40 +08:00 1
@mhycy #24 我觉得作者还是比较谨慎的,所以书里说的是 “所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送”。然而楼上人一看 tcp 粘包就高潮了...
|
35
xiao17174 2018-08-10 17:22:59 +08:00 7
楼主不要被楼上阵容整齐的的嘲讽给吓到了.其实很多人都吃过这个亏的.
楼主能够一本正经的整理出这么一大堆资料,也是一个认真的人. 相信你能在别的事情上成功的. 解释一下粘包,TCP 确实是没有粘包这个说法.之所以会有这个错觉产生,其实是由于自我脑补. 很多人第一次入门网络编程,往往是发一个"helloworld"到另一端,对端大多数时候一次回调或者读取就收到了整个的"helloworld".自然地就以为发了一次,收了一次.就是一个数据包的传输完成了. 这中间发生了什么呢?TCP 底层收到命令到要发送一块内存数据("helloworld"),那么发送端就一个一个字节(只是比喻,实际是 IP 数据帧)地向接收端发送.接收端也开始一个字节一个字节的接受.在发送前,两端没有任何的通信来约定这一次传输一共有多少长的数据.也就是说接收端在收到第一个字节的时候,并不知道接下来它还会收到剩余多少数据. 那么问题就来了,接收端通知你有数据可读时,是以什么为依据呢? 它的逻辑概括一下是(好久好久前的东西了,并不准确):1.收到了一些数据,并且在随后若干毫秒内就没有新数据了;2.数据缓存区满了;3.收到了其它的指令(重置,断开等).以上任意条件触发,那么就通知上层(你的程序)有 TCP 数据到来了.但是它本身并没有你所以为的另外一层含义----这是一个完整的数据包的到来. 只要清楚 TCP 的这一本质,那么你在写代码时就要知道,当你被通知有数据可以接收时,此刻能读取的这些数据本身并没有任何意义,它可能是某个完整数据包的一部分,也可能就是一个完整的数据包.只有你自己亲自去拆开(解析)它后才能判断. |
36
mhycy 2018-08-10 17:26:39 +08:00
@qk3z
前提:业务无法获知 TCP 的底层分包情况 既然无法获知,那么这句“业务上认为”是不存在的,因为业务不能对流式协议的底层做任何假设 而且书上认为 TCP 最终发包的时候会被如何分片的估计情况并不完整 毕竟 TCP 作为一个流式传输协议,在网络传递过程中被中间设备如何重组数据包都是不确定的 |
37
vy0b0x 2018-08-10 17:27:33 +08:00
安利一个 c#的 System.IO.Pipelines 在这方面贼好用
|
38
lolizeppelin 2018-08-10 17:29:55 +08:00 via Android 1
中文造了个容易理解的名词而已
能解决问题就好 非要上纲上线干啥 |
39
xiao17174 2018-08-10 17:29:57 +08:00 1
另外补充一下,楼主图中的三种粘包情况的示意图,显然是一个半吊子人画的,1 和 2 还能理解是站在应用层看待 tcp 传输的,而情况 3 是不可能在应用层发生的.
情况 3 的确是 tcp 传输过程中会遇到的,但如果要讨论,这情况就多了去了:丢包,重包,乱序包.tcp 本身都已经把这些都处理掉了,所以在应用层,要么能接收到正确的数据,要么就是读不到数据.不会有情况 3 出现的. |
40
zhujinliang 2018-08-10 17:57:39 +08:00
@wysnylc 水滴对应字节,所谓的包,可以理解为每连续 n 滴水滴。
或者说,先滴 1 滴染成红色的水,后滴 1 滴染成蓝色的水,你在水管另一端能恰好地分隔开红色液体和蓝色液体吗 |
41
dbw9580 2018-08-10 18:06:18 +08:00 via Android
我觉得这只是看待问题的角度不同。发明“粘包”这个词的人,站在应用开发的角度,对网络通信的心理模型是一端发送一次,另一端就接受一次,并且完整地接受所有发送的内容,这一批数据就是“包”。这在处理高层业务逻辑的时候是很合理也自然的模型。然而底层的 TCP 的行为并不符合这个模型,所以用这个模型的话来说就是发生了“粘包”。
换个例子,做操作系统的人让进程间内存隔离,做应用的人不明白隔离的用意,就会把“进程间通信”讲成“打破隔离”之类的了。 |
42
dbw9580 2018-08-10 18:14:39 +08:00 via Android 1
“粘包”这个词错就错在重用了“包”这个词,而“包”已经用于描述 IP 数据包了。并非不能重用,而是这样重用同一个领域(计算机网络)甚至同一个话题(进程间通信)下的名词,会导致不必要的语义含混。
|
43
e8c47a0d 2018-08-10 18:29:15 +08:00 2
最近很火的“碰包”了解一下
|
44
DOLLOR 2018-08-10 18:38:06 +08:00 via Android
“粘包”有对应的英文概念么?
|
45
gamexg 2018-08-10 18:41:26 +08:00
社区氛围有些不好。
老人都知道这个怎么处理,但是新人的确会碰到这个问题,有人愿意介绍是挺好的事情。 |
46
inpro 2018-08-10 19:28:59 +08:00 via iPhone
如果粘包是伪概念,那处理粘包的代码在工作时起到的是什么作用呢?如果没有它们,那些之前用粘包代码的程序可以正常运行吗?
|
47
kernel 2018-08-10 19:44:39 +08:00
@gamexg 什么叫新人的确会碰到这个问题,用 TCP 编程本来就是这么写的,你还有别的写法吗,还专门给这个起个名词,简直了
|
48
palfortime 2018-08-10 19:57:53 +08:00 via Android
这不是楼主自己定义了应用层协议吗?和 tcp 根本无关,粘也是应用层粘在一起而已。
|
49
huskar 2018-08-10 20:11:57 +08:00
@gamexg 提出“粘包”这种词就是在误导新人。tcp 流本身是非常容易理解的概念,新人也不可能理解不了。“粘包”这种说法反倒容易让新手产生混淆。
|
51
realpg 2018-08-10 21:11:54 +08:00
强行用自己方便理解的名词重新定义了 TCP 编程……
|
52
jianpanxia 2018-08-10 23:46:57 +08:00
@qk3z #13 捧着一本现时都不是经典(更别说经历时间检验)的书当圣经?
|
53
zado 2018-08-10 23:51:15 +08:00 1
"粘包"是 TCP 编程经常遇到的问题,专门取个名词也没什么不好,通俗形象,英文造词恐怕是没这么方便,所有就没有专门的词来特指这个问题。
|
54
d18 2018-08-10 23:54:28 +08:00 1
这论坛戾气可真重,恐怖。
|
55
tsui 2018-08-11 00:20:32 +08:00 via iPhone
昨天发明一个“ TCP 碰包”,今天来了个详解“粘包”
该开个民科网络 tag 了 |
56
zhujinliang 2018-08-11 00:44:01 +08:00 via iPhone
@inpro 是建立在 TCP 之上的上层协议。肯定要添加一些信息以区分每条消息的边界,且需要做缓冲区缓存不够一条完整消息的部分
你说的“如果没有它们”,不知是指什么。“避免粘包“代码必定服务端和客户端同时使用,事实上已经是一个自创协议了,一端使用而另一端不使用则无法通信 |
57
iceheart 2018-08-11 00:47:33 +08:00 via Android
"粘包","惊群" 都是二把刀搞出来的名词。根源在于还没搞清楚特定技术的打开姿势,就自以为懂了,出状况了又搞不清原因,就自己给起个名字。然后被其他的二把刀传播...
|
58
PhxNirvana 2018-08-11 01:24:57 +08:00
点了一下计算机网络,连接居然是博客首页。。是想表达这个博客是谢希仁的么。。
|
59
Tyanboot 2018-08-11 01:33:03 +08:00 1
@iceheart 忍不住搜了下什么是"惊群", 看了下真是惊了, 这帮人怕不是不知道 pthread_cond_* 和 condition_variable.
|
60
luozic 2018-08-11 01:33:35 +08:00 via iPhone
应用层数据使用 HTTP 协议走 tcp/ip 分包传输过程中出现了需要使用缓冲区或者异步轮询的方式才能完整反解出来。 你上层的事情为啥要管下层的事? 尼玛用数据库的时候是不是还要骂为啥连接资源就那么点,为啥不能开个几十万个?
缓冲和缓存本来就是处理时间有差异的不同层级东西的架构最优选择。 |
61
bombless 2018-08-11 02:24:57 +08:00 via Android
@zjp 一楼已经说了。其实就是把字节流拆成一段段的,可以理解成是自己在设计应用层协议(或者更一般的说,设计一个在字节流服务上跑的协议)
|
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 |
63
binarylu 2018-08-11 08:59:58 +08:00
|
64
urmyfaith 2018-08-11 10:45:55 +08:00 1
有啥可喷的?
编程的遇到的问题,命名了一个新词而已。 就比如外国人第一次用筷子吃饺子,他发明一个新词,叫 “筷饺现象”,然后在他们国家内讨论的火热。 这个时候,你看到中国会讨论这个现象吗 ? 只是遇到问题,总结一下前人的经验。上纲上线,好好讨论问题不行,有人基础差,或者说你水平高,你耐心评论一下,多指导下不就得了,何必冷嘲热讽? |
65
bombless 2018-08-11 11:10:30 +08:00 via Android
@urmyfaith 主要是看到不懂的人出来传教确实挺尴尬的。可以看出来 po 主没理解 tcp/ip 的多层设计到底是怎么回事。
说起来最近小火了的一个 golang 程序员对归档文件和 jar 的理解也是错的,然后还 |
66
bombless 2018-08-11 11:10:49 +08:00 via Android
跑出来教人
|
67
pangliang 2018-08-11 11:32:30 +08:00
要说本质问题, 本质问题是 tcp 一个流协议却给了一个"包"式的接口:
实际长度=read(缓冲区, 最大长度) |
68
newtype0092 2018-08-11 11:33:42 +08:00
@eastlhu 上大学学的计算机网络,TCP 接数据就这么写的,毕业后接触到粘包的概念,总以为自己没搞懂 TCP,感叹自己没好好学。。。
|
69
zn 2018-08-11 11:48:08 +08:00 1
先搞清楚什么叫 TCP 吧,还神他妈沾包。
其实就是自己基于 TCP 传输的自定义协议的解析问题。 *是*你*自*己*协*议*的*解*析*问*题*,懂了吗? 跟 TCP 一根毛的关系都没有。 |
70
pangliang 2018-08-11 11:51:34 +08:00 2
实际长度=read(缓冲区, 最大长度) 底层就这个接口,
在这段代码层面, 每次 read 完了, "缓冲区" 就是这一段代码这个层面的"包" 这个层面的"包" 跟应用层面的 "包" 不一一对应, 所以出现 两个层面的"包" 的拆和 粘的问题 就算 tcp 是个流, 你有一堆高级特性的 api, 你就骄傲了? 永远还是会遇到 2 个层面的包无法一一对应的问题 硬件缓冲区是不是"包" ? 现在硬件缓冲区能当"流"对待了? 总线不也还是一次按总线位数取"一块"? 拿着一堆高级特性 api 在嘲笑底层特性的 api 用法, 你们到底是在骄傲个啥? |
71
crayygy 2018-08-11 11:51:37 +08:00 via iPhone
@newtype0092 多看英文书...所谓的博客有很多都是臆测,甚至抄袭,所以有些错误的表述就这样被广泛传播
|
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 直接接收一大块数据, 然后再根据里面的包头信息进行解析, 处理起来有点麻烦 |
73
zhicheng 2018-08-11 12:31:54 +08:00
本来不想回复的,但看到有人误人子弟误得更深了,回复一下:
如果你觉得 read(缓冲区,长度)这个读的是一个 “包” , 那么请问我要传送一个 1TB 的数据包,应该怎么做?发送端和接收端各初始化一个 1TB 大小的缓冲区吗?另外接收端怎么知道对方要发一个 1TB 的数据包?还有我的机器是 32 位的怎么办? 经常写代码的工程师,即使没接触过流式协议也会很快想到这个问题,而这个问题的答案,随手就能找到。你们讨论的 “新手” 和 “老手” 其实是 “完全不懂网络编程的” 和 “懂网络编程的” 或者 “完全不懂编程硬要装着自己懂的” 和 “这个问题我不懂要看下书” 的两种人。 |
74
timothyqiu 2018-08-11 12:43:26 +08:00
我一般把一本正经讨论「粘包问题」的情况理解为不肯好好看文档、面向直觉编程。
|
75
pangliang 2018-08-11 12:43:36 +08:00
@zhicheng 你到底拿上层高特性的 api 在较什么劲?
底层能跑的开这种方式收数据吗? 动不动流式协议? 你硬件缓冲区是流式的吗? 你硬件缓冲区有 1TB 吗? 拿着一堆流式协议高级的 api, 然后说"不存在拆和粘的问题, 因为我是流式的" ? |
78
zhicheng 2018-08-11 12:52:09 +08:00 via iPhone
@pangliang 因为 TCP 是流式协议,所以没有包的概念,更没有粘包的概念。你到底要表达的是什么?
|
79
pangliang 2018-08-11 12:57:03 +08:00
@zhicheng
楼主说的是: (用)tcp(这种流协议)(实现业务层的包协议) 会有粘(业务层的)包问题. 所以你们一再强调 tcp 没有包是为啥? 楼主什么时候说 tcp 有包了? 楼主帖子里, "为什么会发生 TCP 粘包、拆包" 说的清清楚楚, 哪一句说 tcp 包了? |
81
pangliang 2018-08-11 13:26:40 +08:00
@zhicheng
(用)tcp(缓冲区读取的使用方式)(实现业务层的包协议) 会有(被缓冲区)粘(业务层的)包问题. 这样够确切了吗? 理解楼主要表达的意思就好了, 拿一些不相关的东西喷楼主是为啥呢? |
82
pangliang 2018-08-11 13:31:49 +08:00
@zhicheng
我同意 "tcp 这种流协议就应该用流式 api" 我也同意 "2018 年了不应该还在讨论粘包" 但是我不同意 "tcp 不存在这个问题" 因为一开始根本没有 流式 api 所以不要拿着 2018 年的 api 去嘲笑 2000 年的 api |
84
zhicheng 2018-08-11 13:41:18 +08:00 via iPhone
@pangliang 明明就是解释一下为什么
一次 send 可能要多次 recv 才能完成 和 多次 send 一次 recv 也可能能完成 的简单问题。却“发明”一个新手不懂老手也不懂的名词,别人指出来了还要强辩。 |
85
zhicheng 2018-08-11 13:45:42 +08:00 via iPhone
@pangliang 说真的我不知道你到底在讨论什么,2000 年的时候 TCP 或者说 socket 接口也是这样的,如果你硬要强辩,那我告诉你最底层的数据是流式的,那个东西叫做 “电流” 。
|
87
bombless 2018-08-11 13:48:34 +08:00 via Android
|
88
pangliang 2018-08-11 13:51:19 +08:00 1
@bombless
第三张图? Data 理解成是一个业务层定义的包 没问题啊 tcp 是流式, 但是它也不完全是像水一样的, 水的最小流动单位是 "一个原子??" tcp 的"流动"最小单位是 "报文", 也还是"一块" Data 跟 报文的大小不一致, 那么报文到达缓冲区之后 某个特定时刻 缓冲区内部 Data 的个数和完整性是不确定的 所以你如果按缓冲区 "整个" 来读, 那 Data 就肯定不完整 这个根本不是 tcp 是不是流式的问题, 而是 缓冲区把 流 "块" 化了 所以关键是使用缓冲区这个东西的方式 按"块"来用 "缓冲区" 就有问题 如果用流式来用缓冲区就没问题 |
89
bombless 2018-08-11 14:01:17 +08:00 via Android
@pangliang 看来你跟作者观点一样……我简单解释一下吧,你只是在读一个字节流,根本不应该关注你每次读了多少字节。操作系统就可以把邻接的一大串数据一次发给你,也可以每次发你一个字节让你慢慢读。因此你的 recv 每次读多少数据跟网络传输情况并没用关联,一般来说你也不应该关注,更重要的是不能因此影响你正确解析你自己设计的应用层协议。
从另一个方向解释,比如说你从 unix 套接字 recv,这时候是单机的,这时你从套接字读数据就完全是操作系统和往里写数据那一方决定了。 你仔细想想,你读写大文件的时候是不是也是一次不一定读完可能要读多次。 |
90
iwtbauh 2018-08-11 14:03:19 +08:00 via Android
@pangliang #67
你这个说法完全是扯谈 什么时候有过这个接口了? 实际长度=read(缓冲区, 最大长度) socket 接口可是这样的 读取到的长度=read(文件描述符, 缓冲区, 缓冲区长度) 文件描述符就是一个流的引用,你想从这个流里面读多少字节,就设置多大的缓冲区长度。 否则流的底层接口你说怎么设计啊。 现在明白 Unix 的最终设计思想“一切都是文件”的含义了吧:“一切都是文件”就是在暗示“一切都是字节流” |
91
pangliang 2018-08-11 14:07:06 +08:00
@iwtbauh 你说的接口跟我的没区别, 我的最大长度说的就是 缓冲区长度, 实际长度 就是 read 到的"实际"长度
|
92
pangliang 2018-08-11 14:08:39 +08:00
@iwtbauh 那你有没有想过, 为啥 这个接口 是
读取到的长度=read(文件描述符, 缓冲区, 缓冲区长度) 它为何有个 读取到的长度 ? 而不是 read(文件描述符, 缓冲区, 我就要这么长不读够不返回) ? |
93
pangliang 2018-08-11 14:10:09 +08:00
|
95
bombless 2018-08-11 14:11:32 +08:00 via Android
@pangliang 那是为了最终拼接起来,你只应该关注拼接到什么进度了,不应该关注拼接了几次。这就是我说的你不应该关注每次调用返回了多少数据
|
96
iwtbauh 2018-08-11 14:11:46 +08:00 via Android
@pangliang #91
你的#67 原话:“本质问题是 tcp 一个流协议却给了一个"包"式的接口” 我的意思:你在故意模糊接口参数的含义(最大长度和缓冲区大小看起来区别不大,但是表达的是不同的思想),故意隐藏掉“文件描述符”,来试图说明 read 这个接口“是包式接口“ 但其实是恰恰相反 |
99
bombless 2018-08-11 14:14:15 +08:00 via Android
@pangliang 看来你懂了,那你明白我为什么说 po 主放的图是错的了吧。他对整个问题都误解了
|