文章目录
前言
由于内容细致,导致篇幅过长,因此将分为三部分来讲述,目录如下:
【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(上)
- 概述;
- 三握四挥;
- 重传机制;
- 粘包拆包;(TCP 是基于流的,其实没这表述的)
【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(中)
- 滑动窗口;
- 流量控制;
- 拥塞控制;
- 状态机;
【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(下)
- 握手失败;
- 挥手失败;
- 为什么是三次握手?
- 如何避免 SYN 攻击?
- MTU 与 MSS 那些事儿;
- TIME_WAIT 的巧妙设计;
- 初始序列号 ISN 为什么不同?
- 知道 TCP 的最大连接数吗?
概述
传输控制协议(TCP,Transmission Control Protocol) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义;
TCP 旨在适应支持多网络应用的分层协议层次结构。 连接到不同但互连的计算机通信网络的主计算机中的成对进程之间依靠 TCP 提供可靠的通信服务。TCP 假设它可以从较低级别的协议获得简单的,可能不可靠的数据报服务。 原则上,TCP 应该能够在从硬线连接到分组交换或电路交换网络的各种通信系统之上操作。
TCP 是一种面向广域网的通信协议,目的是在跨越多个网络通信时,为两个通信端点之间提供一条具有下列特点的通信方式:
(1)基于流的方式;
(2)面向连接;
(3)可靠通信方式;
(4)在网络状况不佳的时候尽量降低系统由于重传带来的带宽开销;
(5)通信连接维护是面向通信的两个端点的,而不考虑中间网段和节点。
为满足 TCP 协议的这些特点,TCP 协议做了如下的规定:
① 数据分片:在发送端对用户数据进行分片,在接收端进行重组,由 TCP 确定分片的大小并控制分片和重组;
② 到达确认:接收端接收到分片数据时,根据分片数据序号向发送端发送一个确认;
③ 超时重发:发送方在发送分片时启动超时定时器,如果在定时器超时之后没有收到相应的确认,重发分片;
④ 滑动窗口:TCP 连接每一方的接收缓冲空间大小都固定,接收端只允许另一端发送接收端缓冲区所能接纳的数据,TCP 在滑动窗口的基础上提供流量控制,防止较快主机致使较慢主机的缓冲区溢出;
⑤ 失序处理:作为 IP 数据报来传输的 TCP 分片到达时可能会失序,TCP 将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层;
⑥ 重复处理:作为 IP 数据报来传输的 TCP 分片会发生重复,TCP 的接收端必须丢弃重复的数据;
⑦ 数据校验:TCP 将保持它首部和数据的检验和,这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到分片的检验和有差错,TCP 将丢弃这个分片,并不确认收到此报文段导致对端超时并重发。
基于流的方式
TCP 是一种字节流(byte-stream)协议,流的含义是没有固定的报文边界。
当用户消息通过 TCP 协议传输时,一条消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输,也有可能将多条消息组成一个 TCP 报文进行传输。
这是因为在发送端,当我们调用
send
函数完成数据 发送 以后,数据并没有真正从网络上发送出去,而是从应用程序拷贝到了操作系统内核协议栈中,至于什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等条件。也就是说,我们不能认为每次
send
调用发送的数据,都会作为一个整体完整地消息被发送出去。
这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的;
举个例子,比如发送端陆续调用
send
函数先后发送消息 「
Hello World,
」「
sid10t.
」,
那么考虑实际网络传输过程中的各种影响,在实际发送过程中可能会出现以下几种情况,不考虑两个报文的实际大小 :
- 情况一: 正常发送,两个报文大小均小于协商的 MSS,且又符合真正发送条件;
- 情况二: 合并发送,TCP 将两个数据包打包成一个 TCP 报文发送出去;
- 情况三: 前拆分,后合并;
- 情况四: 前合并,后拆分;
面向连接
连接与断开
- 连接:接收端 在自己的监听端口接收到连接请求,三次握手 之后,维护一定的数据结构和 发送端 的信息,如果确认了该信息:接收端 发送的内容会被 发送端 接收, 发送端 发送的内容也会被 接收端 接收,直至连接断开。
- 断开:通过 四次挥手 确保双方都知道且都同意对方断开连接,然后
remove
为对方维护的数据结构和信息,对方之后发送的数据包也不会被接收,直到再次建立连接。
本质是数据结构
TCP 建立连接的本质是在客户端和服务端各自维护一定的数据结构(一种状态机),来记录和维护这个 连接 的状态,并不是真的在这两个端之间有一条类似 “网络专线” 的东西。
在 IP 层,网络情况该不稳定还是不稳定,数据传输走的什么路径不是上层所能控制的,TCP 能优化的就只有做更多判断,重试,拥塞控制之类的东西。
连接只是术语
数据包最终都是通过链路层、物理层等一层一层出去的,所以连接只是一种逻辑术语,并不存在像管道那样子的东西,连接 在这里相当于双方的一种约定,双方按协商的规矩维护状态。
维护状态
三次握手之后,客户端与服务端之间能够确认自己发送的数据能被对方所接收,因此只需要维护这种状态就可以了,连接就此建立;
连接是一种状态,建立连接是维持一种状态,维持状态通过一定的数据结构来完成;
口诀
我心里有你,并不是我心里真的有你(连接并不是像管子一样真的连着),而是一种感觉,我每天想着你,你占据着我内心的一份空间(数据结构),是一种状态,我需要一直维护,天天想你(状态机)。你生气的时候,我殷勤一点,你高兴的时候,我放松一点(拥塞控制,快慢有度),直到我遇到了另一个她,再见(删除数据结构),唉,男人!
可靠通信方式
IP 是一种无连接、不可靠的协议:它尽最大可能将数据报从发送者传输给接收者,但并不保证包到达的顺序会与它们被传输的顺序一致,也不保证包是否重复,甚至都不保证包是否会达到接收者。不保证有序、去重、完整。
- 数据分片;
- 到达确认;
- 超时重发;
- 滑动窗口;
- 失序处理;
- 重复处理;
- 数据校验;
- 拥塞控制;
采用全双工协议
全双工通信又称为双向同时通信,即通信的双方可以同时发送和接收信息的信息交互方式。 RS-422 标准就是全双工通信标准。 全双工(Full Duplex)是在微处理器与外围设备之间采用发送线和接受线各自独立的方法,可以使数据在两个方向上同时进行传送操作。
在 TCP 中发送端和接收端可以是客户端/服务端,也可以是服务器/客户端,通信的双方在任意时刻既可以接收数据也可以发送数据,每个方向的数据流都独立管理序列号、滑动窗口大小、MSS 等信息。
报文首部
- 序列号(Sequence Number):在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
- 确认应答号(Acknowledgement Number):指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。
- 控制位:
ACK
:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
RST
:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
SYN
:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
FIN
:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
URG
:紧急标志位,表示的是此报文段中有紧急数据,将紧急数据排在普通数据的前面;当接受端收到此报文后后必须先处理紧急数据,而后再处理普通数据。
PSH
:催促标志位,当发送端将 PSH 置为1时,TCP会立即创建一个报文并发送。接受端收到 PSH 为1的报文后就立即将接受缓冲区内数据向上交付给应用程序,而不是等待缓冲区满后再交付。
- 窗口大小:用于 TCP 流量控制。告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
- 校验和:由发送端填充,接收端对 TCP 报文段执行 CRC 算法以检验 TCP 报文段在传输过程中是否损坏,检验的范围包括头部、数据两部分,是 TCP 可靠传输的一个重要保障。
- 紧急指针:一个正的偏移量。它和序列号字段的值相加表示最后一个紧急数据的下一个字节的序列号,用于发送端向接收端发送紧急数据。
三握四挥
在对 TCP 有一定的了解之后,那我们就进入正题,TCP 是面向连接的,那么 TCP 是如何建立连接的呢,这也是面试热点问题,TCP 的三次握手与四次挥手,不过在正式介绍三握四挥之前,先在 Linux 上安装网络协议栈测试神器 packetdrill,方便后续的实验操作;
packetdrill
packetdrill 是 Google 开源的一个 测试脚本工具,可以用于测试 TCP、UDP、IP 网络协议栈,其是由基于时间序的脚本行组成,按时间顺序逐条执行。
它的语言设计十分接近于 tcpdump 和 strace ,包含四种类型的语句:
- 数据包。使用类似于 tcpdump 的语法,支持 TCP、UDP、ICMP 数据包,同时也提供了常见 TCP 选项的配置,包括 SACK、MSS、window scale 等。
- 系统调用。使用类似于 strace 的语法。
- Shell 命令。通过``进行调用,可以进行系统参数配置或断言验证网络协议栈状态。
- Python 脚本。通过
%{ command }%
进行调用,可以输出或者断言验证 TCP 状态。
关于 packetdrill 的安装配置博主是参考这篇文章的 packetdrill 工具安装,官方 github 地址:google/packetdrill;
安装完成之后,可以测试一下
tests/linux/fast_retransmit/
目录下的
fr-4pkt-sack-linux.pkt
,这是官方提供的测试脚本:
./packetdrill tests/linux/fast_retransmit/fr-4pkt-sack-linux.pkt
注意:在自己写脚本时,需要确保行尾序列为
LF
,不然就等着怀疑人生吧,谁用谁知道;一模一样复制过去的代码,因为行尾序列不一样,一直在报错:
如果什么回显都没有就表示成功了,不然按照报错进行更改;
因为 socket 绑定的端口默认是 8080,因此可以通过 tcpdump 进行监听,
tcpdump -t -i any port 8080
,如果你知道自己的网卡,比如是
eth0
,那就把
any
换成
eth0
;
如果你觉得 tcpdump 用的不太适应,也可以将数据映射到 Wireshark 中去,在 Wireshark 文件夹下打开 cmd 并使用下列指令,
ssh root@host -p port "tcpdump -i any -n tcp port 8080 -s 0 -l -w -" | "Wireshark.exe" -k -i -
那接下来就让我们探究一下 TCP 是如何建立连接的吧;
三次握手
*握手之前:客户端和服务端都处于
CLOSED
状态。先是服务端主动监听某个端口,处于
LISTEN
状态;*
第一次握手
客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序列号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于
SYN-SENT
状态。
第一个报文—— SYN 报文如下图所示:
第二次握手
服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序列号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于
SYN-RCVD
状态。
第二个报文 —— SYN + ACK 报文如下图所示:
第三次握手
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于
ESTABLISHED
状态。
第三个报文 —— ACK 报文如下图所示:
*握手之后:服务器收到客户端的应答报文后,也进入
ESTABLISHED
状态;*
从上面的过程可以发现第三次握手是可以携带数据的(前两次握手是不可以携带数据),服务器必须等收到 ACK 分组之后才能发送数据,这也是面试常问的题。
一旦完成三次握手,双方都处于
ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
四次挥手
天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过四次挥手方式。
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。
第一次挥手
客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入
FIN_WAIT_1
状态。
第二次挥手
服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入
CLOSED_WAIT
状态。
客户端收到服务端的 ACK 应答报文后,之后进入
FIN_WAIT_2
状态。
第三次挥手
等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入
LAST_ACK
状态。
第四次挥手
客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入
TIME_WAIT
状态。
服务器收到了 ACK 应答报文后,就进入了
CLOSED
状态,至此服务端已经完成连接的关闭。
客户端在经过
2MSL
一段时间后,自动进入
CLOSED
状态,至此客户端也完成连接的关闭。
*每个方向都需要一个 FIN 和一个 ACK,因此通常被称为 四次挥手,不过需要注意的是,**主动关闭连接的,才有
TIME_WAIT
状态*。
场景复现
在 Linux 可以通过
netstat -napt
命令查看 TCP 的连接状态:
根据上述的理论,利用 packetdrill 自行构建环境,下面博主只是构造了三次握手和四次挥手的过程,并没有中间的数据传输过程:
0socket(..., SOCK_STREAM, IPPROTO_TCP)=3+0setsockopt(3, SOL_SOCKET, SO_REUSEADDR,[1],4)=0+0bind(3,...,...)=0+0listen(3,1)=0+0 `echo socket is listening!`
+0< S 0:0(0) win 4000<mss 100>+0> S.0:0(0) ack 1<...>+.1<.1:1(0) ack 1 win 1000+0accept(3,...,...)=4+0 `echo connection established!`
+0< F.1:1(0) ack 1 win 1000+0>.1:1(0) ack 2+0.1close(4)=0+0> F.1:1(0) ack 2<...>+0.01<.2:2(0) ack 2 win 1000+0 `echo connection closed!`
+0 `sleep 100`
思考
现实中的网络环境并不是这么一帆风顺的,总会有各种的意外情况发生,毕竟现实是骨感的;
例如在第二次握手时,服务端回复的 ACK 报文丢失了,那应该怎么处理呢?是客户端重发 SYN 报文还是服务端重发 ACK 报文呢?以及其他的各种问题;
预知后事如何,请客官移步 【网络协议】万文长篇,带你深入理解 TCP;场景复现,掌握鲜为人知的细节(下);
重传机制
TCP 实现可靠传输的方式之一,是通过序列号与确认应答。
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息。
但在错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?
所以 TCP 针对数据包丢失的情况,会用重传机制解决。接下来说说常见的重传机制:
- 超时重传;
- 快速重传;
- SACK;
- D-SACK;
超时重传
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。
TCP 会在以下两种情况发生超时重传:
- 数据包丢失;
- 确认应答包丢失;
超时时间应该设置为多少呢?
我们先来了解一下什么是
RTT
(Round-Trip Time 往返时延),从下图我们就可以知道:
RTT 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。
超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。
假设在重传的情况下,超时时间 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 的策略是超时间隔加倍。也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
场景复现
超时触发重传存在的问题是,超时周期可能相对较长,那是不是可以有更快的方式呢?
于是就可以用「快速重传」机制来解决超时重发的时间等待。
快速重传
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 。
场景复现
+0socket(..., SOCK_STREAM, IPPROTO_TCP)=3+0setsockopt(3, SOL_SOCKET, SO_REUSEADDR,[1],4)=0+0bind(3,...,...)=0+0listen(3,1)=0+0< S 0:0(0) win 32792<mss 1000, sackOK, nop, nop, nop, wscale 7>+0> S.0:0(0) ack 1<...>+.1<.1:1(0) ack 1 win 257+0accept(3,...,...)=4+0write(4,...,1999)=1999+0> P.1:2000(1999) ack 1+.1<.1:1(0) ack 2000 win 257+0write(4,...,4000)=4000+0> P.2000:6000(4000) ack 1+0 `echo 丢失 "seq 3000:4000" 的包`
+.1<.1:1(0) ack 3000 win 257+.0<.1:1(0) ack 3000 win 257+.0<.1:1(0) ack 3000 win 257+3.3<.1:1(0) ack 6000 win 257+0 `echo finish`
+0 `sleep 5`
这里模拟丢失了
seq 3000:4000
的数据包,通过回复三个 dup ack 来触发 TCP的快速重传机制,使得服务端重发
seq 3000:4000
的数据包,但是碰到了有趣的事情,不知道为什么
[P.], seq 5000:6000
也进行了重传;
所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。
比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 ACK 2 是谁传回来的。
根据 TCP 不同的实现,以上两种情况都是有可能的。可见,这是一把双刃剑。
为了解决不知道该重传哪些 TCP 报文,于是就有
SACK
方法。
SACK 方法
还有一种实现重传机制的方式叫:
SACK
( Selective Acknowledgment 选择性确认)。
这种方式需要在 TCP 头部「选项」字段里加一个
SACK
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重传机制,通过 SACK 信息发现只有 200~299 这段数据丢失,那么在重传时,就只选择了这个 TCP 段进行重传。
场景复现
注意
MSS
的长度,将会对这个过程造成一定的影响;
+0socket(..., SOCK_STREAM, IPPROTO_TCP)=3+0setsockopt(3, SOL_SOCKET, SO_REUSEADDR,[1],4)=0+0bind(3,...,...)=0+0listen(3,1)=0+0< S 0:0(0) win 32792<mss 100, sackOK, nop, nop, nop, wscale 7>+0> S.0:0(0) ack 1<...>+.1<.1:1(0) ack 1 win 257+0accept(3,...,...)=4+0write(4,...,199)=199+0> P.1:200(199) ack 1+.1<.1:1(0) ack 200 win 257+0write(4,...,400)=400+0> P.200:600(400) ack 1+0 `echo 丢失 "seq 200:300" 的包`
+.1<.1:1(0) ack 200 win 257<sack 300:400,nop,nop>+0<.1:1(0) ack 200 win 257<sack 300:500,nop,nop>+0<.1:1(0) ack 200 win 257<sack 300:600,nop,nop>+.1<.1:1(0) ack 600 win 257+0 `echo finish`
如果要支持
SACK
,必须双方都要支持。在 Linux 下,可以通过
net.ipv4.tcp_sack
参数打开这个功能(Linux 2.4 后默认打开)。
Duplicate 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 后默认打开)。
粘包拆包
TCP 是基于流的,其实没这表述的,这里就只是解释一下,看到有很多说 TCP 粘包头头是道的,懂得都懂;
TCP 其实就是背锅侠,多个数据包粘连到一起无法拆分是我们的需求过于复杂造成的,是程序猿的问题而不是协议的问题,TCP 协议表示这锅它不想背。
服务器端如果想保证每次都能接收到客户端发送过来的这个不定长度的数据包,程序猿应该如何解决这个问题呢?下面提供几种解决方案:
- 使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包;
- 在每条数据的尾部添加特殊字符,如果遇到特殊字符,代表当条数据接收完毕了; - 有缺陷:效率低,需要一个字节一个字节接收,接收一个字节判断一次,判断是不是那个特殊字符串;
- 在发送数据块之前,在数据块最前边添加一个固定大小的数据头,这时候数据由两部分组成:数据头 + 数据块: - 数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节;- 数据块:当前数据包的内容;
后记
欢迎各位大佬指正,在评论区多多讨论;
站在巨人的肩膀上看 TCP,感谢参考:
- 都说 TCP 是面向连接的,怎样才算是一个连接呢?
- 如何理解是 TCP 面向字节流协议?
- TCP 、UDP、IP包的最大长度
- 【笔记】深入理解 TCP 协议:从原理到实战
- packetdrill 工具安装
- TCP 三次握手和四次挥手详解
- 30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制
版权归原作者 sid10t. 所有, 如有侵权,请联系我们删除。