传输层服务模型

传输层使用 TCP / UDP 协议为运行在不同主机的应用进程之间的提供逻辑通信服务(process to process)。
而网络层是 host to host
从发送的角度:把来自应用层的报文封装成报文段递交给网络层。
从接收的角度:把来自网络层的报文段拆装成报文递交给应用层。

TCP 协议服务提供可靠的、有序的、具有流量控制和拥塞控制的传输服务。
UDP 协议服务提供无连接的、无序的、不可靠的传输服务。
究其原因是因为网络层的 IP 协议是尽力而为交付服务,是不可靠的,所以需要在传输层实现 TCP 协议保证传输的可靠性。

多路复用/分解技术

因为一个主机肯定不止运行一个进程,因此肯定也有很多套接字接口,对于一个主机来说:

  • 作为发送者,从套接字接收来自应用层的不同报文,添加首部封装成报文段后传递给网络层称为多路复用
  • 作为接受者:把接收到的报文段放到正确的套接字传给应用层称为多路分解

image.png

UDP 套接字:[目的 IP 地址,目的端口号]
TCP 套接字:[源 IP 地址,源端口号,目的 IP 地址,目的端口号]

UDP 协议

User Datagram Protocol,用户数据报协议,UDP 协议服务提供无连接的、无序的、不可靠的传输服务,是一种尽力而为的服务。
采用 UDP 协议发送报文有以下特点:

  • 报文可能丢失,目的主机未收到;
  • 报文可能乱序到达目的主机;
  • 没有握手建立连接的过程;

使用 UDP 协议的有:因特网电话,流式媒体,DNS 协议,SNMP 网络管理协议。

如果人为在应用层提供可靠性机制,就可以保证 UDP 协议可靠,如 Google 通过引入 QUIC 层实现了基于 UDP 协议的可靠传输 腾讯技术工程对 QUIC 的科普

UDP 协议存在的意义

  1. 没有握手带来的额外时延。
  2. 简单,发送方和接收方都不需要维持连接状态,为此带来额外开销。
  3. 没有拥塞控制 UDP 可以以更快的速率发送报文段。

UDP 报文段结构

image.png

检验和

用于确定 UDP 报文段从源到目的移动时,比特是否发生变化。
必须注意的是,检验和提供差错检验的能力,却没有差错恢复的能力。
小贴士:一般 UDP 报文段的首部只有 8 字节

方法:

  1. 发送方对 UDP 报文段中所有 16 比特字的和进行反码运算,求和过程中遇到溢出就回卷。
  2. 回卷:发生溢出时,把溢出位当 1 与结果相加。
  3. 反码:0 变 1,1 变 0

如发送方有 2 个 16 比特的字:
image.png
在接收方把这 2 个 16 比特的字在求和(图中的 sum),然后与检验和(图中的 checksum)相加,应该为全 1,否则说明出错。
问题:只能检验出奇数个比特出错,偶数个比特出错检验不出。
image.png

可靠传输原理

reliable data transfer protocol,rdt,可靠传输协议
使用有限状态机(Finite-State Machine,FSM)来描述。

rdt1.0

rdt1.0 假设信道完全可靠,即无位错、不丢包、有序(请记住这个重要前提)。
rdt 的版本演变都是通过一个一个消除这些假设而来的。
那么发送方 sender 和接受方 receiver 的 FSM 如下:
image.png
这是一个非常简单的开始,不现实的,因为信道不是可靠的,位错,丢包,无序经常发生。
接下来的 rdt 版本就是逐步加入应对这些错误的机制,从而得到更完善的 rdt。

rdt2.0

现在进一步假设有位错,但是不丢包、有序(请记住这个重要前提)。
那么如果检验和恢复差错呢?引入检验和 ARQ 自动重传请求机制
ACK 告诉发送方正确收到分组;NAK 告诉发送方收到的分组有错,此时发送方重传分组即可。

