计算机网络 TCP协议如何保证可靠传输

目录

TCP如何保证可靠传输

TCP以段为单位发送数据

重传机制 

超时重传

重发超时如何确定

快速重传

SACK 的引入是为了解决什么问题?

D-SACK 又是什么东西?

ARQ协议

停止等待ARQ协议

连续ARQ协议

利用窗口控制提高速度

发送方的滑动窗口

 程序是如何表示发送方的四个部分的呢?

流量控制与接收方的滑动窗口

接收窗口和发送窗口的大小是相等的吗?

流量控制

操作系统缓冲区与滑动窗口的关系

如果接收方回复的窗口一直是 0 怎么办?

糊涂窗口综合症

怎么让接收方不通告小窗口呢?

怎么让发送方避免发送小数据呢?

拥塞控制

已经有滑动窗口了为什么还要拥塞控制?

慢启动

拥塞避免

拥塞发生

快恢复

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP传输参数优化

滑动窗口

如果确定最大传输速度?

怎样发送调整缓冲区大小?

调节接收缓冲区范围

调节 TCP 内存范围

根据实际场景调节的策略

传输参数小结

提高网络利用率的规范

延迟确认应答

捎带应答

TCP粘包与拆包

粘包和拆包原因

粘包和拆包解决策略

粘包的概念

出现粘包的原因

粘包的处理方式


TCP如何保证可靠传输

为了通过IP数据报实现可靠性传输,需要考虑很多事情,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。

  1. 用数据被分割成TCP认为最适合发送的数据块。
  2. TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
  3. 校验和:TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。
  4. TCP的接收端会丢弃重复的数据。
  5. 流量控制:TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP使用的流量控制协议是可变大小的滑动窗口协议。(TCP利用滑动窗口实现流量控制)
  6. 拥塞控制:当网络拥塞时,减少数据的发送。
  7. ARQ协议:也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
  8. 超时重传:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

TCP以段为单位发送数据

在建立TCP连接的同时,也可以确定发送数据包的单位,我们也可以称其为“最大消息长度"(MSS: Maximum Segment Size)。最理想的情况是,最大消息长度正好是IP中不会被分片处理的最大数据长度。

TCP在传送大量数据时,是以MSS的大小将数据进行分割发送。进行重发时也是以MSS为单位。

MSS是在三次握手的时候,在两端主机之间被计算得出。

两端的主机在发出建立连接的请求时,会在TCP首部中写入MSS选项,告诉对方自己的接口能够适应的MSS的大小。然后会在两者之间选择一个较小的值投人使用。

重传机制 

超时重传

在TCP中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知。这个消息叫做确认应答(ACK)。

通常,两个人对话时,在谈话的停顿处可以点头或询问以确认谈话内容。如果对方迟迟没有任何反馈,说话的一方还可以再重复一遍以保证对方确实听到。因此,对方是否理解了此次对话内容,对方是否完全听到了对话的内容,都要靠对方的反应来判断。网络中的“确认应答”就是类似这样的一个概念。当对方听懂对话内容时会说:“嗯”,这就相当于返回了一个确认应答(ACK)。而当对方没有理解对话内容或没有听清时会问一句“咦?”这好比一个否定确认应答(NACK)

TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。

在一定时间内没有等到确认应答,发送端就可以认为数据已经丢失,并进行重发。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。

未收到确认应答并不意味着数据一定丢失。也有可能是数据对方已经收到,只是返回的确认应答在途中丢失

这种情况也会导致发送端因没有收到确认应答,而认为数据没有到达目的地,从而进行重新发送。

此外,也有可能因为一些其他原因导致确认应答延迟到达,在源主机重发数据以后才到达的情况也履见不鲜。此时,源发送主机只要按照机制重发数据即可。但是对于目标主机来说,这简直是一种“灾难”。它会反复收到相同的数据。而为了对上层应用提供可靠的传输,必须得放弃重复的数据包。为此,就必须引入一种机制,它能够识别是否已经接收数据,又能够判断是否需要接收

上述这些确认应答处理、重发控制以及重复控制等功能都可以通过序列号实现。

序列号是按顺序给发送数据的每一个字节(8位字节)都标上号码的编号。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去。就这样,通过序列号和确认应答号,TCP可以实现可靠传输。

重发超时如何确定

我们先来了解一下什么是 RTT(Round-Trip Time 往返时延),从下图我们就可以知道:

RTT 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。

超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。

RTT是真实的时间,RTO是设定的时间

假设在重传的情况下,超时时间 RTO 「较长或较短」时,会发生什么事情呢?

 上图中有两种超时时间不同的情况:

当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;

当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

精确的测量超时时间 RTO 的值是非常重要的,这可让我们的重传机制更高效。

根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返  RTT 的值。

至此,可能大家觉得超时重传时间 RTO 的值计算,也不是很复杂嘛。

好像就是在发送端发包时记下 t0 ,然后接收端再把这个 ack 回来时再记一个 t1,于是 RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。

实际上报文往返 RTT 的值是经常变化的,因为我们的网络也是时常变化的。也就因为报文往返 RTT 的值 是经常波动变化的,所以超时重传时间 RTO 的值应该是一个动态变化的值。

我们来看看 Linux 是如何计算 RTO 的呢?

估计往返时间,通常需要采样以下两个:

需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。

除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。

RFC6289 建议使用以下的公式计算 RTO:

其中 SRTT 是计算平滑的RTT ,DevRTR 是计算平滑的RTT 与 最新 RTT 的差距。

在 Linux 下,α = 0.125,β = 0.25, μ = 1,∂ = 4。别问怎么来的,问就是大量实验中调出来的。

如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。

也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

在BSD的Unix以及Windows系统中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍。不过,由于最初的数据包还不知道往返时间,所以其重发超时一般设置为6秒左右。

数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。

此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没 有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。 并且通知应用通信异常强行终止。

快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。

快速重传机制,是如何工作的呢?其实很简单,一图胜千言。

在上图,发送方发出了 1,2,3,4,5 份数据:

第一份 Seq1 先送到了,于是就 Ack 回 2;

结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;

后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;

发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。

最后,接收到收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。

比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。

根据 TCP 不同的实现,以上两种情况都是有可能的。可见,这是一把双刃剑。

为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 方法。

SACK 的引入是为了解决什么问题?

SACK 即 Selective Acknowledgment,它的引入就是为了解决发送方不知道该重传哪些数据的问题。

我们来看一下下面的图就知道了。

SACK 就是接收方会回传它已经接受到的数据,这样发送方就知道哪一些数据对方已经收到了,所以就可以选择性的发送丢失的数据。

如图,通过 ACK 告知我接下来要 5500 开始的数据,并一直更新 SACK,6000-6500 我收到了,6000-7000的数据我收到了,6000-7500的数据我收到了,发送方很明确的知道,5500-5999 的那一波数据应该是丢了,于是重传。

而且如果数据是多段不连续的, SACK 也可以发送,比如 SACK 0-500,1000-1500,2000-2500。就表明这几段已经收到了。

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

D-SACK 又是什么东西?

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉发送方有哪些数据被重复接收了。

下面举例两个栗子,来说明 D-SACK 的作用。

栗子一号:ACK 丢包

接收方发给发送方的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)

于是接收方发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉发送方 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着 D-SACK。

这样发送方就知道了,数据没有丢,是接收方的 ACK 确认报文丢了。

栗子二号:网络延时

数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。

而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;

所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。

这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。

可见,D-SACK 有这么几个好处:

可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;

可以知道是不是「发送方」的数据包被网络延迟了;

可以知道网络中是不是把「发送方」的数据包给复制了;

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

ARQ协议

自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。

停止等待ARQ协议

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到ACK确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。

在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。

优点:简单

缺点:信道利用率低,等待时间长

无差错情况:

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

出现差错情况(超时重传):

停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求ARQ。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续ARQ协议可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。

确认丢失和确认迟到

确认丢失:确认消息在传输过程丢失。当A发送M1消息,B收到后,B向A发送了一个M1确认消息,但却在传输过程中丢失。而A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:

1.丢弃这个重复的M1消息,不向上层交付。

2.向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。

确认迟到 :确认消息在传输过程中迟到。A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会,A收到了B第一次发送的对M1的确认消息(A也收到了2份确认消息)。处理如下:

1. A 收到重复的确认后,直接丢弃。

2. B 收到重复的 M1 后,也直接丢弃重复的 M1。

连续ARQ协议

连续ARQ协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

优点:信道利用率高,容易实现,即使确认丢失,也不必重传。

缺点:不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如,发送方发送了5条消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫Go-Back-N(回退 N),表示需要退回来重传已经发送过的N个消息。

利用窗口控制提高速度

TCP以1个段为单位,每发一个段进行一次确认应答的处理,这样的传输方式有一个缺点。那就是,包的往返时间越长通信性能就越低。

为解决这个问题,TCP引人了窗口这个概念。即使在往返时间较长的情况下,它也能控制网络性能的下降。确认应答不再是以每个分段,而是以更大的单位进行确认时,转发时间将会被大幅度的缩短。也就是说,发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送。

窗口大小就是指无需等待确认应答而可以继续发送数据的最大值(简称rwnd。图,窗口大小为4个段。

这个机制实现了使用大量的缓冲区,通过对多个段同时进行确认应答的功能。

发送数据中高亮圈起的部分正是前面所提到的窗口。在这个窗口内的数据即便没有收到确认应答也可以发送出去。此外,从该窗口中能看到的数据因其某种数据已在传输中丢失,所以发送端才能收到确认应答,这种情况也需进行重发。为此,发送端主机在等到确认应答返回之前,必须在缓冲区中保留这部分数据。

在滑动窗口以外的部分包括尚未发送的数据以及巳经确认对端已收到的数据。

当数据发出后若如期收到确认应答就可以不用再进行重发,此时数据就可以从缓存区清除。

收到确认应答的情况下,将窗口滑动到确认应答中的序列号的位置。这样可以顺序地将多个段同时发送提高通信性能。这种机制也被称为滑动窗口控制。

发送方的滑动窗口

我们先来看看发送方的窗口,下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

#1 是已发送并收到 ACK确认的数据:1~31 字节

#2 是已发送但未收到 ACK确认的数据:32~45 字节

#3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节

#4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

 在下图,当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

 程序是如何表示发送方的四个部分的呢?

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

SND.WND:表示发送窗口的大小(大小是由接收方指定的);

SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。

SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。

指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

那么可用窗口大小的计算就可以是:

可用窗口大 = SND.WND -(SND.NXT - SND.UNA)

流量控制与接收方的滑动窗口

接下来我们看看接收方的窗口,接收窗口相对简单一些,根据处理的情况划分成三个部分:

#1 + #2 是已成功接收并确认的数据(等待应用进程读取);

#3 是未收到数据但可以接收的数据;

#4 未收到数据并不可以接收的数据;

其中三个接收部分,使用两个指针进行划分:

RCV.WND:表示接收窗口的大小,它会通告给发送方。

RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。

指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

接收窗口和发送窗口的大小是相等的吗?

并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

发送端根据自己的实际情况发送数据。但是,接收端可能收到的是一个毫无关系的数据包又可能会在处理其他问题上花费一些时间。因此在为这个数据包做其他处理时会耗费一些时间,甚至在高负荷的情况下无法接收任何数据。如此一来,如果接收端将本应该接收的数据丢弃的话,就又会触发重发机制,从而导致网络流量的无端浪费。

为了防止这种现象的发生,TCP提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量。这就是所谓的流控制。它的具体操作是,接收端主机向发送端主机通知自己可以接收数据的大小,于是发送端会发送不超过这个限度的数据。该大小限度就被称作窗口大小。窗口大小的值就是由接收端主机决定的。

TCP首部中,专门有一个字段用来通知窗口大小,这个字段叫做滑动窗口。接收主机将自己可以接收的缓冲区大小放入这个字段中通知给发送端。这个字段的值越大,说明网络的吞吐量越高。

不过,接收端的这个缓冲区一旦面临数据溢出时,窗口大小的值也会随之被设置为一个更小的值通知给发送端,从而控制数据发送量。也就是说,发送端主机会根据接收端主机的指示,对发送数据的量进行控制。这也就形成了一个完整的TCP流量控制(流量控制)。

图中的 #1 是已收到 ACK 的数据,#2 是已经发出去但是还没收到 ACK 的数据,#3 就是在窗口内可以发送但是还没发送的数据。#4 就是还不能发送的数据。

然后此时收到了 36 的 ACK,并且发出了 46-51 的字节,于是窗口向右滑动了。

操作系统缓冲区与滑动窗口的关系

前面的流量控制例子,我们假定了发送窗口和接收窗口是不变的,但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整

当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

那操心系统的缓冲区,是如何影响发送窗口和接收窗口的呢?

我们先来看看第一个例子。

当应用程序没有及时读取缓存时,发送窗口和接收窗口的变化。

考虑以下场景:

客户端作为发送方,服务端作为接收方,发送窗口和接收窗口初始大小为 360

服务端非常的繁忙,当收到客户端的数据时,应用层不能及时读取数据。

根据上图的流量控制,说明下每个过程:

客户端发送 140 字节数据后,可用窗口变为 220 (360 - 140)。

服务端收到 140 字节数据,但是服务端非常繁忙,应用进程只读取了 40 个字节,还有 100 字节占用着缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通过给客户端。

客户端收到确认和窗口通告报文后,发送窗口减少为 260。

客户端发送 180 字节数据,此时可用窗口减少到 80。

服务端收到 180 字节数据,但是应用程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并在发送确认信息时,通过窗口大小给客户端。

客户端收到确认和窗口通告报文后,发送窗口减少为 80。

客户端发送 80 字节数据后,可用窗口耗尽。

服务端收到 80 字节数据,但是应用程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端。

客户端收到确认和窗口通告报文后,发送窗口减少为 0。

可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变,这个内容后面会说,这里先简单提一下。

我们先来看看第二个例子。

当服务端系统资源非常紧张的时候,操心系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。

说明下每个过程:

客户端发送 140 字节的数据,于是可用窗口减少到了 220。

服务端因为现在非常的繁忙,操作系统于是就把接收缓存减少了 100 字节,当收到 对 140 数据确认报文后,又因为应用程序没有读取任何数据,所以 140 字节留在了缓冲区中,于是接收窗口大小从 360 收缩成了 100,最后发送确认信息时,通告窗口大小给对方。

此时客户端因为还没有收到服务端的通告窗口报文,所以不知道此时接收窗口收缩成了 100,客户端只会看自己的可用窗口还有 220,所以客户端就发送了 180 字节数据,于是可用窗口减少到 40。

服务端收到了 180 字节数据时,发现数据大小超过了接收窗口的大小,于是就把数据包丢失了。

客户端收到第 2 步时,服务端发送的确认报文和通告窗口报文,尝试减少发送窗口到 100,把窗口的右端向左收缩了 80,此时可用窗口的大小就会出现诡异的负值。

所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间在减少缓存,这样就可以避免了丢包情况。

如果接收方回复的窗口一直是 0 怎么办?

上文已经说了发送方式根据接收方回应的 window 来控制能发多少数据,如果接收方一直回应 0,那发送方就杵着?

你想一下,发送方发的数据都得到 ACK 了,但是呢回应的窗口都是 0 ,这发送方此时不敢发了啊,那也不能一直等着啊,这 Window 啥时候不变 0 啊?

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;

如果接收窗口不是 0,那么死锁的局面就可以被打破了。

窗口探查探测的次数一般为 3 此次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。

就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车。除非家里有矿的大巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车。

现举个糊涂窗口综合症的栗子,考虑以下场景:

接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:

接收方每接收 3 个字节,应用程序就只能从缓冲区中读取 1 个字节的数据;

在下一个发送方的 TCP 段到达之前,应用程序

还从缓冲区中读取了 40 个额外的字节;

每个过程的窗口大小的变化,在图中都描述的很清楚了,可以发现窗口不断减少了,并且发送的数据都是比较小的了。

所以,糊涂窗口综合症的现象是可以发生在发送方和接收方:

接收方可以通告一个小的窗口

而发送方可以发送小数据

于是,要解决糊涂窗口综合症,就解决上面两个问题就可以了

让接收方不通告小窗口给发送方

让发送方避免发送小数据

怎么让接收方不通告小窗口呢?

接收方通常的策略如下:

当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

怎么让发送方避免发送小数据呢?

发送方通常的策略:

使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:

要等到窗口大小 >= MSS 或是 数据大小 >= MSS

收到之前发送数据的 ack 回包

只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。

另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

拥塞控制

流量控制通过滑动窗口来实现,但是rwnd窗口(就是滑动窗口大小)只考虑了发送端和接收端的问题,没考虑网络的问题。有可能接收端很快,但是网络拥塞了,所以加了一个拥塞窗口(cwnd)。拥塞窗口的意思就是一次性可以连续提交多少个包到网络中。最终的形态是LastByteSent-LastByteAcked<=min(cwnd,rwnd),由两个窗口共同控制发送速度

即,在发送数据包时,将拥塞窗口的大小与接收端主机通知的窗口大小做比较,然后按照它们当中较小那个值,发送比其还要小的数据量。
TCP的拥塞控制主要避免两种现象,包丢失和包重传。网络的带宽是固定的,当发送端发送速度超过带宽后,中间设备处理不完多出来的包就会被丢弃,这就是包丢失。如果我们在中间设备上加上缓存,处理不过来的包就会被加到缓存队列中,不会丢失,但是会增加时延。如果时延到达一定的程度,就会超时重传,这就是包重传。
拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)快重传,4)快恢复

