在socket网络编程中,都是端到端通信,由客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接。在TCP的socket编程中,发送端和接收端都有成对的socket。发送端为了将多个发往接收端的包,更加高效的发给接收端,于是采用了Nagle算法,将多次间隔较小,数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。那么这样一来,接收端就必须使用高效科学的拆包机制来分辨这些数据。

什么是TCP粘包/半包问题

所谓TCP粘包就是连续给对端发送两个或者两个以上的数据包,对端在一次收取中可能收到的数据包大于 1 个,大于 1 个,可能是几个(包括一个)包加上某个包的部分,或者干脆就是几个完整的包在一起(从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾)。当然,也可能收到的数据只是一个包的部分,这种情况一般也叫半包

举个具体的例子

  • A 与 B 进行 TCP 通信,A 先后给 B 发送了一个 100 字节和 200 字节的数据包,那么 B 是如何收到呢?B 可能先收到 100 字节,再收到 200 字节;也可能先收到 50 字节,再收到 250 字节;或者先收到 100 字节,再收到 100 字节,再收到 100 字节;或者先收到 20 字节,再收到 20 字节,再收到 60 字节,再收到 100 字节,再收到 50 字节,再收到 50 字节……

  • 从上面可以看出:A一共给B发送了300字节,B可能以一次或者多次任意形式的总数为300字节收到。假设A给B发送的100字节和200字节分别都是一共数据包,对于发送端A来说,这个是可以区分的,但是对B来说,如果不人为规定多长为一个数据包,B每次是不知道应该把收到的数据中多少字节作为一个有效的数据包的。

这也是为什么说TCP是一个流式协议的具体原因。

为什么要解决粘包问题呢?

因为B不知道应该把收到的数据中多少字节作为一个有效的数据包,可能会有新手写出类似下面这样的代码

发送端:

//...省略创建socket,建立连接等部分不相关的逻辑...
char buf[] = "the quick brown fox jumps over a lazy dog.";
int n = send(socket, buf, strlen(buf), 0);
//...省略出错处理逻辑...

接收端:

//省略创建socket,建立连接等部分不相关的逻辑...
char recvBuf[50] = { 0 };
int n = recv(socket, recvBuf, 50, 0);
//省略出错处理逻辑...
printf("recvBuf: %s", recvBuf);

类似这样的代码在本机一般会工作的很好,接收端也如期打印出来预料的字符串,但是一放到局域网或者公网环境就出问题了,即接收端可能打印出来字符串并不完整;如果发送端连续多次发送字符串,接收端会打印出来的字符串不完整或出现乱码。不完整的原因很好理解,即对端某次收到的数据小于完整字符串的长度,recvBuf 数组开始被清空成 0,收到部分字符串后,该字符串的末尾仍然是 0,printf 函数寻找以 0 为结束标志的字符结束输出;乱码的原因是如果某次收入的数据不仅包含一个完整的字符串,还包含下一个字符串部分内容,那么 recvBuf 数组将会被填满,printf 函数输出时仍然会寻找以 0 为结束标志的字符结束输出,这样读取的内存就越界了,一直找到为止,而越界后的内存可能是一些不可读字符,显示出来后就乱码了。

造成TCP粘包的原因

出现粘包的原因是多方面的,可能是来自接收方,也可能是来自发送方。

发送方原因

TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:

  • 只有上一个分组得到确认,才会发送下一个分组
  • 收集多个小分组,在一个确认到来时一起发送。

Nagle算法造成了发送方可能会出现粘包问题

  • TCP有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入TCP栈中----即使一次只放一个字节也可以(!!!造成“发送端傻窗口综合征”)。但是,每个TCP段中都至少装载了40个字节的标记和首部,所以如果TCP发送了大量包含少量数据的分组,网络的性能就会严重下降,
  • Nagle算法试图在发送一个分组之前,将大量的TCP数据绑定在一起,以提高网络效率。
  • Nagle算法鼓励发送全尺寸的段,只有当所有其他分组都被确认之后,Nagle算法才允许发送非全尺寸的分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来。只有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去。
  • Nagle算法会引发几种HTTP性能问题。首先,小的HTTP报文可能无法填满一个分组,可能会因为等待哪些永远不会到达的额外数据而产生时延。其次,Nagle算法与延迟确认之间的交互存在问题------Nagle算法会阻止数据的发送,直到有确认分组到达为止。但确认分组自身会被延迟确认算法延迟100-200ms,当使用管道化连接时这些问题可能会更加严重,因为客户端可能会有多条报文要发送给同一个服务器,而且不希望有时延存在。
  • HTTP应用程序常常会在自己的栈中设置参数TCP_NODELAY,禁用Nagle算法,提高性能。如果这样做的话,一定要确保会向TCP写入大块的数据,这样就不会产生一堆小分组了