此时的 FSM 如下:
image.png
image.png
请记住假设前提:不丢包,有序,但有位错!!!
缺陷:

  1. ACK 或 NAK 出错。发送方一直停留在“等待 ACK 或 NAK”状态,发送方不清楚接收方是否收到分组。
  2. 停等协议。发送方发送分组后进入“等待 ACK 或 NAK”状态,不能继续发送其他分组,只有收到 ACK 才能回到“等待上层调用”状态,继续发送分组。

若发送方收到出错的 ACK 或 NAK 后选择重传分组,那么接收方如何识别哪些是重复分组呢?
在 rdt2.1 引入分组编号!

rdt2.1

现在假设还是:不丢包,有序,可能出错(请记住这个重要前提)。
引入分组编号,为了区分重复分组,只需要 1 位序号。
此时 FSM 如下:
image.png
发送分组时给分组加个序号。

image.png
右边状态应该是“等待下层调用1”
注意到发送 ACK 或 NAK 时也带了检验和。

到此,rdt2.1 已经成功解决了有序和位错问题,只剩下丢包没有处理。
缺陷:

  1. 若数据分组丢失,那么导致数据不完整。
  2. 若应答正确序号的 ACK 或 NAK 丢失,将导致发送方不能发生状态转移,系统死机。

rdt2.2

在 rdt2.1 的基础上去掉 NAK,只用 ACK。
image.png
image.png
使用带序号的 ACK。

rdt3.0

到此,rdt3.0 要解决丢包的问题。
通过引入定时器解决。
image.png
通过定时器,若发送方在 T 时间内没有收到所发分组的 ACK 就重传分组。
若发送方在 T 时间后收到超时分组的 ACK,说明接收方可能存在重复分组,接收方可通过序号判断。
接收方通过带序号的 ACK 应答分组。

rdt3.0 的接收方与 rdt2.2 一样。

小结

从 rdt1.0 到 rdt3.0 通过逐步引入校验和、ARQ 自动重传机制、序号和定时器来分别解决分组在信道中出现位错、乱序、丢包的问题。

  • 1.0 到 2.0 引入校验和与 ARQ 自动重传机制解决位错。
  • 2.0 到 2.1 引入序号,原意是由于 ACK 或 NAK 出错引起的分组重传,让接收方区分冗余分组。
  • 2.1 到 2.2 修改 ARQ 机制,去除 NAK 只留 ACK。
  • 2.2 到 3.0 引入定时器,解决发送过程中的丢包问题。

通过 rdt 实现了可靠传输,缺点是 rdt 是停等协议,有严重的性能问题。

rdt 停等协议的缺陷

image.png
信道利用率 = 占用信道的时间 / 使用信道的时间(区分“占用”与“使用”:占用表示主机正在往链路上发送数据,使用则不一定)
主机 1 的发送一个分组到链路上所需时间为
假设 RTT 为 30 ms,则主机 1 收到 ACK 需要 30 ms + 8 us 时间,则信道利用率为 8 us / ( 30 ms + 8 us ) = 0.00027 = 0.027 %
如此低的利用率,实属浪费。

流水线可靠传输协议

通过增加序号范围来一次性发送多个分组,但因此发送方和接收方也不得不缓存多个分组。

回退 N 步

Go Back N,GBN,也称滑动窗口协议

发送方

维护一个窗口。
image.png

  1. sent_base:最早未确认的分组序号。
  2. nextseqnum:最小未使用的序号。
  3. N:窗口长度。

接收方

只维护一个 expectedseqnum 变量,表示期待接收到的分组编号。

发送方动作

  1. 响应上层调用。若窗口未满,则产生一个分组并发送;否则,返回数据给上层或缓存数据。同步机制可以让上层在窗口未满时才调用。
  2. 收到一个 ACK,采用累积确认,对序号为 n 的分组。
  3. 超时重传所有未被应答的分组 [send_base, nextseqnum-1]

只有一个定时器。

接收方动作

  1. 累积确认。收到序号为 expectedseqnum 的分组,表示序号小于 expectedseqnum 的分组都已经被正确接收,返回一个 ACK。
  2. 丢弃失序分组。若收到的分组序号不等于 expectedseqnum,直接丢弃。

缺点

重传所有未被应答的分组开销大,原因是接收方不缓存失序分组。

选择重传

Selective repeat,SR

发送方与接收方

发送方维护一个窗口,与 GBN 差不多。接收维护一个窗口,用于缓存失序分组。
image.png
rcv_base:指向 X+1,其中 X 是目前正确有序接收的序号。

发送方动作

  1. 上层调用;若窗口内有可用序号则发送,否则缓存起来待发送或传回上层。
  2. 收到有序 ACK:若 ACK 在窗口内,send_base 移动都最小的未确认的序号,比如 send_base = 1 时收到 ACK1,假设之前已经收到过 ACK2 和 ACK3,没有收到 ACK4,那么 send_base = 4。
  3. 收到乱序 ACK:若 ACK 在窗口内,直接标记确认
  4. 超时:发送方为每个序号分组都维护了一个逻辑定时器,对于超时的分组就重传。

接收方动作

  1. 收到有序分组:rcv_base 移动到最小的下一个期待接收的序号,比如 rcv_base = 1,收到了分组 1,若还缓存了分组 2 和分组 3,那么 rcv_base = 4,并把之前有序的分组(1,2,3)交付给上层(还有发送 ACK1)。
  2. 收到乱序分组:直接缓存起来并发送 ACK。
  3. 收到出错分组:不管,等待重传。

要求

窗口长度必须小于或等于序号空间大小的一半。

N:窗口的长度
maxseq:最大序列号
原因:当窗口长度超过序号空间的一半时,会导致接收方无法判断某个分组是重传分组还是新分组。
如序号空间大小为 4,窗口大小为 3.

  1. 发送方发送 0, 1, 2
  2. 接收方收到,窗口前进到 3, 0, 1,并发送 ACK
  3. ACK 全部丢失导致发送方超时重传 0, 1, 2
  4. 接收方判断 2 不在窗口内为重传分组,0, 1 在窗口内,理应是新分组,但之前收到过,也可能是重复分组。

TCP 协议

Transmission Control Protocol,传输控制协议。
控制二字暗含了:连接管理流量控制拥塞控制
特点:可靠传输、全双工、点对点。

基础知识点

TCP 报文段结构

传输层 - 图16
由于选项字段通常为空,TCP 首部一般是 20 字节

  • 源端口和目的端口:各 16 比特,用于多路复用和多路分解。
  • 序号和确认号:各 32 比特,用于实现可靠数据传输服务。
  • 首部长度:4 比特,指示 TCP 首部的大小,单位是 32 比特的字,即 4 字节,因此能表示的大小为 传输层 - 图17 字节。
  • 6 比特的标志位:ACK 标志位指示确认号是否有效;RST, SYN, FIN 标志位用于建立和销毁 TCP 连接;PSH 标志位被置 1 时表示接收方应立即将数据交给上层;URG 标志指示是否为紧急数据。(其实还有 CWR 和 ECE 两个标志位,用于拥塞控制)
  • 接收窗口:16 比特,用于流量控制。
  • 检验和:16 比特,与 UDP 中的检验和一样,检查首部和数据部分
  • 紧急指针:16 比特,指向紧急数据的最后一个字节。

MSS 和 MTU

MSS,Maximum Segment Size,最大报文段长度,传输层概念。
MTU,Maximum Transmission Unit,最大传输单元,数据链路层概念。
关系:MSS 的大小取决于 MTU 的大小,MTU = MSS + TCP 首部长度 + IP 首部长度
由于 TCP + IP 头一般是 40 字节,以太网的 MTU 为 1500 字节,所以 MSS 一般为 1460 字节。
小贴士:MSS 虽然是传输层概念,但是其大小不包含 TCP 首部长度,1460 字节是指应用层数据的最大长度

序号和确认号

序号:TCP 把数据看成字节流,一个字节给一个序号,因此 TCP 报文段里的序号是有效载荷的首字节的序号
确认号:发送方期待收到的下一个字节的序号。
例子:如 MSS = 1000 字节,假设从 0 开始编号,那么序号范围就是 0 - 999。假设发送方已经收到了来自接收方的序号为 0 - 100 的所有字节,那么发送方在发给接收方的报文段中,确认序号将被填为 101。假设发送方收到了乱序到达的序号为 201 - 300 的所有字节,那么发送方在发给接收方的报文段中,确认序号仍被填为 101,因此 TCP 提供累积确认

TCP 应对失序报文段

实践中,TCP 对于失序报文段一般采取缓存而非丢弃手段,更合理利用网络带宽。

TCP 超时重传

对于报文段丢失问题,TCP 如何设置超时重传的时间呢?

估计往返时间

公式:传输层 - 图18
SampleRTT:报文段的样本RTT,即从发送到收到应答之间的时间间隔。
EstimatedRTT:SampleRTT 的均值,得到一个新的 SampleRTT 便更新一次 EstimatedRTT。
α:推荐值是 0.125

RTT偏差

公式:传输层 - 图19
DevRTT:RTT 的偏差,用于估计 SampleRTT 的波动大小。
β:推荐值是 0.25

超时间隔

公式:传输层 - 图20
TimeoutInterval 的推荐初始值为 1 秒。

3 种典型场景

image.png
图 - ACK 丢失
A 发送序号为 92 且含有 8 字节数据的报文段,B 接收到后发送 ACK 却丢失了,A 由于超时,重传该报文段。

image.png
图 - 提前超时
A 连续发送两个序号分别为 92 和 100、分别含有 8 和 20 字节数据的报文段,B 收到并发送两个 ACK,但是 A 却提前超时了,重传了序号为 92 的报文段。
小贴士:TCP 超时重传只会传 base 所指的一个报文段。

image.png
图 - 累积确认
A 连续发送两个序号分别为 92 和 100、分别含有 8 和 20 字节数据的报文段,B 收到了并发送 ACK=100 和 ACK = 120,但 ACK=100 丢失,由于 TCP 采用累积确认,A 收到 ACK=120 就知道 B 已经收到了序号在 120 之前的所有报文段,因此并不会重传序号为 92 的报文段。

TCP 发送方和接收方动作

发送方:

  1. 收到来自上层的数据,立即封装成报文段并交给下一层处理。
  2. 定时器超时,立即重传 base 指向的那个报文段。
  3. 收到 ACK,更新 base,并计算冗余 ACK 数量。

接收方:
image.png
注意接收方收到按序到达的报文段时,并不会立即发送 ACK,而是采用了 500 ms 延迟。

快速重传

问题:超时间隔过长,导致丢失的分组被延迟重传,带来了额外的端到端时延。
解决:通过收到 3 个冗余 ACK 实现快速重传。
冗余 ACK:发送方在收到 ACK 时,发现之前已经收到过这个 ACK 了。
原理:接收方收到失序报文段时,会发送收到的最后一个有序报文段的 ACK,因此若发送方连续发送的多个报文段,接收方期待收到的那个丢失了(有序的那个),而其他的都正常接收到了,接收方就会重复发送最后一个有序报文段的 ACK(冗余 ACK),发送方发现了 3 个冗余 ACK,判断有序报文段已丢失,不用等定时器超时就直接重传失序报文段。

流量控制

问题:A 和 B 之间建立了 TCP 连接,都为双方分配了 TCP 缓存,接收方收到数据后放入缓存供应用进程读取。但是应用进程可能忙于其他任务来不及读取或者读取速度较慢,而发送方又发得太多、太快,这将会导致接收缓存溢出!
因此,流量控制是针对发送方的,但 TCP 是全双工通信,因此可以说针对双方。

解决:双方都维护一个接收窗口 rwnd,表示剩余的缓存大小。

公式:传输层 - 图25
RcvBuffer:接收方缓存大小。
LastByteRcvd:接收方收到的最后一个字节的编号。
LastByteRead:接收方应用进程读取的最后一个字节的编号。
那么 LastByteRcvd-LastByteRead 就表示缓存中剩余未读字节的大小。
显然要求 LastByteRcvd-LastByteRead <= RevBuffer

过程:
考虑两个主机 A 和 B 之间的一条 TCP 连接,A 是发送方,B 是接收方。

  1. B 初始 rwnd = RevBuffer,将 rwnd 放入接收窗口字段中,通知 A 还有多少空间可用。
  2. A 维护两个变量:LastByteSent 表示最后发送的字节的编号,LastByteAcked 表示最后被确认的字节编号。则 LastByteSent - LastByteAcked 表示已发送但未确认的字节数,那么发送方在发送数据时,只要令 LastByteSent - LastByteAcked <= rwnd 就能保证接收方不会发生缓存溢出。

一个小的技术问题:当 rwnd = 0 时,B 告知了 A,A 会停止发送数据,那么当 B 的应用进程读完数据后,A 也无法得知缓存被清空了,可以继续发数据。
解决:当接收方的 rwnd = 0 时,发送方继续发送有效载荷只有一个字节的报文段,接收方会确认这些报文段,当缓存被清空时,接收窗口被置为非 0 数,发送方得以重新发送数据。

连接管理

三次握手

  1. 客户端向服务器发送 SYN 报文段(SYN 标志置 1),随机初始化序号 client_isn。
  2. 服务器收到 SYN 报文段,发送 SYN ACK 报文段确认,随机初始化序号 server_isn,SYN 标志置 1,显然此时 ack = client_isn + 1,此时服务器为客户端分配 TCP 缓存和变量。
  3. 客户端收到 SYN ACK 报文段,发送确认报文段,SYN 标志置 0,显然序号 seq = client_isn + 1,确认号 ack = server_isn + 1,此时客户端也为服务器分配 TCP 缓存和变量。

image.png
问题:SYN 泛洪攻击,客户端恶意发送大量 SYN 报文段而不发送确认报文段,导致服务器分配了大量 TCP 缓存和变量(简称半连接)。
解决:cookie + 资源延后分配。服务器在收到确认报文后才分配 TCP 缓存和变量,cookie 是服务器用一个只有自己知道的哈希函数根据源、目的 IP 和目的端口号计算出来的特殊初始序号。服务器将这个特殊初始序号放入报文段发回给客户端,若客户端不发送确认报文,那服务器就不分配资源;若客户端发送的确认报文中 ACK ≠ cookie + 1,则服务器认为是恶意客户端,也不分配资源。服务器并需要保存生成的 cookie,因此它只需要在收到确认报文时,用同样的方法就可以计算出 cookie。

经典问题:为什么是三次握手,而不是两次、四次?
三次握手的目的是确保双方都知道对方为自己分配了 TCP 缓存和变量。若两次握手,服务器不知道客户端是否已经分配了资源。而四次握手完全是多余的,因为第三次握手后,双方已经都为对方分配了资源。

四次挥手

参与一条 TCP 连接的两个进程中的任何一个都能够终止该连接。
假设是客户端终止连接,过程如下:

  1. 客户端发送 FIN 报文段(FIN 标志置 1)
  2. 服务器收到 FIN 报文段并发送确认报文
  3. 服务器发送 FIN 报文段。
  4. 客户端收到 FIN 报文段并发送确认报文

第四次挥手后,双方都回收了资源。

状态转换图

客户端:
image.png
图 - 客户端 TCP 连接状态转换
问题:为什么等待 2MSL,MSL 是 Maximum Segment Lifetime,最大报文段生存时间,表示任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。当客户端接收到 FIN 报文段时发送确认报文段,若确认报文段丢失,服务器会超时重传 FIN 报文段,客户端再次发送确认报文段。

服务器:
image.png

拥塞控制

拥塞原因

很多很多,但是根本原因是有太多的源想以过高的速率发送数据。

拥塞代价

  1. 当分组的到达速率接近链路容量时,分组经历巨大的排队时延。
  2. 发送方必须执行重传以补偿因缓存溢出而丢失的分组。
  3. 发送方在遇到大时延时所进行的不必要重传会引起路由器利用其链路带宽转发不必要的分组。
  4. 当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费掉。

指导性原则

引入拥塞窗口 cwnd,用来限制发送方的发送速率。
要求
假设 rwnd 无限大,则允许发送方最大发送 cwnd 字节数据,在一个 RTT 内收到 ACK 后重新调整 cwnd,这个过程中的发送速率约为 字节 / 秒。
因此通过调整拥塞窗口的大小,可以控制发送速率。

发送方如何感知拥塞?
丢包即拥塞!用超时或收到 3 个冗余 ACK 来表示一个“丢包”事件,此时应减小 cwnd。
收到 ACK 即顺利!收到 ACK 意味着网络通畅,可以适当提高发送速率,此时应增大 cwnd。

带宽探测:逐渐增大发送速率直至发生“丢包”事件,获得最大发送速率。

TCP 拥塞控制算法

算法组成:慢启动 + 拥塞避免 + 快速恢复。
慢启动和拥塞避免是强制部分,快速恢复是推荐部分。

慢启动

窍诀:指数增
过程:

  1. 初始 cwnd = 1MSS,发送速率为
  2. 对收到的每个确认报文,cwnd 增加一个 MSS,即每个 RTT 内,cwnd 翻倍。因此 cwnd 的变化是:1, 2, 4, 8, 16……,以 2 的指数级增长。

3 种结束情况:

  1. 发生超时,重新开始慢启动
  2. 收到 3 个冗余 ACK,进入快速恢复阶段
  3. cwnd 到达 ssthresh,进入拥塞避免阶段

ssthresh 是“慢启动阈值”。初始为 0,不是慢启动结束标志,当发生情况 1 或 2 时,ssthresh = cwnd / 2,在这以后 ssthresh 就可以当做结束标志之一了。

拥塞避免

窍诀:线性增
过程:

  1. 当 cwnd = ssthresh 时,继续指数增可能比较鲁莽。
  2. 因此一个 RTT 内,只增加 1MSS,比如一个 RTT 内收到了 10 个确认报文段,那么对于每个报文段,cwnd 只增加 1/10 MSS.

2 种结束情况:

  1. 超时,此时 ssthresh = cwnd / 2, cwnd = 1*MSS重新进入慢启动阶段
  2. 收到 3 个冗余 ACK,此时 ssthresh = cwnd / 2, cwnd = ssthresh + 3*MSS,进入快速恢复阶段

快速恢复

过程:对于每个冗余 ACK,cwnd 增加 1MSS
结束情况:

  1. 收到新 ACK 进入拥塞避免阶段。
  2. 超时,进入慢启动阶段。

TCP 版本

TCP Tahoe 是早期版本,没有快速恢复阶段。
TCP Reno 是较新版,有快速恢复,下面拥塞算法的有限状态机 FSM 就是这个版本的。
image.png
图 - TCP Reno 版本的 FSM
由图可知:

  1. 任何阶段发生超时采取的动作都是一样的,ssthresh = cwnd / 2, cwnd = 1 MSS,duplicateACK = 0,即超时必然重新进入慢启动阶段,慢启动阈值变为拥塞窗口的一半,拥塞窗口置 1 MSS,冗余 ACK 清零。
  2. 慢启动和拥塞避免阶段收到 3 个冗余 ACK 采取的动作也是一样的,ssthresh = cwnd / 2, cwnd = ssthresh + 3*MSS,然后进入快速恢复阶段。

加性增、乘性减

总结就是花里胡哨,故弄玄虚。
加性增指慢启动和拥塞避免中 cwnd 的指数增和线性增。
乘性减指 cwnd 减半,在 FSM 中,在慢启动收到 3 个冗余 ACK、拥塞避免收到 3 个冗余 ACK 时会发生。