已经有滑动窗口了为什么还要拥塞控制?

前面我已经提到了,加了拥塞控制是因为 TCP 不仅仅就管两端之间的情况,还需要知晓一下整体的网络情形,毕竟只有大家都守规矩了道路才会通畅。

前面我们提到了重传,如果不管网络整体的情况,肯定就是对方没给 ACK ,那我就无脑重传。

如果此时网络状况很差,所有的连接都这样无脑重传,是不是网络情况就更差了,更加拥堵了?

然后越拥堵越重传,一直冲冲冲!然后就 GG 了。

慢启动

有了TCP的窗口控制,收发主机之间即使不再以一个数据段为单位发送确认应答,也能够连续发送大量数据包。然而,如果在通信刚开始时就发送大量数据,也可能会引发其他问题。

一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。在网络出现拥堵时,如果突然发送一个较大量的数据,极有可能会导致整个网络的瘫痪。

TCP为了防止该问题的出现,在通信一开始时就会通过一个叫做慢启动的算法得出的数值,对发送数据量进行控制。

首先,为了在发送端调节所要发送数据的量,定义了一个叫做“拥塞窗口”的概念

于是在慢启动的时候,将这个拥塞窗口的大小设置为1个数据段(1 MSS)发送数据,之后每收到一次确认应答(ACK),拥塞窗口的值就加1。