接收方原因

TCP接收到数据包时,并不会马上交到应用层处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据报到缓存的速度岛屿应用程序从缓存中读取数据报的速度,多个包就会被缓存,应用程序就有可能读取到多个收尾相接粘到一起的包

如何处理粘包现象

发送方

对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。

        acl_socket* conn_fd = listen_fd_->acl_accept(NULL, 0, NULL);if (conn_fd == NULL) {if (open_flag_ & ACL_NON_BLOCKING) {logger_error("accept error %s", strerror(errno));} else if (errno != EAGAIN && errno != EWOULDBLOCK) {logger_error("accept error %s", strerror(errno));return NULL;}}conn_fd->acl_tcp_set_nodelay();
void acl_tcp_set_nodelay(ACL_SOCKET fd)
{acl_tcp_nodelay(fd, 1);
}
// 1表示打开 TCP_NODELAY选项
void acl_tcp_nodelay(ACL_SOCKET fd, int onoff)
{const char *myname = "acl_tcp_nodelay";int   on = onoff ? 1 : 0;int   n = acl_getsocktype(fd);#ifdef AF_INET6if (n != AF_INET && n != AF_INET6)
#elseif (n != AF_INET)
#endifreturn;if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,(char *) &on, sizeof(on)) < 0) {acl_msg_error("%s(%d): set nodelay error(%s), onoff(%d)",myname, __LINE__, acl_last_serror(), onoff);}
}

接收方

接收方没有办法来处理粘包问题,只能将问题交给应用层来处理

应用层

应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。

解决方法:循环处理,应用程序从接收缓存中读取分组时,读完一条数据,就应该循环读取下一条数据,直到所有数据都被处理完成,但是如何判断每条数据的长度呢?也就是说怎么从收到的数据中包包与包的边界给区分出来(这也是最关键的地方)

无论是半包还是粘包问题,其根源就是因为TCP是一个流式协议。所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要我们人为的去给这些协议划分边界,以便接收端知道从什么位置取出多少字节来当成一个数据包去解析,这就是我们设计网络通信协议格式要做的工作之一。

目前主要有三种方法:

  • 固定包长
  • 以指定字符(串)为包的结束标志
  • 包头 + 包体格式

ps:在使用大多数网络库时,通常你需要根据协议格式自己给数据包分界和解析,一般的网络库不提供这种功能是处于需要支持不同的协议,由于协议的不确定性,因此没法预先提供具体的解包代码。当然,这不是绝对的,也有一些网络库提供了这种功能

固定包长的数据包

顾名思义,即每个协议包的长度都是固定的。举个例子,比如我们可以规定协议包的大小是64个字节,每次收满64个字节,就取出来解析(如果不够,就先存起来)。

这种通信协议的格式简单但灵活性差。如果包内容不足指定的字节数,剩余的空间需要填充特殊的信息,如 \0(如果不填充特殊内容,如何区分包里面的正常内容与填充信息呢?);如果包内容超过指定字节数,又得分包分片,需要增加额外处理逻辑-------在发送端进行分包分片,在接收端重新组装包片。

以指定字符(串)为包的结束标志

这种协议包比较常见,即字节流中遇到特殊的符号值时就认为到一个包的末尾了。比如,我们熟悉的FTP协议,发邮件的SMTP协议,一个命令或者一段数据后面加上"\r\n"(即所谓的 CRLF)表示一个包的结束。对端收到后,每遇到一个"\r\n"就把之前的数据当做一个数据包。

