🤔 快速回忆
- TCP = Transmission Control Protocol,传输控制协议
- 是传输层协议,位于OSI模型体系第四层,向应用层提供面向连接、可靠传输的服务
- 由于是面向连接,通信双方使用 TCP 进行数据传输首先需要建立 TCP 连接
- TCP 保证交付给应用层的数据不丢失,不重复,按序交付,是 HTTP / SSH / FTP / SMTP 等应用层协议的基础
- 为了实现可靠传输,TCP 设计了 计时器 / 超时重传 / 滑动窗口 / 序号 / 确认 等机制来保证端到端的可靠传输
- 标准文档为RFC 793
✅ 可靠传输机制
可靠传输即保证交付给应用层的数据不丢失,不重复,按序交付。
为了实现可靠传输,TCP 设置如下机制:
- 对 PDU (协议数据单元) 设置序号,网络协议的设计建立在网络不可靠的假设下,因此无法保证数据一定按序到达,此时接收方需要根据序号对 PDU 做重排序
- 双方应设置对 PDU 的确认机制,接收方在收到 PDU 后应向发送方发送确认信息,让发送方知晓数据没有在链路上丢失
- 双方都设置有计时器,当设定时间内没有收到对方的确认后将重传此前已发送的数据
- 发送方应设置发送缓存和计时器,当某个已发出的 PDU 在设定的时间内没有收到对方的回复,可以认为该数据已丢失,此时需要发起重传
- 由于通信双方对数据的处理能力可能有差别,发送频率过快会使接收方处理不及导致大量数据在缓冲区堆积,过低的发送速度又会造成链路利用率不高,需要设置流量控制机制协调双方的收发
✉️ TCP Segment
TCP 数据传输的基本单位称为 TCP Segment,它由 Header 和 Payload 两部分构成。
TCP 在 Header 中设置诸如 段序号 / 确认号 / 校验和 等字段用来实现可靠传输,Header 结构如下所示:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|P|R|S|F| | | Offset| Reserved |R|C|S|S|Y|I| Window | | | |G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
各个字段的语义如下:
字段 | 长度 | 作用 |
Source Port | 16 bit | 发送端端口号 |
Destination Port | 16 bit | 接收端端口号 |
Sequence Number | 32 bit | TCP Segment 序号,在建立 TCP 连接时,都会随机生成一个起始序号(Initial Sequence Number,ISN),然后在此基础之上,每发送一个 Segment,序号便增加一 |
Acknowledgment Number | 32 bit | TCP Segment 确认号,确认号用于实现对 Segment 的确认机制,确认号是接收方当前已收到并校验无误的最大序号加一,即确认号等于 N 代表接收方已经收到了包括 N-1 在内的所有 Segment,期望接收序号为 N 的 Segment |
Data Offset | 4 bit | 该字段以四字节为单位,表示 Payload 部分的起始位置相对于 TCP Segment 的起始位置的偏移量。
若为 N 表示 Payload 相对于起始位置的偏移量为 N*32 bit,由于该字段的长度为4 bit,因此最大 Header 长度为 60 Byte。 |
Reserved | 6 bit | 保留字段,使用时应当置 0 |
URG | 1 bit | 紧急指针,当 URG = 1 时表示当前 Segment 为高优先级, 应优先发送当前的 Segment |
ACK | 1 bit | 当 ACK = 1 时, 确认号 (Acknowledgment Number) 字段有效 |
PSH | 1 bit | 告知接收方此 Segment 尽快交付给应用层,因为从 TCP 及其往下的网络协议栈通常都是由内核实现的,TCP 将接收到的数据交付给应用层需要将内核空间的数据拷贝到用户空间,操作耗时,通常 TCP 的实现会等待数据达到一定数量再交付给应用层,PSH 标志通知接收方尽快将 Segment 交付给应用层 |
RST | 1 bit | 用于释放连接,当 RST = 1 时通常说明网络发生了严重错误,应立即断开并重新连接 |
SYN | 1 bit | 用于握手阶段来同步序号,当通信方发起 TCP 握手时,应设置 SYN = 1 及 ACK = 0,对方若同意握手请求,则将响应 Segment 中的 SYN 设置为 1,将 ACK 设置为 1 |
FIN | 1 bit | 用于释放 TCP 连接 |
Window | 16 bit | 该字段指示发送方自身的接收窗口,例如发送方发出的 Segment 的确认号为 N,窗口为 M,则表示接收方从 N 算起还可以接收 M 个字节的数据,Window 用来实现 TCP 端到端的流量控制 |
Checksum | 16 bit | TCP Segment (Header 及 Payload) 的校验和,用于接收方校验接收到的数据是否有差错 |
Urgent Pointer | 16 bit | 紧急指针,该字段指示紧急数据的末尾在该 Segment 中的位置,紧急数据都放在普通的数据之前,因此该字段也可以理解为普通数据的起始位置 |
Options | 可变 | 用于存放 TCP 的选项信息,由于 TCP Header 的最大长度为 60 Byte (见 Data Offset),其中前 20 Byte 为固定 Header,因此选项字段的长度上限为 40 Byte。
选项字段可以存放的信息比较多,例如可以存放时间戳:TCP Segment 的序号长度为 32 位,TCP 发送方会随机初始化一个 ISN,一方面 ISN 本身可能很大,导致序号很快到达最大值而又绕回 0,另一方面即便 ISN 设置为 0,由于现在的网络传输速度很快,可能很快把序号用到最大值,当序号绕回 0 之后,若此时又收到了此前没有收到的大序号的 Segment,可能导致无法区分二者的新旧次序,使用时间戳选项可以实现新旧 Segment 的区分 |
Padding | 可变 | TCP Segment 是 4 字节对齐的,Options 字段的长度可能不是 4 字节的整数倍,此时使用 Padding 来填充,Padding 的值应设置为全 0 |
关于 Reserved 字段长度
由于本文基于 RFC 793,故长度为 6 bit,新的 TCP 协议中为 3 bit,只剩下3个保留位。也就是说增加了3位标志位,增加的是:
NS: "nonce sum"简写。随机和,用来保护不受发送者发送的突发的恶意隐藏报文侵害。
CWR: "Congestion Window Reduced"简写。拥塞窗口减,发送方降低它的发送速率,发送者在接收到一个带有ECE flag包时,将会使用CWR flag。
ECE: "ECN-Echo"简写。ECN 表示Explicit Congestion Notification(显式拥塞通知),发送方接收到了一个更早的拥塞通告。表示 TCPpeer有ECN能力。
🤝 TCP 连接建立
用例子看三次握手建立连接过程:
在 TCP 连接建立之前, 双方都处于 CLOSED 状态
- svr4.1037 向 bsdi.discard 发送一个 SYN 报文段 (报文段1):
- 时间:0.0秒
- 报文段内容:
SYN 1415531521:1415531521(0) <mss 1024>
1415531521
:初始序列号(Initial Sequence Number,ISN)。(0)
:表示没有数据负载(即载荷长度为0)。<mss 1024>
:最大报文段长度(Maximum Segment Size,MSS)为 1024 字节。- svr4.1037 由 CLOSED 状态转变为 SYN - SENT 状态
- bsdi.discard 向 svr4.1037 发送一个 SYN-ACK 报文段 (报文段2):
- 时间:0.002402秒(相对于起始时间的时间差为0.0024秒)
- 报文段内容:
SYN 1823083521:1823083521(0) ack 1415531522, <mss 1024>
1823083521
:初始序列号(ISN)。(0)
:表示没有数据负载(即载荷长度为0)。ack 1415531522
:确认号(Acknowledgment Number),表示期望接收的下一个序列号,是初始序列号1415531521
加 1。<mss 1024>
:最大报文段长度为 1024 字节。- bsdi.discard 由 LISTEN 状态转变为 SYN - RCVD 状态
- svr4.1037 向 bsdi.discard 发送一个 ACK 报文段 (报文段3):
- 时间:0.007224秒(相对于起始时间的时间差为0.0048秒)
- 报文段内容:
ack 1823083522
ack 1823083522
:确认号,表示期望接收的下一个序列号,是初始序列号1823083521
加 1。- svr4.1037 由 SYN - SENT 状态转变为 ESTAB - LISTEN 状态
可以看到三次握手确定了双方间包的序号、最大接受数据的大小(window)以及MSS(Maximum Segment Size)。
MSS = MTU - IP头 - TCP头,MTU表示最大传输单元,它一般为 1500 个字节。
MSS限制了TCP包携带数据的大小,它的意思就是当应用层向传输层提交数据通过TCP协议进行传输时,如果应用层的数据大于MSS就必须分段,分成多个段,逐个的发过去。
为什么不能两次握手?
如果只采用两次握手 (即一次请求与确认), 则可能会发生如下情况:
由于网络是不可靠的,主动发起连接的一方向另一方发起握手请求,但该数据可能会丢失,发送方在设定的时间内没有收到对方的握手响应,于是重传握手请求,双方进行正常的 TCP 通信并正常结束,而实际上第一次发送的握手请求并没有丢失,只是在链路上传递的时间过长,当双方的 TCP 通信结束之后接收端又收到了此前没有到达接收端的握手请求,其认为另一方再次发起了 TCP 握手,于是发出握手响应,如果只采用两次握手,则被动打开的一方在发出握手响应之后便进入了 ESTAB - LISTEN 状态,对它来说认为连接已经建立,而实际上另一方并没有发起 TCP 连接,从而造成不必要的资源浪费。
👋 TCP 连接释放
TCP 是全双工通信的可靠传输,当连接建立以后,双方可以同时收发数据,因此 TCP 连接的释放也分两部分,即释放 A → B 上的连接以及释放 B → A 上的连接。
假设 A 为首先发起连接释放的一方,整个 TCP 连接释放的过程如下:
- A 向 B 发送 FIN = 1, Sequence Number = k 的 TCP Segment, 当该 Segment 发出以后, A 由 ESTAB - LISTEN 状态转变为 FIN - WAIT - 1 状态 (第一次挥手)
- B 收到 A 发来的第一次挥手 Segment 后, 向 A 发送 ACK = 1, Acknowledgment Number = k + 1, Sequence Number = m 的 TCP Segment, 当该 Segment 发出以后, B 由 ESTAB - LISTEN 状态转变为 CLOSE - WAIT 状态 (第二次挥手)
- A 收到 B 发来的第二次挥手 Segment 之后, 便由 FIN - WAIT - 1 状态转变为 FIN - WAIT - 2 状态, 此时由 A → B 上的连接可以认为已经释放, 但 B 仍然可以给 A 发送消息
- B 如果不想关闭连接, 可以持续正常地向 A 发送 TCP Segment, 假设在某个时间点上, B 也想关闭 TCP 连接, 则 B 向 A 发送 FIN = 1, ACK = 1, Acknowledgment Number = k + 1, Sequence Number = j 的 Segment, B 发出以后, 它由 CLOSE - WAIT 状态转变为 LAST - ACK 状态 (第三次挥手)
- A 收到 B 发来的 FIN = 1 的 Segment 后, 向 B 发送 ACK = 1, Acknowledgment Number = j + 1, Sequence Number = k + 1 的 Segment, 该 Segment 发出以后, A 由 FIN - WAIT - 2 状态转变为 TIME - WAIT 状态 (第四次挥手)
- B 收到 A 发送的第四次挥手消息之后, 便由 LAST - ACK 状态转变为 CLOSED 状态, 对 B 来说, TCP 连接已彻底释放
- 但 A 仍需要等待一段时间, RFC 793 建议的等待时长为 2min * 2, 其中 2 min 是 MSL (Maximum Segment Lifetime, 即估计一个 TCP Segment 从发出以后在被接收之前在网络中存活的最长时间), 这里 A 在最后一次发出 Segment 之后仍需要等待 2 * MSL 才可以彻底释放连接
为什么 A 在发出最后一个 Segment 之后仍需等待 2 * MSL
一方面, 网络是不可靠的,A 最后发送的 Segment 可能没有到达 B,对 B 来说,当设定的时间内没有收到 A 挥手响应,将会重传此前发送的第三次挥手 Segment,如果 A 在发送完毕之后直接释放连接将会收不到 B 重传的消息;
另一方面,与 3 次握手的设置相同,等待 2 * MSL 可以保证本次 TCP 连接过程中的所有的 Segment 都从网络中消失,避免旧的 Segment 对之后的连接产生影响 (举例来说,A 与 B 使用 TCP 进行通信,双方正常收发数据,通信完成之后释放连接,主动关闭的一方没有在最后设置额外的等待时间,而是直接关闭了连接,这时可能会在网络中存在没有被交付的 Segment,而此时 A 与 B 再建立新的 TCP 连接进行新一轮的通信,此前在网络中没有交付的 Segment 可能会在本轮通信中出现,而实际上这是上轮已关闭的连接中的 Segment,为了避免这种情况,设置 2 * MSL 的等待时间可以保证上一轮连接中的 Segment 都从网络中消失)
🪟 TCP 滑动窗口
在了解滑动窗口前,我们看看 TCP 目前还有什么问题。
TCP 采用“带重传功能的肯定确认 (positive acknowledge with retransmission)”技术作为提供可靠数据传输服务的基础。
这项技术要求接收方收到数据之后向发送方回送确认信息 ACK。发送方对发出的每个分组都保存一份记录(缓存),在发送下一个分组之前等待确认信息。发送方还在送出分组的同时启动一个定时器,并在定时器的定时期满而确认信息还没有到达的情况下,重发刚才发出的分组。这称为 ARQ 机制,即 Automatic Repeat reQuest。
虽然根据 TCP 通信的双方具有同时进行双向通信的能力,但由于在接到前一个分组的确认信息之前必须推迟下一个分组的发送,随时准备重传,也就是停止等待式的 ARQ,这种简单的肯定确认协议浪费了大量宝贵的网络带宽,如果可以一次性发送多个 Segment 并一次性等待确认就能节省大量时间,也就是连续式的 ARQ。同时,如果发送方发送过快使得接收方来不及处理,也将导致后续全部的 Segment 被丢弃而没被确认,需要重新发送,如果能够及时与发送方协商收发的流量节奏就能减少不必要的浪费。
为此,TCP使用滑动窗口技术来提高网络吞吐量,同时解决端到端的流量控制。简单来说,滑动窗口技术是简单的带重传的肯定确认机制的一个更复杂的变形,它允许发送方在等待一个确认信息之前可以发送多个分组,同时规范了通信双方对流量控制的协商。此外滑动窗口机制还体现了 TCP 面向字节流的设计思路:TCP 使用以字节为单位的滑动窗口。
首先,发送方的发送缓存内的数据都可以被分为4类:
- 已发送并已收到 ACK
- 已发送但未收到 ACK
- 未发送但允许发送
- 未发送且不允许发送
而滑动窗口是一个比较形象的比喻,滑动窗口可以由窗口前沿和窗口后沿来确定:
- 发送窗口的后沿之后的部分是已发送并已收到 ACK 的 Segment 集合,对该部分的 Segment 可以从发送缓存中删除
- 发送窗口的前沿之前的部分是未发送且不允许发送的 Segment 集合
- 在发送窗口的前沿和后沿之间的便是发送方已发送但未收到 ACK、未发送但允许发送的 Segment 的集合
显而易见的,窗口大小指的是不需要等待确认应答而可以继续发送数据包的最大值。
当这窗口内的 Segment 发出并收到对方的确认之后,发送窗口的后沿便可以向前移动,接收端收到的 Segment 后,可以在回应的确认 Segment 中声明自己的接收窗口大小,若接收窗口大小不变,则当发送方发送 Segment 并收到对方确认之后可以将发送窗口整体都向后移动,若发送方将位于发送窗口内的 Segment 都已经发出了但是一直没有收到接收方的确认,则发送方的发送窗口将减小到 0,此时发送方必须等待而不能继续发送新的 Segment。
如果接收方一个个确认并让发送方一个个移动,双方都需要耗费大量的时间在这上面,因此接收方可以采用累积确认的方式,如发送方连续发送 N、N + 1、N + 2 共 3 个 TCP Segment,接收方全部接收到后可以只发送对 N + 2 的确认,当发送方收到接收方对 N + 2 的确认 (即收到的确认号为 N + 3)后,可以将发送窗口后沿连续向前滑动 3 个单位的距离。
下图中,每1000个字节表示一个数据包。发送端同时发送了3个数据包(2001-5000),接收端响应的确认应答包为“下一个发送4001”,表示接收端成功响应了前两个数据包,没有响应最后一个数据包。此时,最后一个数据包要保留在窗口中。由于窗口大小为3,发送端除了最后一个包以外,还可以继续发送下两个数据包(5001-6000和6001-7000)。窗口滑动到7001处。
🚰 TCP 流量控制
别忘了滑动窗口的流量控制才是重中之重。
在使用滑动窗口机制进行数据传输时:
- 如果窗口过小,发送端发送少量的数据包,接收端很快就处理了,并且还能处理更多的数据包。当传输比较大的数据时需要不停地等待发送方,造成很大的延迟。
- 如果窗口过大,发送端发送大量的数据包,而接收端处理不了这么多的数据包,这样,就会堵塞链路。如果丢弃这些本应该接收的数据包,又会触发重发机制。
为了避免这种现象的发生,TCP提供了流量控制:在发送数据包过程中动态调整窗口大小。
发送端第一次以窗口大小(该窗口大小是根据链路带宽的大小来决定的)发送数据包,接收端接收这些数据包,并返回确认应答,告诉发送端自己下次希望收到的数据包是多少(新的窗口大小),发送端收到确认应答包以后,将以新的窗口大小进行发送。
- 发送端根据当前链路带宽大小决定发送3个数据包,接收端接收但只能处理2个数据包,第3个数据包没有被处理。因此返回确认应答,设置窗口大小为2,告诉发送端自己现在只能处理2个数据包,下一次请发送2个数据包。
- 发送端接收到确认应答并知道接收端窗口大小为2,第3个数据包没有被处理,需要重发,所以发送2个数据包中包含之前第三个。
如果在接收端返回的确认应答包中,窗口设置为0,则表示现在不能接收任何数据。这时,发送端将不会再发送数据包,只有等待接收端发送窗口更新通知才可以继续发送数据包。如果这个更新通知在传输中丢失了,那么就可能导致无法继续通信。为了避免这样的情况发生,发送端会时不时地发送窗口探测包,该包仅有1个字节,用来获取最新的窗口大小的信息。
🔂 TCP 状态
针对上面的状态图,再看正常的TCP连接建立和关闭过程中客户端和服务器经历的状态变化:
💓 TCP 保活
对于 TCP 链接来说,他们之间一旦建立了连接,那么可以一直没有消息通讯。只要两端的主机没有被重启,则连接依然保持建立,意味着我们可以启动一个客户与服务器建立一个连接,然后离去数小时、数天、数个星期或者数月,而连接依然保持。这对于客户端来说,倒还好一点,毕竟不会有那么多的连接被占用,对于服务器来说,就是一个很糟糕的事情,这种连接无疑是一种僵尸连接,平白无辜的占用着服务器的资源,一旦这种连接非常多,服务器往往会因为连接数量的限制,导致没有办法接入新的客户端。
这个时候,其实就需要一种定时探测对端连接是否还存活的机制存在,让彼此都能知道对方的状态,是否还能继续使用。
这种机制就是TCP的保活机制。TCP设有一个保活计时器,服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,过期还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次,俗称“心跳”。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。具有保活器的TCP就是长连接。
长连接:建立一个连接,多个请求复用这个连接,一直用同一个链接传输数据。
短连接:建立一个连接,传输一个请求,发送完数据后就关闭连接。
注意,保活并不是 TCP 规范中的一部分。HostRequirements RFC 提供了3个不使用保活定时器的理由:
- 在出现短暂差错的情况下,这可能会使一个非常好的连接释放掉
- 它们耗费不必要的带宽
- 在按分组计费的情况下会在互联网上花掉更多的钱
🔁 窗口滑动的数据重发
在进行数据包传输时,难免会出现数据丢失情况。这种情况一般分为两种。
- 第一种,如果未使用滑动窗口机制,发送的数据包没有收到确认应答包,那么数据都会被重发;如果使用了滑动窗口机制,即使确认应答丢失,也不会导致数据包重发。
- 第二种,发送的数据包丢失,将导致数据包重发。
下面详细介绍使用滑动窗口机制的两种情况。又是懒得画图
- 发送端同时发送3个数据包
- 接收端返回确认应答:数据包1001-2000的确认应答丢失
- 发送端收到接收端发来的确认应答,虽然没有收到数据包1001-2000的确认应答包,但是收到了数据包2001-3000的确认应答包。判断第一次发送的3个数据包都成功到达了接收端。再次发送3个数据包
- 下面同理
- 发送端同时发送4个数据包
- 数据包2001-3000在发送过程中丢失了,没有成功到达接收端。接收端返回下一个应该发送2001数据包的确认应答
- 发送端仍然继续发送4个数据包
- 当接收端收到后面4个数据包时发现不是自己应该接收的数据包2001-3000,返回下一个应该发送2001数据包的确认应答
- 发送端连续3次收到接收端发来的下一个应该发送2001数据包的确认应答,认为数据包2001-3000丢失,重发该数据包
💭 窗口大小与吞吐量
TCP通信的最大吞吐量由窗口大小和往返时间决定。加入最大吞吐量为 ,窗口大小为 ,往返时间是 的话,那么最大吞吐量的公式如下:
假设窗口为 65535 字节, 为 0.1秒,那么最大吞吐量 如下:
以上公式表示1个TCP连接所能传输的最大吞吐量为5.2Mbps。如果建立两个以上连接同时进行传输时,这个公式的计算结果则表示每个连接的最大吞吐量。也就是说,在TCP中,与其使用一个连接传输数据,使用多个连接传输数据会达到更高的网络吞吐量。在web浏览器中一般会通过同时建立4个左右连接来提高吞吐量。
🔢 TCP 选项
典型的 TCP 选项结构:
kind (1 Byte) | length (1 Byte) | info (n Byte) |
- kind: 选项的类型,有的选项仅包含kind字段。
- length: 选项的总长度,包括 kind 和 length 字段占据的 2 Byte。
- info: 选项的具体信息。
下面看 Options 字段可以有哪些选项:
- kind=0,选项表结束(EOP)选项
一个报文段仅用一次。放在末尾用于填充,用途是说明:首部已经没有更多的消息,应用数据在下一个32位字开始处。
- kind=1,空操作(NOP)选项
没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
- kind=2,最大报文段长度(MSS)选项
TCP连接初始化时,通信双方使用该选项来协商最大报文段长度。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是1460(1500-40)字节。
- kind=3,窗口扩大因子选项
TCP连接初始化时,通信双方使用该选项来协商接收窗口的扩大因子。在TCP的头部中,接收窗口大小是用16位表示的,故最大为65535字节,但实际上TCP模块允许的接收窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。
假设TCP头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小是N*(2^M),或者说N左移M位。注意,M的取值范围是0~14。我们可以通过修改/proc/sys/net/ipv4/tcp_window_scaling内核变量来启用或关闭窗口扩大因子选项。
和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收窗口大小就是该TCP报文段的实际接收窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。
- kind=4,选择性确认(SelectiveAcknowledgment,SACK)选项
TCP通信时,如果某个TCP报文段丢失,则TCP会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP只重新发送丢失的TCP报文段,而不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。我们可以通过修改/proc/sys/net/ipv4/tcp_sack 内核变量来启用或关闭选择性确认选项。
- kind=5,SACK实际工作的选项
该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿(edgeofblock)参数包含一个4字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑选项类型和长度占用的2字节)。
- kind=8,时间戳选项。
该选项提供了较为准确的计算通信双方之间的回路时间(RoundTrip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改/proc/sys/net/ipv4/tcp_timestamps内核变量来启用或关闭时间戳选项。
⚠️ 相关攻击
SYN 泛洪,一种 TCP 拒绝服务攻击,在这种攻击中一个或多个恶意的客户端产生一系列 TCP 连接尝试(SYN报文段),并将它们发送给一台服务器,它们通常采用"伪造"的源 IP 地址。服务器会为每一条连接分配一定数量的连接资源,由于连接尚未完全建立,服务器为了维护大量的半打开连接会在耗尽自身内存后拒绝为后续的合法连接请求服务。
针对 SYN 泛洪,一种称为 SYN cookies 的机制做出以下处理:当一个 SYN 到达时,这条连接存储的大部分信息都会被编码并保存在 SYN+ACK 报文段的序列号字段。采用 SYN cookies 的目标主机不需要为进入的连接请求分配任何存储资源,只有当 SYN+ACK 报文段本身被确认后才会分配真正的内存。在这种情况下,所有重要的连接参数都能够重新获得,同时连接也能够被设置位ESTABLISHED 状态。
有一种是攻击影响路径 MTU 发现过程,伪造 ICMP PTB 消息迫使通信双方采用非常小的数据报进行传输,从而降低通信的性能。
还有种破坏甚至劫持TCP连接的攻击,该攻击通常包含的第一步是使两个之前正在通信的TCP节点"失去同步"。这样他们就使用了不正确的序列号,称为序列号攻击。至少有两种方法实现上述攻击:在连接建立过程中引发不正确的状态传输;在ESTABLISHED状态下产生额外的数据。
欺骗攻击,这类攻击所涉及的 TCP 报文段由攻击者精心定制,目的在于破坏或改变现有 TCP 连接的行为。攻击者可能生成一个伪造的重置报文段并将其发送给一个 TCP 通信节点,假设与连接相关的4元组以及校验和都是正确的,序列号也处于正确的范围,就会造成连接的任意一端失败。相关的防御技术包括:使用 TCP-AO 选项;要求重置报文段拥有一个特殊的序列号以代替处于某一范围的序列号,要求时间戳选项具有特定的数值;使用其他形式的 cookie 文件,让非关键的数据依赖于更加准确的连接信息或一个秘密数值。
🤔 一些补充
在连接建立超时的时候,连接的打开方会产生指数回退行为:客户端TCP为了建立连接而频繁发送SYN报文段,首个报文段发送后3s发送第二个,第二个报文段之后6s发送第三个,第三个报文段12s后发送第四个…
并且一些系统可配置发送初始 SYN 的次数,通常选择一个较小的数值5。比如在 Linux 中,系统配置变量
net.ipv4.tcp_syn_retries
表示了在一次主动打开申请中尝试重新发送 SYN 报文段的最大次数;net.ipv4.tcp_synack_retries
则表示响应对方的一个主动打开请求时尝试重新发送SYN+ACK 报文段的最大次数。当 TCP 发现一个到达的报文段对于相关连接而言是不正确的时候,TCP就会发送一个重置报文段,该报文段将 TCP 头部的 RST 位字段置位。重置报文段的用途:
- 针对不存在端口的连接请求:回忆之前学习的 UDP,当数据报遇到目的地不可达的情况会生成一个 ICMP 目的不可达的消息;而对于 TCP 则使用重置报文来代替相关工作
- 终止一条连接:发送 FIN 的终止连接方法称为有序释放;通过发送一个 RST 重置报文段也可以终止一条连接,被称为终止释放。终止连接可以为应用程序提供两大特性:
- 发送端任何排队的数据都将被抛弃,一个重置报文段会被立即发送出去
- 重置报文段的接收方会说明通信另一端采用了终止的方式而不是正常关闭
- 半开连接:通信一方的主机(例如服务器)奔溃的情况下,只要不尝试通过半开连接传输数据,正常工作的一端(客户端)将不会检测出另一端已经奔溃。服务器重启后,客户端再次连接,由于服务器对收到的报文无法做出处理,会响应一个重置报文段,之后两端之间的连接将被关闭以重新建立
- 时间等待错误
时间等待错误过程如下图:
如上图,当 FIN 报文被发送且客户端已经进入 TIME_WAIT 状态时,如果这时候服务器发送了一个如图上的"旧"的报文段(Seq=L-100,ACK=K-200),客户端会响应一个ACK(ACK=L,Seq=K)说明最新数据的信息,然而服务器接收到这个报文后它已经是关闭状态了,所以服务器响应一个RST报文段,告诉客户机(提前)关闭连接。
📚 参考
RFC 查找: