【计算机网络】运输层 - TCP

570 阅读20分钟

TCP 是最重要的网络协议之一,它保证了数据在网络中的可靠传输,和各网络连接对网络资源的公平使用。

系列文章:

概述

TCP 在运输层基本要求之上TCP 提供了几种附加服务,即全双工服务可靠数据传输拥塞控制

  • 全双工服务让进程可以在同一条连接中相互、同时地向彼此发送数据。
  • 可靠数据传输是面向进程的,它使得 TCP 可以正确、有序地将数据从发送端进程传递给接收端进程。
  • 拥塞控制是面向因特网的,它使得每一个通过拥塞网络链路的连接都能平等地共享网络链路带宽。

TCP 报文

TCP_Segment.jpg

TCP 报文段头部由源端口目的端口序号确认号数据偏移保留位标志位接收窗口检验和紧急指针选项数据填充组成。

  • 16 位的源端口目的端口是运输层的要求。
  • 32 位的序号和 32 位的确认号用于实现可靠的数据传输。
  • 4 位的数据偏移用于记录头部的长度,因为头部存在选项这一区域,所以头部长度是非固定的。
  • 6 位保留位用于以后的拓展。
  • 6 位标志位用于表示 TCP 报文的类型和状态。
  • 16 位接收窗口用于告诉对方自己愿意接收的字节数量。
  • 16 位校验和用于差错检验。
  • 16 位紧急指针用于指向被应用设置为“紧急”的数据,因为窗口长度最大是 16 位,所以它也是 16 位。

后面的选项用于进行一些额外的操作,填充是因为 TCP 头部的单位是 32 位,将头部长度填充到 32 位的倍数。通常选项是空的,因此 TCP 头部通常情况下的长度是 20 字节。

6 位标志位分别为 ACKRSTSYNFINCWRECEACK 于指示确认字段的值是有效的。RSTSYNFIN 用于连接的建立和终止。CWRECE 用于拥塞控制。

序号和确认号

序号是建立在传送的字节流之上的,而不是建立在传送的报文段序列之上的。因此一个报文段的序号是该报文段中数据的首字节的字节流编号。如当前报文段传送第 4000 到 5000 比特,则当前序号为起始序号 + 4000。

确认号是接收方期望从发送方接收的下一字节的序号。如当前接收到的序号为 4000,数据内容长度为 1000,则放回的确认数据报中确认号为 5001。

如果接收方接收了 0 ~ 533 和 900 ~ 1024 范围的报文,那么接收方的确认数据报中的确认号会持续为 534,只确认该流中第一个丢失的字节,因此 TCP 被称为提供累积确认(Cumulative Acknowledgement)。

建立 TCP 连接

TCP 连接与 TCP 断开这两个部分中,报文段都会直接称为报文。

TCP 连接的建立需要至少三个 TCP 报文才能建立,而这三个报文时双方交替发送的,因此这个过程也被称为“三次握手”,但实际上一点都不形象和贴切。不过由于太多人使用,也易于表达,就沿用下来了。

过程

客户端准备与服务器建立连接,此时客户端 TCP 处于 CLOSED 状态,服务器 TCP 处于 LISTEN 状态。

  1. 客户端向服务器发送连接报文,报文首部 SYN 标志位置为 1,因此称为 SYN 报文。SYN 报文中的初始序号 client_isn 是客户端随机生成并存储的。SYN 报文发送完毕客户端进入 SYN_SENT 状态。

  2. 服务器获取 SYN 报文,并为该 TCP 连接分配 TCP 缓存和变量,并向客户端发送允许连接的报文。该报文中 SYN 标志位被置为 1,ACK 标志位为 1,确认号字段为 client_isn + 1,初始序号 server_isn 由服务器随机生成。因此该报文被称为 SYNACK 报文。SYNACK 报文发送完毕服务器进入 SYN_RCVD 状态。

  3. 客户端获取 SYNACK 报文,客户端开始分配 TCP 缓存和变量,并且发送一个报文对服务器进行确认。由于连接已经建立,该报文 SYN 标志位为 0,ACK 标志位为 1,确认号为 server_isn + 1,序号为 client_isn + 1。此时客户器 TCP 进入 ESTABLISHED 状态。

  4. 服务器接收 ACK 报文,不作响应,进入 ESTABLISHED 状态。