如果ACK正常收到,每当过了一个RTT则翻倍,以指数增长。如果网速很快的话增长速度还是很可观的。(因为如果当前是4,都发送出去,然后返回ack 4次,cwnd=4+4=8,相当于翻倍)

如果重发采用超时机制,那么拥塞窗口的初始值可以设置为1以后再进行慢启动修正。有了上述这些机制,就可以有效地减少通信开始时连续发包1,导致的网络拥堵,还可以避免网络拥塞情况的发生。

拥塞避免

不过,随着包的每次往返,拥塞窗口也会以1、2、4等指数函数的增长,拥堵状况激增甚至导致网络拥塞的发生。为了防止这些,引入了慢启动阀值(ssthresh)的概念

一般来说 ssthresh 的大小是 65535 字节。

只要拥塞窗口的值超出这个阀值,在每收到一次确认应答时,只允许以下面这种 比例放大拥塞窗口:

当涨到一次发送超过ssthresh,就会减速,改成每次加1/cwnd,比如之前一次发送cwnd是10个MSS,现在每次收到一个确认cwnd加1/10个MSS,每过一个RTT加1个(收到1个ack增加1/10,一次发送10个,收到10次ack,增加1MSS)。这样一直加下去知道拥塞出现,比如包丢失。

拥塞窗口越大,确认应答的数目也会增加。不过随着每收到一个确认应答,其涨幅也会逐渐减少,甚至小过比一个数据段还要小的字节数。因此,拥塞窗口的大小会呈直线上升的趋势。

拥塞发生

虽然超过ssthresh时会减速,但是还是在涨,早晚会产生拥塞的。这时候有两种处理方式

1)超时重传。

一旦超时重传出现,则把ssthresh改成cwnd/2,cwnd窗口改成1,重新从头开始慢启动。(注意:TCP的通信开始时,并没有设置相应的慢启动阀值,而是在超时重发时,才会设置为当时拥塞窗口一半的大小。)但是这种方式太激进了,反应也很强烈,会造成网络卡顿。

2)快重传。

还有一种情况就是收到接收端SACK要求重传(这种情况要求至少3次的确认应答数据段到达对方主机后才会触发,相比超时重传的情况,网络的拥堵要轻一些),这种TCP认为不严重,ssthresh改成cwnd/2,cwnd降为cwnd/2+3。之后进入快恢复算法。

有了这样一种控制,TCP的拥塞窗口如图所示发生变化。由于窗口的大小会直接影响数据被转发时的吞吐量,所以一般情况下,窗口越大,越会形成高吞吐量的通信。当TCP通信开始以后,网络吞吐量会逐渐上升,但是随着网络拥堵的发生吞吐量也会急速下降。于是会再次进入吞吐量慢慢上升的过程。因此所谓TCP的吞吐量的特点就好像是在逐步占领网络带宽的感觉。

快恢复

