运输层
因特网的运输层在应用程序端点之间传送应用层报文,运输层分组称为报文段(segment)。运输层协议运行在端系统中,为运行在不同主机上的应用进程之间提供了逻辑通信功能。
3.2 多路复用与多路分解
一个进程有一个或多个套接字(socket)。将运输层报文段中的数据交付到正确的套接字的工作称为多路分解。在源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(这将在以后用于分解)从而生成报文段,然后将报文段传递到网络层,所有这些工作称为多路复用。
要实施正确的多路复用与多路分解,每个套接字都需要唯一的标识符以区分彼此,而 UDP 与 TCP 的套接字标识符又有所不同。两者共有的标识部分,是运输层报文段的首部信息中的端口号字段(包括源端口号字段和目的端口号字段)。端口号是一个 16 比特的数,其大小在 0 ~ 65535 之间。0 ~ 1023 范围的端口号称为周知端口号,保留给周知应用层协议使用。其余的端口号用于客户端进程,通常由运输层自动为客户端从中选取。
- UDP
一个 UDP 套接字由一个二元组 (目的 IP 地址,目的端口号) 标识的。因此,如果两个 UDP 报文段具有不同的源 IP 地址/源端口号,而有相同的目的 IP 地址和目的端口号,那么这两个报文将被指向同一个套接字。
UDP 套接字标识并未使用到首部信息中的源端口号字段,但该字段仍有存在的必要:用作返回地址的一部分。
- TCP
一个 TCP 套接字由一个四元组 **(源 IP 地址,源端口号,目的 IP 地址,目的端口号)**标识的。因此,如果两个 TCP 报文段具有不同的源 IP 地址/源端口号,即使有相同的目的 IP 地址和目的端口号,也将被指向不同的套接字(例外状态是,服务器准备了一个特殊套接字用于初始创建连接,所以为创建连接的、来源不同的 TCP 报文段,将被指向同一个套接字。当连接建立完成后,来源不同的客户将具有各自不同的套接字。此时,来源不同的 TCP 报文段就会被指向不同的套接字了)。
3.3 无连接运输:UDP
UDP 只做了运输层协议能做的最少的工作:复用/分解和少量的差错检测。
选择 UDP 的应用,主要考虑到
- 能够更自由地发送数据,不必受限于一些拥塞控制机制;
- 无需建立连接,也就不会有建立连接的时延;
- 无连接状态,也就不会有维护连接的资源消耗;
- 分组首部开销小,仅 8 字节(相比 TCP 的 20 字节)。
UDP 报文段结构
UDP 首部只有 4 个字段,每个字段由两个字节组成。源端口号用于返信,目的端口号用于分解,长度指示 UDP 报文段的字节数(首部 + 数据),检验和用于差错检测。
UDP 检验和
发送方将 UDP 报文段(事实上,还要加上 IP 数据报的首部,称为伪首部)看作若干个 16 比特字,对这些字的和取反码(溢出时回卷,即加回到最低位),得到的结果放到 UDP 报文段的检验和字段。
接收方重复发送方的计算过程,并将结果与报文段中的检验和字段值相加,若无差错,结果将为 1111111111111111。
许多链路层协议都提供了差错检测,那么 UDP 为什么还要提供检验和呢?原因是,不能保证源和目的之间的所有链路都提供差错检测,也不能保证报文段在路由器内存中时不引入比特差错。指望较低层次提供差错检测(或其他功能)是徒劳的,因此,UDP 就必须在端与端之间实现差错检测(或其他功能)。这被称作 端到端原则(end2end principle)。
虽然 UDP 提供差错检测,但它无法恢复差错。UDP 只能丢弃受损的报文段。
3.4 可靠数据传输原理
经完全可靠信道的可靠数据传输:rdt 1.0
既然下层信道完全可靠,那就没什么好说的了。
经具有比特差错信道的可靠数据传输:rdt 2.0
在分组中的比特可能受损的情况下,可以采用自动重传请求协议(ARQ)来确保可靠传输。ARQ 由三个重要机制构成:
- 差错检测。接收方需要能够检测出分组是否出错,如 UDP 的检验和。
- 接收方反馈。接收方通过回送肯定确认(ACK/1)与否定确认(NAK/0)反馈接收信息。
- 重传。接收方受到由差错的分组时,发送方将重传该分组。
rdt 2.0 是个标准的 ARQ 协议。
rdt 2.0 的发送方初始时等待上层的调用。当调用发生时,发送方接收下传的数据,将其封装为带有检验和的分组,送往接收方。一旦发送完毕,发送方进入等待反馈状态,根据接收方反馈的信息来决定是重传(收到 NAK)还是进入等待调用状态(收到 ACK)。
接收方则无脑接收分组。若分组未出差错,接收方将分组信息抽出,传给上层,并向接收方反馈 ACK,表明当前分组顺利接收;若出错,则反馈 NAK。
注意到,当发送方处于等待反馈状态时,它无法接收上层的数据,直到接收到接收方反馈的 ACK。我们称这样的行为为停等,rdt 2.0 因此是个停等协议。
rdt 2.0 有一个致命缺陷:ACK/NAK 分组可能受损。要解决这个问题,有三种解决方案:
- 发送方请求重复确认。可惜,请求重复确认分组同样可能受损:这将把我们引向哲学。
- 增强差错检测与差错恢复。对于会产生差错而不会丢包的信道,这很好。
- 当发送方收到含糊不清的 ACK/NAK 分组时,重传当前分组。然而,这引入了冗余分组,接收方无法得知接收到的分组是否是一次重传。
解决方案三的简单方法是,在分组中添加序号字段,接收方仅需检查序号即可得知接收到的分组是否是一次重传。此时,我们得到了 rdt 2.1 版本。
rdt 2.1
对于停等协议这种简单情况,1 比特序号就足矣。发送方交替使用序号 0 和 1 来发送分组,而接收方并不需要使用序号来标注 ACK/NAK,因为发送方知道所接收到的 ACK/NAK(无论是否受损)是为响应其最近发送的数据分组而生成的。
要证实这点,想象当处于等待分组 0 的接收方,接收到了一个序号为 1 的分组,这个序号为 1 的分组必定是上一个分组,是接收方已经顺利接收到了的分组(否则接收方就不会从等待分组 1 状态转移到等待分组 0 状态)。此时,接收方就知道,自己的反馈受损了,发送方此时还处在等待反馈 1 状态,需要接收到 ACK 才能发送序号为 0 的分组,也就是接收方当前想要得到的分组。那么,接收方就只要发送 ACK 就好了。既然接收方能够从分组序号推断出发送方的状态,发送方只要无脑根据 ACK/NAK 来转移状态就好了。
rdt 2.2
如果不发送 NAK,而是对上次正确接收的分组发送一个 ACK,也能实现与 NAK 一样的效果。发送方接收到对同一个分组的两个 ACK (即冗余 ACK)后,就知道接收方没有正确接收到跟在被确认两次的分组后面的分组。这时,接收方就需要在 ACK 中包括确认接收到的分组的序号了,发送方也需要检查 ACK 中被确认的分组的序号。
经具有比特差错的丢包信道的可靠数据传输:rdt 3.0
信道丢包在现实中很常见,这意味着发送方发送的分组与接收方反馈的 ACK 都有可能丢失。要解决这个问题,最简单的方法,就是重传。如果发送方发送的分组丢失,发送方重传就再好不过了;如果接收方反馈的 ACK 丢失,这意味着接收方已收到完好的分组,而此时发送方重传分组,就引入了冗余数据分组。好在,rdt 2.2 协议利用序号就能解决冗余分组的问题(接收到受损的分组或冗余分组,直接反馈带有接收到的上一个分组的序号的ACK)。
重传能够解决问题,但引入了新的问题:如何确定重传需要等待的时间?可以知道的是,至少需要一个往返时延加上接收方处理一个分组所需的时间,但这仍然难以估计。
为了实现基于时间的重传机制,需要一个倒计数定时器,超出给定的时间后,可中断发送方。因此,发送方需要能做到:
- 每次发送一个分组(包括第一次分组和重传分组)时,便启动定时器;
- 响应定时器中断(采取适当的动作,比如重传);
- 终止定时器。
rdt 3.0 的确可靠,但面临着低效的问题。定义发送方(或信道)的利用率为:发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比。那么,定义 rdt 3.0 的发送方利用率 U = L / R R T T + L / R U = \frac{L/R}{RTT+L/R} U=RTT+L/RL/R 。这个公式很显然。L/R 通常很小(微秒),RTT 相对来说大得多(毫秒),将量级带入式子,我们发现利用率低得可怜。
要提高性能,一种技术是流水线:不采用停等,允许发送方发送多个分组而无需等待确认。采用流水线,协议需要做出以下改进:
- 增加序号范围。流水线提高了同时在信道中传播的分组。
- 发送方与接收方需要缓存多个分组。发送方至少要缓存已发送但未被确认的分组;接收方至少要缓存那些已正确接收的分组。
根据差错恢复的不同处理方式,有两种流水线协议:回退 N 步(GBN)和选择重传(SR)。
GBN
实践中,分组的序号承载在分组首部的一个固定长度 k 的字段中,序号空间 [0, 2 k − 1 2^k-1 2k−1] 被循环使用。定义以下变量
基序号 base:最早的未确认分组的序号
下一个序号 nextseqnum:最小的未使用的序号(即下一个待发分组的序号)
窗口长度 N:从基序号开始,已被发送但还未被确认的分组的许可序号范围
于是,整个序号空间被分割成 4 段:
-
[0, base-1]:已经发送并被确认的分组
-
[base, nextseqnum-1]:已经发送但未被确认的分组
-
[nextseqnum, base+N-1]:将要发送的分组能够使用的序号
-
[base+N, MAX_SEQ_NUM-1]:暂时无法使用的序号,直到当前流水线中未被确认的分组(特别是序号为 base 的分组)得到确认
窗口被限定长度,且随着分组被确认而移动,这使得 GBN 协议也被称为滑动窗口协议。
接收方的动作很简单:如果序号为 n 的分组完好且按序(上一个接收到的分组序号为 n-1)被接收到,则接收方发送一个序号为 n 的 ACK,并将分组交付上层;在其他情况下,接收方丢弃该分组,并发送一个序号为最近按序接收到的分组的 ACK。这种方式称为累积确认。
发送方需要响应三种事件:
- 上层的调用。上层希望发送数据时,首先检查发送窗口是否已满,只有窗口未满时可以发送新的分组。当发送的分组之前所有发送的分组都已被确认时,开启定时器。
- 收到 ACK。发送方接收到序号为 n 的 ACK,意味着序号在 n 以前且包括 n 的分组都已被正确接收到了。如果已发送但未被确认的分组都已被确认,则关停定时器,否则,重置定时器(要注意,GBN 只有一个定时器,这个定时器为最早发送的但未被确认的分组计时)。
- 超时。如果出现超时,发送方重传所有已发送但未被确认的分组,即序号为 [base, nextseqnum-1] 范围内的分组。由于这个范围最大长度为 N,所以 “回退 N 步“ 的名称也就不言而喻了。
SR
GBN 存在着性能问题,单个分组的差错就能引起大量的重传,而许多分组本不必重传。选择重传协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。这种个别的、按需的重传要求接收方逐个地确认正确接收的分组,也就是说,SR 对失序的分组也会反馈 ACK。
发送方需要响应三种事件:
- 上层的调用。与 GBN 一样,已发送但未被确认的分组数量不能超过窗口长度。
- 超时。GBN 只有一个定时器,SR 必须为每个分组配置一个定时器,因为超时发生后只能重传一个分组。
- 收到 ACK。如果收到 ACK,若该分组序号在窗口内,则发送方将那个被确认的分组标记为已接收。若该分组的序号等于 send_base,则窗口基序号向前移动到具有最小序号的未确认分组处。如果窗口移动了,并且有未发送分组的序号落在窗口内,则发送这些分组。
接收方也需要响应三种情况:
- 序号在 [rcv_base, rcv_base+N-1] 内的分组被正确接收。分组序号落在窗口内,接收方回送一个具有该分组序号的 ACK。若该分组以前没有收到过,则缓存该分组。若该分组的序号等于接收窗口的基序号,则将该分组以及以前缓存的序号连续的(起始于 rcv_base 的)分组交付上层,然后移动窗口。
- 序号在 [rcv_base-N, rcv_base-1] 内的分组被正确接收。必须回送一个 ACK,即使该分组已接收过。原因在于,发送方与接收方看到的窗口在多数时间是不一样的,对于哪些分组已经被正确接收,哪些没有,发送方和接收方并不总是能看到相同的结果。发送方重传分组,意味着其并未收到接收方对该分组的 ACK(ACK 分组丢失了,发送方以为接收方没收到),如果接收方因为其已确认过该分组而选择不反馈(接收方以为发送方已收到),那么,发送方的窗口就永远无法向前滑动了。
- 其他情况。忽略该分组。
序号空间的大小与窗口长度的协调对 SR 来说是至关重要的。假定序号空间大小为 4,窗口长度为 3,那么,在无知之幕下,以下两种状况对接收方而言是等价的,但真实的情况却完全不同。
从上述情形,可以看到,序号空间对于窗口必须足够大,否则会出现接收方无法判断接收到的报文是超时重传还是启用了新一轮序号的分组。
假设当前接收方正在等待的、序号最小的分组序号为 m,窗口大小为 N,那么接收方的窗口范围为 [m, m+N-1],这意味着接收方已发送了序号范围为 [m-N, m-1] 共 N 个分组,并且回送了 ACK。假设极端情况下(这也是 3-27 发生的情况),前 N 个分组的 ACK 均丢失(发送方窗口范围为 [m-N, m-1]),或发送方接收到前 N 个分组的 ACK 并发送了接下来序号范围为 [m, m+N-1] 的分组(发送方窗口范围为 [m, m+N-1],且 m+N-1 大于序号空间 k)而前若干个分组丢失。若序号空间过小,那么这两种行为接收方无法分辨。
在上述情况下,发送方可能发送的最小序号为 m-N,最大序号为 m+N-1,由于 m+N-1 可能超过序号空间 k,从而对 k 取模而大于等于 m-N,那么就会出现范围为 [m-N, (m+N-1)%k] 的一段重叠空间,序号在其中的分组,接收方就无法分辨是重传还是新分组。可以想到,只要阻止这个重叠空间出现即可,那么就要满足以下关系:
m − N ≥ 0 m-N ≥ 0 m−N≥0
m + N − 1 ≤ k − 1 m+N-1≤k-1 m+N−1≤k−1
解得 N ≤ k / 2 N ≤ k/2 N≤k/2
即窗口的大小最大为序号空间大小的一半。
演进历程
—无损信道> rdt 1.0 无脑协议
—比特差错> rdt 2.0 停等协议
—反馈受损> rdt 2.1 分组序号
—NAK多余> rdt 2.2 冗余 ACK
—信道丢包> rdt 3.0 超时重传
—停等低效> GBN 流水线化
—冗余重传> SR 选择重传
3.5 面向连接的运输:TCP
TCP 连接
TCP 是面向连接的,在两个应用程序可以发送信息之前,必须握手以建立连接。这个连接并不是电路或虚电路,而是一个虚拟连接,其连接状态完全保留在两个端系统中。
TCP 连接在点对点之间提供全双工服务,两端都有各自的发送缓存和接收缓存。
TCP 报文段结构
TCP 报文段由首部字段和数据字段组成。
数据字段的长度是有限制的,最大报文长度 MSS 根据最大链路层帧长度(即最大传输单元 MTU)设置。MSS 要保证一个 TCP 数据段加上 TCP/IP 首部长度(通常 20+20 = 40 字节)不超过 MTU,而 MTU 的典型长度是 1500 字节,所以 MSS 的典型值是 1460 字节。注意,MSS 是数据字段的长度,而不是数据字段+首部的总长度。
首部包含许多字段:
- 源端口号和目的端口号:各 16 比特,与 UDP 一样,用于复用/分解。
- 序号和确认号:各 32 比特,讨论见后。
- 首部长度:4 比特,指示 TCP 报文段首部的长度,以 4 byte 为单位。
- 标志:6 比特,6 个标志位各 1 比特。ACK 比特用于指示确认号字段中的值是有效的,即该报文段包括一个已被成功接收报文段的确认。RST、SYN 和 FIN 比特用于连接的建立和拆除。(PSH 比特用于指示接收方应立即将数据交付上层。URG 用于指示该报文为紧急的,紧急数据的最后一个字节由 16 比特的紧急数据指针字段指出。括号内的这些都不常用。)
- 接收窗口:16 比特,用于流量控制。
- 检验和:16 比特,与 UDP 一样,用于差错检测。
- 选项:可变长,实践中不常用。
接下来澄清一下序号和确认号的作用。
TCP 把数据看成一个无结构的、有序的字节流,因为序号建立在传送的字节流之上,而不是传送的报文段序列之上。一个 TCP 报文段的序号因此是该报文段首字节的字节流编号。假定数据流由一个 500000 字节的文件组成,其 MSS 为 1000 字节,数据流的首字节编号是 0,那么 TCP 将为该数据流构建 500 各报文段,序号分别为 0、1000、2000、…。当然,首字节编号也可以不为 0。事实上,一条 TCP 连接的双方均可随机地选择初始序号。这样做可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性(它碰巧与旧连接使用了相同的端口号)。
确认号则是报文段的发送方期望从对方收到的下一字节的序号。假定主机 A 已收到编号为 0 ~ 535 的所有字节,那么其发送的下一个报文段中的确认号字段为 536。TCP 是累积确认的。假定主机 A 又收到编号为 900 ~ 1000 的字节,那么其发送的下一个报文段中的确认号字段仍为 536,因为主机此时期望收到的下一字节确实是 536。对于失序的报文段(如例子中的 900 ~ 1000),实践中,接收方保留失序的字节,并等待缺少的字节以填补该间隔。这表现得类似于 SR 协议,但 TCP 绝不是一个完全的 SR 协议,后面我们将再来讨论这个。
简化模型
在序号与确认号的讨论中,我们描述了 TCP 接收方的动作。现在,简要(意指不受流量控制和拥塞控制的影响的情况下)描述一下发送方需要响应的事件:
- 上层的调用。序号方面与前面描述的相同。另外,当发送的分组之前所有发送的分组都已被确认时,开启定时器(与 GBN 一样,这个定时器是与最早的未被确认的报文段相关联的)。
- 超时。重传引起超时的报文段(与 SR 一样),重置定时器(与 GBN 一样,TCP 只有一个定时器)。
- 收到 ACK。累积确认,更新最早的未被确认的字节值。如果还有未被确认的报文段,则重置定时器。
可以看到,TCP 发送方的响应动作与 GBN 和 SR 都有重叠之处:公用定时器、累积确认等看起来像是 GBN,而选择重传等看起来又像是 SR,所以,TCP 是一个介于 GBN 与 SR 之间的协议。
往返时间的估计与超时间隔加倍
TCP 采用超时/重传机制来处理报文段的丢失问题。前面我们看到,对超时间隔长度的设置是一个棘手的问题。
TCP 通过采样并更新来应对。在任意时刻,TCP 仅为一个已发送的但目前尚未被确认的报文段(这个报文段绝不会是已被重传过的)来估计 SampleRTT 值。一旦获得一个新 SampleRTT 时,TCP 就会根据下列公式来更新 RTT 估计值 EstimatedRTT:
E s t i m a t e d R T T = ( 1 − α ) ⋅ E s t i m a t e d R T T + α ⋅ S a m p l e R T T EstimatedRTT = (1-\alpha)·EstimatedRTT + \alpha·SampleRTT EstimatedRTT=(1−α)⋅EstimatedRTT+α⋅SampleRTT
其中 α \alpha α 参考值为 0.125。这是一个指数加权移动平均。
另外,TCP 还会测量 RTT 的偏差值:
D e v R T T = ( 1 − β ) ⋅ D e v R T T + β ⋅ ∣ S a m p l e R T T − E s t i m a t e d R T T ∣ DevRTT=(1-\beta)·DevRTT + \beta · |SampleRTT-EstimatedRTT| DevRTT=(1−β)⋅DevRTT+β⋅∣SampleRTT−EstimatedRTT∣
其中 β \beta β 的参考值为 0.25。这也是一个指数加权移动平均。
TCP 的超时间隔就依照上述两个值来确定:
T i m e o u t I n t e r v a l = E s t i m a t e d R T T + 4 ⋅ D e v R T T TimeoutInterval = EstimatedRTT+4·DevRTT TimeoutInterval=EstimatedRTT+4⋅DevRTT
TimeoutInterval 的初始值参考值为 1 秒。
实践中,TCP 并不完全依靠采样计算平均值来设定超时间隔,而是仅在“上层的调用” 和 ”收到 ACK“ 中重置定时器时,会通过 EstimatedRTT 和 DevRTT 计算 TimeoutInterval。其他时候(指重传),都会将下一次的超时间隔设置为先前值的两倍。这样,超时间隔就在每次重传后呈指数型增长。这可以是一个形式受限的拥塞控制。
快速重传
超时重传存在的问题之一是超时间隔太长了,引入快速重传可以提前发现丢包。
因为发送方经常一个接一个地发送大量报文段,如果一个报文段丢失,就很可能引起许多一个接一个的冗余 ACK。如果 TCP 发送方接收到对相同数据的 3 个冗余 ACK,它把这当作一种指示,说明跟在这个已被确认过 4 次的报文段之后的报文段已经丢失。一旦收到 3 个冗余 ACK,TCP 就执行快速重传,即在该报文段的定时器过期之前重传丢失的报文段。
为什么是 3 个冗余 ACK 呢?只能说这是一个经验值,出现三个冗余 ACK 是丢包的概率较大。像逼乎的那个高赞回答,就是装逼忽悠人的典型。
另外,RFC 为产生 ACK 提出了建议:
流量控制
TCP 连接的两端,都各自有一个接收缓存。放入接收缓存中的分组往往并不会立即被读取,所以,若一方发送得太多、太快,就可能引起对方的接收缓存溢出。
为了匹配发送方的发送速率与接收方应用程序的读取速率,TCP 提供了流量控制。TCP 让双方各自维护一个称为接收窗口的变量,在发送数据时填入报文段的接收窗口字段中,用于提醒对方,自己的接收窗口所剩余的空间。
对于接收方,定义以下变量:
LastByteRead: 应用进程从缓存读出的数据流的最后一个字节的编号。
LastByteRcvd: 已缓存的数据流的最后一个字节的编号。
RsvBuffer: 接收缓存的大小。
rwnd: 接收窗口。
为了防止溢出,很显然,有
L a s t B y t e R s v d − L a s t B y t e R e a d ≤ R c v B u f f e r LastByteRsvd-LastByteRead\le RcvBuffer LastByteRsvd−LastByteRead≤RcvBuffer
r w n d = R c v B u f f e r − [ L a s t B y t e R c v d − L a s t B y t e R e a d ] rwnd = RcvBuffer-[LastByteRcvd-LastByteRead] rwnd=RcvBuffer−[LastByteRcvd−LastByteRead]
rwnd 就是需要填入接收窗口字段的值,它是动态变化的,初始时设置为 RcvBuffer。
对于发送方,为了控制流量,定义以下变量:
LastByteSent: 发送的最后一个字节的编号。
LastByteAcked: 确认的最后一个字节的编号。
发送方在连接的整个生命周期必须保证:
L a s t B y t e S e n t − L a s t B y t e A c k e d ≤ r w n d LastByteSent-LastByteAcked \le rwnd LastByteSent−LastByteAcked≤rwnd
也就是,已发送但未被确认的字节数必须控制在 rwnd 以内。
特别的,当主机 A 向主机 B 通告 rwnd = 0 时,B 必须继续发送只有一个字节数据报文段,A 在接收到这个报文段后开始清空缓存,并在确认报文中包含一个非 0 的 rwnd 值。这么做是为了防止 A 通告 rwnd = 0 后,又没有任何数据要发送给 B,这时,B 就无法得知 A 的接收窗口大小,就陷入了阻塞。
TCP 连接建立:三次握手
要建立一个 TCP 连接,需要经过三次握手:
-
客户端的 TCP 首先向服务器端的 TCP 发送一个 SYN 报文段。该报文段不包含应用层数据,但首部的 SYN 标志位被置为 1。另外,客户会随机选取一个初始序号 ISN,放入该报文的序号字段中。
-
服务器收到 SYN 报文段,为该 TCP 连接分配缓存和变量,并向该客户发送表示允许连接的 SYNACK 报文段。该报文段也不包含应用层数据,首部的 SYN 标志位也被置为 1。然后,服务器对客户的 ISN 进行确认,即将该报文的确认号字段置为接收到的 SYN 报文段中的 ISN + 1,这样,客户发送给服务器的数据就是可靠的了,而不用担心会是前段时间在网络中逗留过久的。然后,同客户一样,服务器随机选取一个初始序号放入序号字段中。
-
客户收到 SYNACK 报文段,为该 TCP 连接分配缓存和变量,并向服务器发送对 SYNACK 报文段的确认报文段。该报文段的首部 SYN 标志位被置为 0,因为连接已经建立了。然后,客户需要对服务器的 ISN 进行确认,即将该报文段的确认号字段置为接收到的 SYNACK 报文段中的 ISN + 1,这样,服务器发送给客户的数据就是可靠的了。另外,该报文可以捎带应用层数据了。
为什么需要三次握手?
客户和服务器都各自随机选取一个初始序号值,是为了避免将先前连接发送的、在网络中逗留过久的报文段误认为是当前接收的,所以,互相确认对方选择的初始序号值就很有必要。若只有两次握手,只有服务器对客户的 ISN 进行确认,而客户并没有对服务器的 ISN 进行确认,那么,只有客户到服务器的报文段是可靠的。举个例子就是,当服务器接收到了逗留过久的、实际已经失效的 SYN 报文段,若返回一个 SYNACK 报文段,客户并不会响应该 SYNACK 报文段,连接也就无法建立起来,而服务器又为之配置了资源,造成了浪费。
至于四次握手,就是性能的问题了。三次握手已足够。
TCP 连接拆除:四次挥手
要拆除一个 TCP 连接,需要经过四次挥手:
- 客户向服务器发送一个 FIN 报文段,该报文段首部的 FIN 标志位被置为 1。
- 服务器收到 FIN 报文段,回送一个确认报文段。
- 服务器向客户发送一个 FIN 报文段,该报文段首部的 FIN 标志位被置为 1。
- 客户收到 FIN 报文段,回送一个确认报文段。
此时,两台主机上用于该连接的所有资源都被释放了。
3.6 拥塞控制原理
拥塞的代价
- 当分组的到达速率接近链路容量时,分组经历巨大的排队时延。
- 发送方在遇到大时延时所进行的不必要重传会引起路由器利用其链路带宽来转发不必要的分组副本。
- 当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费掉了。
拥塞控制方法
- 端到端拥塞控制。TCP 的方案,后面提到。
- 网络辅助的拥塞控制。路由器向发送方反馈拥塞状态的信息,有两种反馈方式:一是利用阻塞分组直接反馈给发送方,二是路由器标记或更新从发送方流向接收方的分组中的某个字段来指示拥塞的产生。
3.7 TCP 拥塞控制
概述
TCP 所采用的拥塞控制方法是,让双方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。关于这种方法,能够提出三个问题:
- 一个 TCP 发送方如何限制它向其连接发送流量的速率呢?
与流量控制类似,采用一个拥塞窗口 cwnd,通过调节 cwnd 来限制其发送的速率,即有
L a s t B y t e S e n t − L a s t B y t e A c k e d ≤ m i n { c w n d , r w n d } LastByteSent-LastByteAcked \le min\{cwnd, rwnd\} LastByteSent−LastByteAcked≤min{cwnd,rwnd}
- 一个 TCP 发送方如何感知从它到目的地之间的路径上存在拥塞呢?
存在拥塞时,就会产生丢包。TCP 发送方将丢包事件定义为超时或 3 个冗余 ACK,也就是说,当发生两者其一,就说明丢包,也就意味着出现了拥塞。
- 当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?
TCP 通过以下指导性原则改变速率:
- 一个丢失的报文段意味着拥塞,因此当丢失报文段时应当降低 TCP 发送方的速率。
- 一个确认报文段指示该网络正在向接收方交付发送方的报文段,因此,当对先前未确认报文段的确认到达时,能够增加发送方的速率。
- 为探测拥塞开始出现的速率,TCP 发送方增加它的发送速率,直到出现拥塞,降低速率,然后再度开始探测。
TCP 拥塞控制算法
TCP 拥塞控制算法包括三部分:慢启动、拥塞避免和快速恢复。其中,快速恢复并非是必须的。
- 慢启动
当一条 TCP 连接开始时,cwnd 的值通常设置为 1 MSS,初始发送速率也就为 1 MSS/RTT。
可用带宽很可能必 1 MSS/RTT 大得多,所以 TCP 发送方希望迅速找到可用带宽的值。因此,在慢启动状态,每当一个报文段首次被确认,就将 cwnd 增加 1 MSS。
想象这样一个场景:第一个 RTT,cwnd = 1 MSS,发送方发送一个报文段,并收到一个确认,cwnd += 1 MSS;第二个 RTT,cwnd = 2 MSS,发送方发送两个报文段,并收到两个确认,cwnd += 2 MSS;第三个 RTT,cwnd = 4 MSS,发送方发送四个报文段,并受到四个确认,cwnd += 4 MSS;…。
就这样,在慢启动状态,每过一个 RTT,发送速率就翻番,呈指数型增长。指数型增长很容易引发拥塞,因此需要适时停止。根据以下事件,慢启动状态发生转移:
- 超时。TCP 发送方将 ssthresh(慢启动阈值)置为 cwnd/2,cwnd 置为 1 MSS,然后重新开始慢启动。
- cwnd ≥ ssthresh。进入拥塞避免状态。
- 3 个冗余 ACK。TCP 发送方将 ssthresh 置为 cwnd/2,cwnd 置为 ssthresh + 3 MSS(这 3 MSS 是 3 个冗余 ACK 提供的速率增长),然后进入快速恢复状态。
- 拥塞避免
一旦进入拥塞避免,cwnd 的值大约是上次遇到拥塞时的值的一半,距离拥塞可能并不遥远。因此,在拥塞避免状态,每个 RTT 只将 cwnd 的值增加 1 MSS。
一种方法是,每当一个报文段首次被确认,就将 cwnd 增加 (MSS/cwnd) MSS。由于一个 RTT 共发送 cwnd 字节,分为 cwnd/MSS 个报文段发送,每个 ACK 增加 (MSS/cwnd) MSS,那么,在该 RTT 结束后,cwnd 共增加 cwnd/MSS · (MSS/cwnd) MSS = 1 MSS。
就这样,在拥塞避免状态,cwnd 呈线性增长。根据以下事件,拥塞避免状态发生转移:
- 超时。TCP 发送方将 ssthresh(慢启动阈值)置为 cwnd/2,cwnd 置为 1 MSS,然后进入慢启动。
- 3 个冗余 ACK。TCP 发送方将 ssthresh 置为 cwnd/2,cwnd 置为 ssthresh + 3 MSS(这 3 MSS 是 3 个冗余 ACK 提供的速率增长),然后进入快速恢复状态。
- 快速恢复
在快速恢复中,每个新收到的引起 TCP 进入快速恢复状态的冗余 ACK,都将 cwnd 的值增加 1 MSS。
根据以下事件,快速恢复状态发生转移:
- 超时。TCP 发送方将 ssthresh(慢启动阈值)置为 cwnd/2,cwnd 置为 1 MSS,然后进入慢启动。
- 对丢失报文段的一个 ACK 到达。TCP 发送方将 cwnd 置为 ssthresh,然后进入拥塞避免状态。
以上是 TCP Reno 版本的快速恢复动作。若是 TCP Tahoe,不管发生超时还是 3 个冗余 ACK,都将 cwnd 减至 1 MSS,并进入慢启动阶段。TCP Reno 相对于 TCP Tahoe, cwnd 在一次慢启动后,接下来整体处于一个较高的水平,在吞吐率得到保证的情况下减少了网络波动。TCP Reno 相较于 TCP Tahoe 是更优的。
为什么 Reno 版本在进入快速恢复时选择将 ssthresh 和 cwnd 都设为出现数据包丢失时的 cwnd 值的 1/2,而不是其它值呢?比如 1/3, 2/3?
假设 A 与 B 建立了一个 TCP 连接,A 向 B 发送一个数据段,B 将以一个 ACK 数据段回应,这段时间就是一个 RTT。由于光速的限制以及链路的长度,数据段在传输过程中是有容量的,假设这个容量为 C,显然有 C = r * RTT,r 为最大发送速率。
试想,既然一个 A 发送一个数据段后 B 要以一个数据段回应,而这两个数据段是异步发送的,那么当 cwnd 大于 1 时,一定有一个时刻,A 发送的尚未到达 B 的数据段与 B 发送的对已到达的数据段的 ACK 回应同时在链路上传播,并且两者数量之和等于 A 此 RTT 中发送的数据段数量。由此,我们可将容量分为两份(或者说,出于 A、B 两者的公平性,平分链路容量),一份是 A 发送给 B 的仍在传播的数据段,一份是 B 回应给 A 的仍在传播的 ACK,假定 A、B 发送速率相同,那么又可进一步得到 C = r * RTT = 2 * N,N 为 A 发送给 B 和 B 发送给 A 的数据段刚好填满各自的链路容量(C/2)时的数量。由于 B 每接收一个来自 A 的数据段,就会回应一个 ACK 给 A,那么,在 A 发送其刚刚最后一个数据段而 B 的最早一个 ACK 即将抵达 A 时,这样一个 RTT 时间内,能使链路满载的 A 发送的数据段数量显然是 2 * N,这就是最大的 cwnd 值,一旦超过这个值,就会发生阻塞。
一旦发生阻塞,我们就能够知道,当前可供 A、B 通信的最大网络容量 C 即为 cwnd。此时,需要将 ssthresh 减半,为什么是 1/2,而不是 1/3、2/3 呢?原因就在于 ssthresh 其实就相当于 N!N 是 A 发送至 B 的网络容量能够容纳的数据段的数量,也就是说,对于 A 而言,此时网络是满载的,如果遭遇网络波动,就会导致丢包。可以说,N 就是一个分水岭,它是 A 发送给 B 的数据段数量相对能够说是安全的最大值,低于 N,网络尚未填满,有一定的缓冲可以应对网络波动;高于 N,网络将会处于一段较长的持续满载状态,一旦有网络波动,就会导致丢包。作为慢启动与拥塞避免的临界值,ssthresh 的最佳选择无疑是选择一个分水岭,也就是 N!
从以上推导可以得出,ssthresh= N,而 C = r * RTT = 2 * N = cwnd,有 cwnd = 2 * N,最终得到 N = ssthresh = cwnd/2,这也就是 1/2 的由来。
具体可参考:https://blog.csdn.net/dog250/article/details/51439747
公平性
TCP 趋于在竞争的多条 TCP 连接提供对一段瓶颈链路带段的平等分享。