image.png

设计思想

TCP 连接的建立设计为三次 TCP 包的原因,在于如何花最小的代价建立一个全双工的通道?

先思考如何花最小的代价建立一条单工的通道? 答案非常简单,主机 A 向主机 B 发送建立请求,主机 B 同意建立。需要 2 个报文实现。

如果我们要建立一个双工通道,那么就需要向让主机 A 建立起到主机 B 的单工通道,再让主机 B 建立起到主机 A 的单工通道。最简单的实现就是四个报文。分别是 A 到 BB 到 AB 到 AA 到 B。由于会有连续两个 B 到 A 的报文,因此可以进行优化把他们合并到一起,这就是最简单的全双工通道建立方案了。

所以仅两次握手是无法建立起全双工的连接通道的,只能建立单方向的通道。

从另一个角度来讲,当服务器发送 SYNACK 报文之后,客户端知道服务器工作正常,此时仅仅只是客户端是连接建立状态,服务器并不知道客户端是否仍在线、是否接收到 SYNACK 报文,所以还无法和客户端建立连接。但本质就是两个报文无法建立起全双工通道。

从握手的角度讨论是不对的,因为握手本来就是一个错误的比喻。

SYN 泛洪攻击

三次握手的设计存在着被 DOS 攻击的漏洞,最著名的就是 SYN 泛洪攻击。SYN 泛洪攻击是通过让大量的客户端向服务器发送 SYN 报文,使服务器由于要初始化大量 TCP 连接而导致资源耗尽。因为服务器在接收 SYN 报文时需要初始化 TCP 连接缓存和变量,再发送 SYNACK 报文,此时客户端接收到 SYNACK 报文后不回应,服务器只有等到超时才会释放这些 TCP 连接,这样服务器的资源就会在短时间内被消耗殆尽。

SYN Cookie

SYN 防洪攻击可以通过 SYN Cookie 进行有效防御。防御的方法就是,不在接收到 SYN 报文时就马上初始化 TCP 连接,等到客户端第三次握手时才初始化并直接建立 TCP 连接。而识别是哪个客户端进行的第三次握手,就是通过 SYN Cookie 实现的。

当服务器接收到 SYN 报文时,服务器不为该报文生成 TCP 连接,也不保存客户端相关的任何信息,而是将信息保存在发出去的 SYNACK 报文上。通过 SYN 报文的源 IP 地址、源端口号和本地密钥生成一个 cookie,并将该 cookie 作为 SYNACK 报文的序号 seq 发送回客户端。此时客户端响应就必须将序号加一作为 ack 携带在第三次握手的报文中。如果服务器收到 ACK 报文,而发现本地没有建立该 TCP 连接,就会使用密钥对 ack 进行解密,提取出源 IP 和源端口号并于当前报文的 IP 和端口号对比,相同则表示该第三次握手的报文合法,服务器开始建立 TCP 连接。

第三个报文用于存储数据

当服务器返回 SYNACK 报文时,客户端就可以建立起对服务器的连接了,此时是单工的通道,只能由客户端向服务器发送数据。所以从这一刻开始,客户端发送的所有报文就都是数据传输报文了,而不是带有 SYN 标志的请求连接报文。所以第三次握手的报文是一个数据报文,客户端可以向里面存储数据。

而实际上基本所有 TCP 实现,都会在第三次握手的 ACK 报文中开始传输数据。

断开 TCP 连接

TCP 连接的断开通常需要四个 TCP 包实现,由于这个过程类似于挥手告别示意,因此被称为“四次挥手”,这个比喻就比较贴切了。

但是有一个非常多人存在的误区,四次挥手不是必须的,最少是需要三个 TCP 包实现断开连接,而这种情况,在实际使用中还非常常见。

学计算机的人最重要的是思考能力,而不是单纯接受知识和背诵能力。事实上无论学什么,最重要的都是思考的能力。

过程

客户端进程准备与服务端进程终止连接,终止之前二者都处于 CONNECTED 状态。

  1. 客户端 TCP 向服务器发送终止报文,首部 FIN 标志位为 1,称为 FIN 报文,示意客户端已经做好终止连接的准备。此时客户端进入 FIN_WAIT_1 状态,处于 FIN_WAIT_1 状态的客户端依旧可以从服务器接收数据,但不会主动发送数据。

  2. 服务器接收到 FIN 报文之后,发送对该报文的 ACK 响应报文,示意服务端已接收该终止信号。此时服务器进入 CLOSE_WAIT 状态。此时服务器可能仍旧有资源需要向客户端发送,处于 CLOSE_WAIT 状态的服务器依旧可以向客户端发送资源。

  3. 服务器剩余资源发送完毕,向客户端发送 FIN 报文,示意服务端已经做好终止连接的准备。服务器进入 LAST_ACK 状态。

  4. 客户端收到 FIN 报文之后,发送 ACK 响应报文,示意客户端会断开连接。此时客户端进入 TIME_WAIT 状态,等待 2MSL 之后,客户端断开连接进入 CLOSED 状态。如果 ACK 报文丢失,则在 TIME_WAIT 状态期间客户端可以重传该报文。

  5. 服务器接收到 ACK 报文之后,不做任何响应直接关闭连接,进入 CLOSE 状态。

设计思想

断开 TCP 连接需要四个 TCP 报文,要讨论这四个报文的设计,就得思考断开应该怎么操作。全双工通道的断开,需要断开双方的连接,也就是断开 A 到 B 方向和 B 到 A 方向的连接。

断开一个 A 到 B 的连接,只需要 A 向 B 发送断开通知,B 发送确认通知即可。B 无权拒绝,因为数据是 A 到 B 的,A 想不发送数据就不发送数据。

因此断开双发连接需要进行两次上面的操作,

  1. A 通知 B。
  2. B 确认 A 的断开。
  3. B 通知 A。
  4. A 确认 B 的断开。

TCP 协议里面有一个点,就是即使设备没有建立 TCP 连接,也可以对 TCP 包进行应答,也就是 ACK,所以 A 断开 B 之后还可以对 B 进行确认。

那既然 2 和 3 步骤都是 B 发送的,为什么不合并到一起呢?

答案是当然可以啊!TCP 协议并没有禁止这么操作。之所以默认设计为四个步骤,是因为 A 断开 B 之后,只是表示 A 没有数据发送给 B 了,B 可能还有数据需要发送给 A。因此在 2 和 3 步骤之间,B 可以不断地发送数据给 A,只是 A 无法发送数据给 B 了而已。此时 A 依旧可以应答,因为应答是不需要建立连接的。等到 B 没有数据需要发送之后,再断开它到 A 的连接即可。

三次挥手

如果 A 在断开 B 连接之后,B 也没有任何数据需要发送了,B 可以将 FIN 报文和 ACK 报文合并在一起发送给 A,此时就变成了三次挥手。

大部分系统内核的 TCP 实现都是支持三次挥手的,而现实中绝大多数 TCP 连接的断开都是采用三次挥手断开的。因为通常情况下服务器就算没有数据发送给客户端,也不会主动断开连接。而客户端主动断开连接时,服务器基本上都不会有数据需要发送的。

image.png

TIME_WAIT 状态

TIME_WAIT 状态存在的意义在于,客户端的 ACK 报文可能没有传输到服务器,所以它需要确保服务器收到 ACK 报文。因为如果响应端没有收到 ACK 报文,会重传 FIN 报文,此时客户端如果接收到重传的 FIN 报文,就重传 ACK 报文。

而设置为 2MSL 的原因就是,服务器重传的 ACK 报文到达的时间最长是 2MSL。MSL 是一个报文在网络中最长的存活时间。发送端的 ACK 报文最长在 1MSL 到达服务段,因此服务端在 1MSL 时就会重传 FIN 报文,此时 FIN 报文同样最长需要 1MSL 到达客户端,因此 TIME_WAIT 等待 2MSL。