当发送方连续收到三个重复确认时,如果执行快重传,就执行“乘法减小”算法,把慢开始门限减半。这是为了预防网络发生拥塞,但不执行慢开始算法,开始执行快恢复算法。 

cwnd = cwnd/2 ,也就是设置为原来的一半;

ssthresh = cwnd;

拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了)

重传丢失的数据包

如果再收到重复的 ACK,那么 cwnd 增加 1

如果收到新数据的 ACK 后,设置 cwnd 为 ssthresh,接着就进入了拥塞避免算法

 当采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络超时时才使用。

如果已经建立了连接,但是客户端突然出现故障了怎么办?

如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP 有一个机制是保活机制。这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

net.ipv4.tcp_keepalive_time=7200

net.ipv4.tcp_keepalive_intvl=75  

net.ipv4.tcp_keepalive_probes=9

tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制

tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;

tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。

如果开启了 TCP 保活,需要考虑以下几种情况:

第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置等待下一个 TCP 保活时间的到来

第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

TCP传输参数优化

TCP 连接是由内核维护的,内核会为每个连接建立内存缓冲区:

如果连接的内存配置过小,就无法充分使用网络带宽,TCP 传输效率就会降低;

如果连接的内存配置过大,很容易把服务器资源耗尽,这样就会导致新连接无法建立;

因此,我们必须理解 Linux 下 TCP 内存的用途,才能正确地配置内存大小。

滑动窗口

TCP 报文发出去后,并不会立马从内存中删除,因为重传时还需要用到它。

由于 TCP 是内核维护的,所以报文存放在内核缓冲区。如果连接非常多,我们可以通过 free 命令观察到 buff/cache 内存是会增大。

如果 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。这个模式就有点像我和你面对面聊天,你一句我一句,但这种方式的缺点是效率比较低的。

按数据包进行确认应答
所以,这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。

要解决这一问题不难,并行批量发送报文,再批量确认报文即刻。

然而,这引出了另一个问题,发送方可以随心所欲的发送报文吗?当然这不现实,我们还得考虑接收方的处理能力。

当接收方硬件不如发送方,或者系统繁忙、资源紧张时,是无法瞬间处理这么多报文的。于是,这些报文只能被丢掉,使得网络效率非常低。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是滑动窗口的由来。

接收方根据它的缓冲区,可以计算出后续能够接收多少字节的报文,这个数字叫做接收窗口。当内核接收到报文时,必须用缓冲区存放它们,这样剩余缓冲区空间变小,接收窗口也就变小了;当进程调用 read 函数后,数据被读入了用户空间,内核缓冲区就被清空,这意味着主机可以接收更多的报文,接收窗口就会变大。

因此,接收窗口并不是恒定不变的,接收方会把当前可接收的大小放在 TCP 报文头部中的窗口字段,这样就可以起到窗口大小通知的作用。

发送方的窗口等价于接收方的窗口吗?如果不考虑拥塞控制,发送方的窗口大小「约等于」接收方的窗口大小,因为窗口通知报文在网络传输是存在时延的,所以是约等于的关系。

 从上图中可以看到,窗口字段只有 2 个字节,因此它最多能表达 65535 字节大小的窗口,也就是 64KB 大小。

这个窗口大小最大值,在当今高速网络下,很明显是不够用的。所以后续有了扩充窗口的方法:在 TCP 选项字段定义了窗口扩大因子,用于扩大TCP通告窗口,使 TCP 的窗口大小从 2 个字节(16 位) 扩大为 30 位,所以此时窗口的最大值可以达到 1GB(2^30)。

Linux 中打开这一功能,需要把 tcp_window_scaling 配置设为 1(默认打开):

要使用窗口扩大选项,通讯双方必须在各自的 SYN 报文中发送这个选项:

主动建立连接的一方在 SYN 报文中发送这个选项;

而被动建立连接的一方只有在收到带窗口扩大选项的 SYN 报文之后才能发送这个选项。

这样看来,只要进程能及时地调用 read 函数读取数据,并且接收缓冲区配置得足够大,那么接收窗口就可以无限地放大,发送方也就无限地提升发送速度。

这是不可能的,因为网络的传输能力是有限的,当发送方依据发送窗口,发送超过网络处理能力的报文时,路由器会直接丢弃这些报文。因此,缓冲区的内存并不是越大越好。

如果确定最大传输速度?

在前面我们知道了 TCP 的传输速度,受制于发送窗口与接收窗口,以及网络设备传输能力。其中,窗口大小由内核缓冲区大小决定。如果缓冲区与网络传输能力匹配,那么缓冲区的利用率就达到了最大化。

问题来了,如何计算网络的传输能力呢?

相信大家都知道网络是有「带宽」限制的,带宽描述的是网络传输能力,它与内核缓冲区的计量单位不同:

带宽是单位时间内的流量,表达是「速度」,比如常见的带宽 100 MB/s;

缓冲区单位是字节,当网络速度乘以时间才能得到字节数;

这里需要说一个概念,就是带宽时延积,它决定网络中飞行报文的大小,它的计算方式:

宽时延积=RTT*带宽

比如最大带宽是 100 MB/s,网络时延(RTT)是 10ms 时,意味着客户端到服务端的网络一共可以存放 100MB/s * 0.01s = 1MB 的字节。

这个 1MB 是带宽和时延的乘积,所以它就叫「带宽时延积」(缩写为 BDP,Bandwidth Delay Product)。同时,这 1MB 也表示「飞行中」的 TCP 报文大小,它们就在网络线路、路由器等网络设备上。如果飞行报文超过了 1 MB,就会导致网络过载,容易丢包。

由于发送缓冲区大小决定了发送窗口的上限,而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此,发送缓冲区不能超过「带宽时延积」。

发送缓冲区与带宽时延积的关系:

如果发送缓冲区「超过」带宽时延积,超出的部分就没办法有效的网络传输,同时导致网络过载,容易丢包;

如果发送缓冲区「小于」带宽时延积,就不能很好的发挥出网络的传输效率。

所以,发送缓冲区的大小最好是往带宽时延积靠近。

怎样发送调整缓冲区大小?

在 Linux 中发送缓冲区和接收缓冲都是可以用参数调节的。设置完后,Linux 会根据你设置的缓冲区进行动态调节。

先来看看发送缓冲区,它的范围通过 tcp_wmem 参数配置;

 上面三个数字单位都是字节,它们分别表示:

第一个数值是动态范围的最小值,4096 byte = 4K;

第二个数值是初始默认值,87380 byte ≈ 86K;

第三个数值是动态范围的最大值,4194304 byte = 4096K(4M);

发送缓冲区是自行调节的,当发送方发送的数据被确认后,并且没有新的数据要发送,就会把发送缓冲区的内存释放掉。

调节接收缓冲区范围

而接收缓冲区的调整就比较复杂一些,先来看看设置接收缓冲区范围的 tcp_rmem 参数:

上面三个数字单位都是字节,它们分别表示:

第一个数值是动态范围的最小值,表示即使在内存压力下也可以保证的最小接收缓冲区大小,4096 byte = 4K;

第二个数值是初始默认值,87380 byte ≈ 86K;

第三个数值是动态范围的最大值,6291456 byte = 6144K(6M);

接收缓冲区可以根据系统空闲内存的大小来调节接收窗口:

如果系统的空闲内存很多,就可以自动把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而提升发送方发送的传输数据数量;

反正,如果系统的内存很紧张,就会减少缓冲区,这虽然会降低传输效率,可以保证更多的并发连接正常工作;

发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能:

调节 TCP 内存范围

接收缓冲区调节时,怎么知道当前内存是否紧张或充分呢?这是通过 tcp_mem 配置完成的:

 上面三个数字单位不是字节,而是「页面大小」,1 页表示 4KB,它们分别表示:

当 TCP 内存小于第 1 个值时,不需要进行自动调节;

在第 1 和第 2 个值之间时,内核开始调节接收缓冲区的大小;

大于第 3 个值时,内核不再为 TCP 分配新内存,此时新连接是无法建立的;

一般情况下这些值是在系统启动时根据系统内存数量计算得到的。根据当前 tcp_mem 最大内存页面数是 177120,当内存为 (177120 * 4) / 1024K ≈ 692M 时,系统将无法为新的 TCP 连接分配内存,即 TCP 连接将被拒绝。

根据实际场景调节的策略

在高并发服务器中,为了兼顾网速与大量的并发连接,我们应当保证缓冲区的动态调整的最大值达到带宽时延积,而最小值保持默认的 4K 不变即可。而对于内存紧张的服务而言,调低默认值是提高并发的有效手段。

同时,如果这是网络 IO 型服务器,那么,调大 tcp_mem 的上限可以让 TCP 连接使用更多的系统内存,这有利于提升并发能力。需要注意的是,tcp_wmem 和 tcp_rmem 的单位是字节,而 tcp_mem 的单位是页面大小。而且,千万不要在 socket 上直接设置 SO_SNDBUF 或者 SO_RCVBUF,这样会关闭缓冲区的动态调整功能。

传输参数小结

 TCP 可靠性是通过 ACK 确认报文实现的,又依赖滑动窗口提升了发送速度也兼顾了接收方的处理能力。

可是,默认的滑动窗口最大值只有 64 KB,不满足当今的高速网络的要求,要想要想提升发送速度必须提升滑动窗口的上限,在 Linux 下是通过设置 tcp_window_scaling 为 1 做到的,此时最大值可高达 1GB。

滑动窗口定义了网络中飞行报文的最大字节数,当它超过带宽时延积时,网络过载,就会发生丢包。而当它小于带宽时延积时,就无法充分利用网络带宽。因此,滑动窗口的设置,必须参考带宽时延积。

内核缓冲区决定了滑动窗口的上限,缓冲区可分为:发送缓冲区 tcp_wmem 和接收缓冲区 tcp_rmem。

Linux 会对缓冲区动态调节,我们应该把缓冲区的上限设置为带宽时延积。发送缓冲区的调节功能是自动打开的,而接收缓冲区需要把 tcp_moderate_rcvbuf 设置为 1 来开启。其中,调节的依据是 TCP 内存范围 tcp_mem。

但需要注意的是,如果程序中的 socket 设置 SO_SNDBUF 和 SO_RCVBUF,则会关闭缓冲区的动态整功能,所以不建议在程序设置它俩,而是交给内核自动调整比较好。

有效配置这些参数后,既能够最大程度地保持并发性,也能让资源充裕时连接传输速度达到最大值。

提高网络利用率的规范

延迟确认应答

接收数据的主机如果每次都立刻回复确认应答的话,可能会返回一个较小的窗口。那是因为刚接收完数据,缓冲区已满。

当某个接收端收到这个小窗口的通知以后,会以它为上限发送数据,从而又降低了网络的利用率。为此,引人了一个方法,那就是收到数据以后并不立即返回确认应答,而是延迟一段时间的机制。

  • 在没有收到2x最大段长度的数据为止不做确认应答(根据操作系统的不同,有时也有不论数据大小,只要收到两个包就即刻返回确认应答的情况。)
  • 其他情况下,最大延迟0.5秒发送确认应答(很多操作系统设置为0.2秒左右)

事实上,大可不必为每一个数据段都进行一次确认应答。TCP采用滑动窗口的控制机制,因此通常确认应答少一些也无妨。TCP文件传输中,绝大多数是每两个数据段返回一次确认应答。

捎带应答

根据应用层协议,发送出去的消息到达对端,对端进行处理以后,会返回一个回执。例如,电子邮件协议的SMTP或POP、文件传输协议FTP中的连接控制部分等。这些应用协议使用同一个连接进行数据的交互。即使是使用WWW的HTTP协议,从1.1版本以后也是如此。再例如远程登录中针对输入的字符进行回送校验,也是对发送消息的一种回执。

在此类通信当中,TCP的确认应答和回执数据可以通过一个包发送。这种方式叫做捎带应答 (PiggyBack Acknowledgement)。通过这种机制,可以使收发的数据量减少。

另外,接收数据以后如果立刻返回确认应答,就无法实现捎带应答。而是将所接收的数据传给应用处理生成返回数据以后进再进行发送请求为止,必须一直等待确认应答的发送。也就是说,如果没有启用延迟确认应答就无法实现捎带应答。

延迟确认应答是能够提高网络利用率从而降低计算机处理负荷的一种较优的处理机制。

TCP粘包与拆包

TCP是个“流”协议,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;

(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;

(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;

(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

粘包和拆包原因

(1)要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;

(2)接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;

(3)要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;

(4)待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

粘包和拆包解决策略

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

消息定长。发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

设置消息边界。服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如FTP协议。

将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。
更复杂的应用层协议。

粘包的概念

粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大)

出现粘包的原因

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

发送方引起的粘包是由TCP协议本身造成的:

TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

 

TCP协议规定有MSS,如果数据包过长就会被分开传输。这样接收方就收到了粘包数据。
 

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

在代码中常见体现:

要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。

要发送的数据大于MSS,TCP在传输前将进行拆包。

要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。

接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
等等。

粘包的处理方式

当时短连接的情况下,不用考虑粘包的情况

如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包

如果双方建立长连接,需要在连接后一段时间内发送不同结构数据

发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。

可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

 

  • 8
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值