这种协议一般用于一些包含各种命令控制的应用中,其不足之处就是如果协议数据包内容部分需要使用包结束标志字符,就需要对这些字符做转码或者转义操作,以免被接收方错误地当成包结束标志而误解析。

包头 + 包体格式

这种格式的包一般分为两部分,即包头和包体,包头是固定大小的,而且包头中必须含有一个字段来说明接下来的包体有多大。

比如:

struct msg_header{int32_t bodySize;int32_t cmd;
};

这就是一个典型的包头格式,bodySize 指定了这个包的包体是多大。由于包头大小是固定的(这里是 size(int32_t) + sizeof(int32_t) = 8 字节),对端先收取包头大小字节数目(当然,如果不够还是先缓存起来,直到收够为止),然后解析包头,根据包头中指定的包体大小来收取包体,等包体收够了,就组装成一个完整的包来处理。在有些实现中,包头中的 bodySize可能被另外一个叫 packageSize 的字段代替,这个字段的含义是整个包的大小,这个时候,我们只要用 packageSize 减去包头大小(这里是 sizeof(msg_header))就能算出包体的大小,原理同上。

UDP会不会产生粘包问题呢?

TCP为了保证可靠传输并减少额外的开销(每次发包都要验证),采用了基于流的传输,基于流的传输不认为消息是一条条的,是无保护消息边界的(保护消息边界:指传输协议把数据当做一条独立的消息在网上传输,接收端一次只能接受一条独立的消息)。

UDP则是面向消息的,是由保护边界的,接收方一次只接收一条独立的消息,所以不存在粘包问题

举个例子:有三个数据包,大小分别为2k、4k、6k,如果采用UDP发送的话,不管接受方的接收缓存有多大,我们必须要进行至少三次以上的发送才能把数据包发送完,但是使用TCP协议发送的话,我们只需要接受方的接收缓存有12k的大小,就可以一次把这3个数据包全部发送完毕。

面试题

粘包问题发生在哪里?

  • TCP

包乱序发生在哪里

  • UDP

你怎么解决TCP粘包问题?

  • 发送端:使用TCP_NODELAY关闭Nagle算法,但是如果不是时延敏感的应用尽量不要关闭
  • 接收端:没法解决,只能交给应用端解决
  • 应用层:有三种方法:
    • 第一种:只发送固定包长的数据包,但是这个方法基本不用,灵活性太差了
    • 第二种:指定标识结尾,比如\r\n之类的
    • 第三种:包头+包体,包头一般是固定长度,并且里面有一个字段可以告知我们接下来的包体有多大

面试题:网络通信时,如何解决粘包、丢包或者包乱序问题?

  • 如果是TCP协议,在大多数场景下,是不存在丢包和包乱序的问题的,TCP通信是可靠通信方式,TCP协议栈通过序列号和包重传确认机制保证数据包的有序和一定被正确发到目的地
  • 如果是UDP协议,如果不能接受少量丢包,那就要自己在UDP的基础上实现类似TCP这种有序和可靠传输机制(比如RTP协议、RUDP协议)。所以,问题拆解后,只剩下如何解决粘包的问题了

那么怎么解决粘包问题呢?

  • 发送端:关闭Nagle算法算法

  • 接收端只能在应用层解决:

    • 固定包长
    • 以指定字符(串)为包的结束标志
    • 包头 + 包体格式
  • TCP 协议如何解决粘包、半包问题(待研究)

网络:什么是TCP粘包/半包?怎么解决这个问题相关推荐

  1. Netty框架之TCP粘包/半包解决方案

    Netty框架之TCP粘包/半包解决方案 一.TCP粘包 二.TCP半包 三.TCP粘包/半包解决方案 1.FixedLengthFrameDecoder定长解析器 2.LineBasedFrameD ...

  2. TCP 粘包半包 netty 编解码 三者关系

    1 何为粘包 / 半包? 对方一次性接收了多条消息这种现象,我们就称之为 粘包现象. 对方多次接收了不完整消息这种现象,我们就称之为 半包现象. 粘包的原因: 发送方发送的消息 < 缓冲区大小 ...

  3. 三、Netty的粘包半包问题解决

    一.定义 TCP 传输中,客户端发送数据,实际是把数据写入到了 TCP 的缓存中,粘包和半包也就会在此时产生.客户端给服务端发送了两条消息ABC和DEF,服务端这边的接收会有多少种情况呢?有可能是一次 ...

  4. Netty如何解决粘包半包问题

    何为粘包 / 半包? 比如,我们发送两条消息:ABC 和 DEF,那么对方收到的就一定是 ABC 和 DEF 吗? 不一定,对方可能一次就把两条消息接收完了,即 ABCDEF:也可能分成了好多次,比如 ...

  5. Netty粘包/半包问题解析

    目录 一.什么是粘包/半包问题 二.TCP粘包/半包发生的原因 三.粘包/半包解决办法 四.Netty中粘包/半包解决示例 1. 采用固定长度数据包编解码方式 2. 采用特殊字符作为边界字符编解码方式 ...

  6. websocket是否需要处理粘包半包问题分析

    结论: ​ 不需要. 背景: ​ 公司通信涉及到websocket相关,我们都知道websocket是基于tcp的,而tcp是面向字节流的,是需要处理粘包半包问题的.那么websocket是否需要处理 ...

  7. netty——黏包半包的解决方案、滑动窗口的概念

    黏包半包 滑动窗口 在深入理解黏包半包问题之前,先了解TCP的一个知识点--滑动窗口 我们都指定tcp是一种可靠的传输协议,这主要是因为在tcp中客户端给服务器端发送一条消息,要等待服务器端的应答,如 ...

  8. java获取一个tcp包大小_Java网络编程之TCP粘包拆包

    TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想象河里的流水,他们是连成一片的,其间并没有分界线.TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实 ...

  9. Http 调用netty 服务,服务调用客户端,伪同步响应.ProtoBuf 解决粘包,半包问题.

    实际情况是: 公司需要开发一个接口给新产品使用,需求如下 1.有一款硬件设备,客户用usb接上电脑就可以,但是此设备功能比较单一,所以开发一个服务器程序,辅助此设备业务功能 2.解决方案,使用Sock ...

最新文章

  1. Pycharm + Anaconda 安装遇到的问题以及自己的理解
  2. 计算机基础知识_2020年河北省高职单招计算机基础知识和实践技能培训
  3. ML之sklearn:sklearn.metrics中常用的函数参数(比如confusion_matrix等 )解释及其用法说明之详细攻略
  4. Vim与clang-format
  5. mysql url格式,关于MySql链接url参数的设置
  6. linux 命令 记忆方法,linux 记忆命令心得
  7. 常用于评价回归模型优劣的统计量包括( )。_第四十一讲 R-判断回归模型性能的指标...
  8. ios 销毁当前页面重新开启_问:如何强制销毁iOS中的视图控制器?
  9. 编程实战:如何管理代码里的常量
  10. 使用Fluent NHibernate和AngularJS的Master Chef(第1部分)ASP.NET Core MVC
  11. 网络通讯技术在嵌入式系统中的应用
  12. Python实现快乐的数字
  13. python+sklearn利用特征文件来训练和测试模型并使用joblib方法持久化存储模型
  14. 最高单价计算机函数公式,通达信公式主动性买盘均价,通达信分时图中 分时均价线黄线的源码怎么写...
  15. 快速模版的本地化改进
  16. 戴戒指的含义(以后要结婚的必看)
  17. FFmpeg指令行打开usb摄像头(windows)
  18. 整理一道测试面试题(微信更换头像测试用例)
  19. Git超详解七 储藏 (看不懂算我输)
  20. 路由器的四种配置模式

热门文章

  1. XGBoost Feature Importance Computed in 3 Ways with Python
  2. Radiation Diagram
  3. 雷诺又回来了 雷诺江铃D180能否打开中国市场
  4. python 求两个list的差集,并集和交集
  5. taro生命周期详解
  6. bp神经网络进行交通预测的matlab源代码_神经网络进行股票价格预测软件----MATLAB--毕业设计...
  7. 数据库笔记整理--基于《数据库系统概论》第五版王珊一书|第二章--关系数据库知识整理和课后习题答案
  8. ocacle 执行计划_Oracle执行计划详解
  9. css中控制鼠标手型的属性(以及js控制)
  10. 【网络协议】TCP的交互数据流和成块数据流