MSL 是报文最大生存时间(Maximum Segment Lifetime),是网络上任何报文能够存活的最长时间。IP 数据报头部存在一个 TTL(Time to Live),即生存时间,是该 IP 数据包可以经过的最大路由数,每经过一个路由就减少一,为 0 则被丢弃,同时发送 ICMP 报文通知源主机。MSL 是大于 TTL 的,以保证超过 MSL 时间的报文必定消失。

可靠数据传输

TCP 在 IP 的尽力而为的服务之上创建了一种可靠数据传输服务。确保了从进程的发送缓存到另一进程的接收缓存中读出的数据是无损坏、无间隙、无冗余和按序的数据流。

TCP 通过按序交付冗余确认超时重传的方式实现了可靠的数据传输服务。

  • 按序交付
    TCP 通过序号和确认号实现了按序交付,通过积累确认机制,接收端永远都会示意接收已接受到按序数据中的下一个字节。

  • 冗余确认
    当接收端接收到比所需数据序号更大的报文段时,会重复发送对丢失报文前一个报文的确认,通过冗余确认机制使得发送方知道出现丢失的报文段,此时发送端将重传该丢失的报文段。

    由于 TCP 采用积累确认机制,发送方发送连续多个报文之后,若前面某个报文发生丢失,接收方会重复发送对丢失报文前一个报文的确认,该重复确认报文称为 冗余 ACK

  • 超时重传
    发送端在每次发送报文段时都会启动计时器,若计时器超时时仍未接收到该报文段的确认,则发送端会重传该报文段。

其中有几个细节值得注意,假设 A、B、C 是发送方要连续发送的报文段:

  • 确认报文丢失
    如果接收方对 A 的确认报文段丢失,如果发送方还没有发送 B,会在 A 超时之后对 A 进行超时重传。如果发送方继续发送 B、C,接收方会对 A 进行冗余确认。

  • 超时间隔加倍
    TCP 每次超时重传,都会将下一次超时时间设置为先前的两倍。这事实上提供了形式受限的拥塞控制,因为超时很有可能是网络拥塞引起的,如果频繁重传分组,会使网络拥塞更为严重。

  • 快速重传
    当发送端收到对 A 的三个冗余 ACK 时,发送端会在 B 报文段过期前重传 B。

  • 选择确认
    如果发送方连续发送了 A、B、C,恰好 B 报文段丢失,接收端会保存 A 同时缓存 C,并对 A 进行冗余确认。此时发送发就会重传 B。当接收端接收到 B 之后,会返回对 C 的确认,跳过 B。

流量控制

TCP 提供流量控制服务(Flow-control Service)以消除发送方使接收方缓存溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率和接收方的接受速率相匹配。

接收窗口

TCP 通过让发送方维护一个接收窗口(Receive Window)来提供流量控制服务。

接收方的接收缓存大小减去已使用缓存大小,即为剩余的接收窗口大小。由于已使用接收缓存大小只有接收方知道,因此接收窗口的大小由接收方计算而出,接收方通过把接收窗口值放入 TCP 首部的 16 位窗口字段中,通知发送方剩余接收窗口的大小。发送方每次发送的数据量都必须保持小于接收窗口的大小。

当接收窗口大小为 0 时,如果不做任何操作,当接收缓存被全部读取之后,接收方无法提醒发送方窗口大小已更新,会导致发送方被阻塞而一直无法发送数据。因此 TCP 规定接收窗口为 0 时,发送方会继续发送只有一个字节数据的报文段,这些报文段在接收窗口扩大时将得到确认,此时确认报文中会携带新的接收窗口大小

拥塞控制

发送方的发送速率也可能因为网络层的拥塞而被遏制,这种控制行为被称为拥塞控制

拥塞控制与流量控制不同,拥塞控制的目的是解决发送速率大于传播链路速率的问题,流量控制的目的主要是平衡发送方和接收方的速率。

原因

网络中每条链路的吞吐量是有限的。由此网络拥堵会有 4 种负面影响:

  1. 排队时延:当分组速率接近链路容量时,分组会经历巨大的排队时延。
  2. 分组丢失:当分组由于拥塞被丢弃时,每个上游路由器用于转发该分组而使用的传输容量全部都被浪费掉了。
  3. 频繁重传:当分组由于拥塞被丢弃时,发送方必须执行重传。
  4. 路由压力:分组遇到巨大时延时,发送方会进行不必要的重传,引起路由器利用其链路带宽来转发不必要的分组副本。

拥塞控制方法

在面临网络拥塞时,根本解决方法在于使用一些方法让发送方察觉网络拥堵,让发送方降低发送速率。通常有两种方式:

  1. 端到端拥塞控制。端系统通过网络行为观察链路是否阻塞。
  2. 网络辅助的拥塞控制。网络层向发送方提供拥塞状态信息。

TCP 拥塞控制

TCP 使用端到端的拥塞控制,因为 IP 网络层不会向端系统提供显式的网络拥塞反馈,这样违反了分层协议。

TCP 发送端通过维护一个拥塞窗口变量对发送方能向网络中发送流量的速率进行限制。以实现拥塞控制机制。

拥塞感知

当 TCP 发送方接收发现丢包事件,则发送方判定传播链路发生了拥塞。丢包事件包括确认超时重复确认,具体是连续收到三个相同的 ACK。

因此可以说 TCP 是使用确认来触发拥塞控制的,这一方式使得 TCP 被说成是自计时(Self-clocking)的。

拥塞控制算法

TCP 拥塞控制算法分为三个主要部分:1. 慢启动;2.拥塞避免;3. 快速恢复。慢启动和拥塞避免是 TCP 强制部分,快速恢复是推荐部分,并非必需。

  1. 慢启动
    慢启动过程中,TCP 发送方的目的是希望快速找到可用带宽的数量。拥塞窗口起始值为 1 最大报文段长度(Maximum Segment Size,MSS),每收到一个确认报文窗口就增加 1MSS。因此每经过一个往返时延(Round Trip Time,RTT),拥塞窗口的大小就会增加一倍。因此慢启动过程中拥塞窗口的大小以指数方式扩大。

    当发生丢包事件时,TCP 发送方就将拥塞窗口重新置为 1 并重新开始慢启动,并将上一次慢启动中最大窗口长度的一半设置为当前慢启动阈值。当慢启动窗口长度到达或者超过该阈值时,进入拥塞避免模式。

  2. 拥塞避免
    进入拥塞避免模式时,拥塞窗口长度是上次拥塞时窗口长度的一半或以上,表明现在很大概率将发生阻塞。因此此时窗口长度不再翻倍,而是采用每个往返时延都增加一个最大报文段长度的方式来拓展窗口。

    实现的方式通常采用每次到达一个新的确认,发送方就将窗口扩大 MSS/cwnd (cwnd 指拥塞窗口长度)字节的方式,当一个 RTT 结束时,由于 cwnd * (MSS / cwnd) = MSS,窗口只增加一个最大报文段长度。

    窗口在线性增长的过程中,同样会遇到丢包事件,此时对于不同丢包事件会有不同的决策。

    • 对于超时事件,与慢启动遇到阻塞一致,将当前窗口大小一半设置为当前慢启动阈值,将窗口长度重置为 1 并重新进入慢启动过程。
    • 对于冗余 ACK 事件,将当前窗口大小减半并加 3(加上 3 个冗余 ACK),并进入快速恢复阶段。
  3. 快速恢复
    快速恢复阶段中,对于引起 TCP 进入快速恢复状态的缺失报文段,对收到的关于该报文段的每个冗余 ACK,都会将拥塞窗口长度增加 1 个 MSS。最终,直到丢失报文段的 ACK 到达时,发送方减低拥塞窗口大小后进入拥塞避免状态。如果发生超时事件,则将窗口大小一半设置为慢启动阈值并进入慢启动状态。

从整体看,TCP 拥塞控制主要是:每个往返时延后增加 1 个最大报文段长度,直到出现超时事件,就将窗口减半。因此 TCP 拥塞控制常被称为加性增、乘性减的拥塞控制方式。该拥塞控制算法的优异之处在于能够在维持较好的吞吐量的同时避免拥塞。

其他特点

TCP 重试机制