传输层的目的是将从 终端-终端通信的 ip 协议转变为 app-app 的通信
IP 是一种弱连接服务模型, 数据包有可能损坏, 延迟, 丢包, 重复等, 以及物理线路流量问题(例如流量大的时候如何明智的选择时机发送包), 处理这些问题也可以让应用层避免考虑这些棘手的问题

多项选择与去选择

  • Mux: 发送端将不同软件的数据汇集到一起发送到网络层
  • Demux: 将汇总的数据发送到接收端不同的 socket

传输层的功能

  1. 事实上的通信是在 process 之间进行的, 通过 ports 进行实现收发
  2. 帮助软件层实现 end-to-end 服务, 要求可靠, 配送速度均匀
  3. 发送太快会占满带宽
  4. 发送太慢会导致低效
  5. udp 是一种 最小运输协议
  6. 只提供 mux/demux 功能
  7. socket 类型是 SOCK_DGRAM
  8. tcp 是一种可靠的, 有序的 byte stream abstraction
  9. socket 类型是 SOCK_STREAM
  10. 拥有拥塞控制 (Congestion Control)

Socket

是一种软件的抽象, 作为 app process 与系统 (transmit layer) 通信的功能抽象

Port

UDP: User Datagram Protocol

header 包含了发送端的 port, 接收端的 port 与 ip address, 信息长度以及 checksum 校验位
udp_protocol.png

  • optional checksum: 等于 0 表示不需要校验
  • source port: 可以不需要提供

Reliable 确保发送

数据包破损 corruption

接收端通过 checksum 发现包破损之后返回一个 NACK (nack) 信号
发送端通过 checksum 发现 ack 损坏信号
以上二者都会导致发送端再次发送一次包
接收端可以在 header 中看到 sequence number 是否重复

数据包丢失 packet loss

定一个有效时限 timeout 即发送端从发送之后等待一定的时间 如果过了这段时间没有收到有效的 ack 则再次发送
这会导致 duplicate
注意这里 timeout 的阈值不能太小, 至少大于一次 RTT

串行收发模型

由于 发送-等待ack 模型需要每次都等待回包再接着发送, 这个速度非常慢

滑动窗口 sliding window

定义一系列的 packet sequence 为一个窗口, 假设窗口尺寸是 n
一般一次性最多发送一个窗口的数据, 即尺寸为 n 个 packet
window 应该包含还在传输中的数据, 而当收到 ack 数据之后会发生移动
sliding_window.png

吞吐量

应该是 min(n*DATA/RTT, Link BandWidth), 对比 stop and wait 模型的吞吐量为 DATA/RTT

累计确认模型 Cumulative Ack

作为接收方, 如果连续收到窗口内的多个包, 返回第一个没有收到包的 id
cumulative_ack.png
例如发送 1-3, 接受1, 2, 则返回 ack(3)
发送 12345, 接受1245, 则返回 ack(3)

选择性确认模型 Selective Ack

逐个返回任何收到的包, 可以提供更加详细的收包信息

丢包重发的思路

Go-Back-N

对应于 cumulative Ack 策略, 只接受下一个没有收到ack的包
会丢弃不符合顺序的数据包
gobackn.png
例如发送 1-9的包, id=5 的丢失了, 那么收到的 ack 为 1234,5,5,5,5,5…

Selective Repeat

每次只会重新发没有被正确确认的包
高效但是复杂的状态记录

选择

  • 错误率低的时候使用 gbn, 否则会浪费带宽
  • 高错误率用 selective, 否则太过复杂

TCP 协议

tcp 功能抽象

  1. tcp 提供了一种可靠的, 按照字节顺序的字节流
  2. 可靠性: 可以递归的发送丢失的数据包
  3. 只会处理连续的数据 chunk 片段
  4. 字节流: 接收方会假设有一段数据流过来, 并且尝试将其转发到 app

表头 header

职能

  1. checksum: check integrity
  2. sequence number: the pack id
  3. sender + recver hold sliding window
  4. recv: send cumulative ack
  5. recv: buffer out of seq packs

内容

tcp_header_concept.png
如图所示, tcp 表头拥有 src port, destination port, seq id, ack/nack,

  • src/dest port: 用来 mux/demux
  • checksum: 用来检查包的完整性
  • sequence number: 为包的字节偏移量, 定义 segment 为网络包中去除 ip header 与 tcp header 的裸数据部分, 则通过 seq num 来记录本数据包的第一个 byte 的偏移量
    • MTU: maximum transmission unit, ip 包的最大尺寸
    • MSS: maximum segment size, tcp data segment 的最大尺寸, MSS=MTUip header sizetcp header sizeMSS = MTU - ip\ header\ size - tcp\ header\ size
    • ISN: initial sequence number, 表示第一个 byte 的序列号 - 因此任意的数据序列号可以用 ISN + k (offset) 来表示 - 发送端和接收端的 ISN 互不相同但是经过 tcp 握手之后会发生同步 (只是知道对方的 isn 但是不会将自己的 isn 变成一样的值) - 随机 isn 可以有效防止不同会话的冲突以及防止黑客攻击
  • HDRlen: header 长度, 单位为 word (4B)
  • flags: 用于建立/解除 tcp 通信使用的标识符
    • SYN: tcp 发送端请求建立连接的flag (此时 ack 序号 N/A)
    • ack: 确认收包的 flag
    • fin: 确认结束的 flag
    • RST: 请求重启, 一般是发送方程序意外终止之后发送 RST , 接收方不会 ack因此这个 RST 包并不保证接受, 但是如果收到 接收方还在给发送方发送任何数据, 就认为接收方没有收到 RST 从而再次发送 RST

丢包的处理

根据 cumulative 的发包特点, 丢包之后会重复收到一个固定的包, 发送方通过收到相同的包 k 次之后会重新发送, 其中 tcp 的 k=3
tcp 丢包之后重新发送会等待收到对应的 ack 再继续向下

重发时间间隔 Single Retransmission Timer

如果在一定的 timeout 之后发送者没有再次收到对应的包, 会再次重新发送
timeout 是基于 rtt 的

rtt 估算

采用如下公式低通滤波:

Estimated RTTk=(1α)Estimated RTTk1αSample RTT\text{Estimated RTT}_k = (1 - \alpha)\cdot \text{Estimated RTT}_{k-1} \alpha \cdot \text{Sample RTT}

这是一个持续的过程, 每次收发都会重新预测一次 rtt, 但是如果某次出现传输丢包则忽略这一次的更新
Karn/Partridge 算法: 将 α=0.125\alpha = 0.125 带入上述公式, timeout 间隔为 RTO = 2 Estimated RTT 作为基础值, 每次遇到发包失败就双倍 timeout, 来给tcp恢复建立时间基础, 一旦发包再次成功, timeout 恢复到 2ER, 缺陷是 对于 RTT 变化影响大
Jacobson/Karels 算法: 为了解决对 RTT 波动影响大的问题, 令 Deviation=SampleEstimatedDeviation = | Sample - Estimated | 并且令 DevRTT=EstimatedRTT+4×DeviationDevRTT = EstimatedRTT + 4\times Deviation

三次握手

three_handshake.png

发送的时候丢包处理是用时间来等待 (因为最初没有 RTT 测量值, 只能猜测时间) tcp 的默认时长是 3s

四次挥手

four_wavehand.png
结束方首先发送请求关闭连接, 然后等待 ack 之后将自己 half-closed 并且等待对方发来 fin 最后发送 ack 确认关闭 fin, 发送完等待一定时间间隔 (大于一个 RTT 来确保对方收到 ack 结束) 才最终完成挥手

TCP 流控制 Flow Control

指传输过程不涉及流量拥塞的情况下对数据流量进行控制

RWND

用来防止 sender 的窗口尺寸 > receiver 的窗口尺寸
receiver 在 ack 中会指定对应的 rwnd 尺寸, 因此 sender 方要保证空中运输的数据尺寸大小不超过 rwnd, 即 rwnd 表示当前 buffer 剩余空间, 会收到新接受数据和应用层消耗数据的影响而改变, 由于发送端的发送速度以 byte/sec 为单位, 因此发送速度不能超过 rwnd/rtt (即在未来一个回路时间内发包不能超过 rwnd 的限制)
指定的 rwnd 尺寸存在 tcp header 的 Advertised Window 部分

RWND=BufferSize(LastByteReceiveLastByteRead)\text{RWND} = \text{BufferSize} - (\text{LastByteReceive} - \text{LastByteRead})

因此这个只和收到的最后一个包序号有关, 和中间包掉包无关
tcp_rwnd_calc.png
如果在接收端返回 rwnd = 0 (零窗口通告) 此时发送端应当持续发送 1B 的试探数据

TCP 拥塞控制 Congestion Control

根本原因是 router 的 statistical multiplexing, 这个机制会导致大量并发请求下(直接原因)流量链路发生拥塞, 例如在拥塞情况下导致掉包, 发送端不断重新发送包导致有效更新更加缓慢
Jacobson 通过升级 router 和应用层, 以及添加几行 tcp 实现代码来完成修改

  • 找到 bottleneck 的带宽量
  • 将流量调整到 bottleneck 附近
  • 不同 flow 之间要共享 bottleneck
    将 router 抽象为一个数据包的队列
    发送的速度一般和窗口尺寸有关即 WindowSize/RTT\text{WindowSize} / \text{RTT} 左右, 因此拥塞控制事实上是在控制窗口的尺寸

CWND

表示拥塞控制下一个窗口的尺寸, 最终实际窗口尺寸 = min{CWND,RWND}\min\{CWND,RWND\}
注意这里对 cwnd 的讨论以 MSS 为单位, 实际中更多以 byte 为单位
检测 congestion: 1. router 告知自身拥塞 2. 丢包 3. 包延迟
其中丢包分为 duplicate acks (局部丢失, 较为轻微) 和 timeout (全面丢失, 较为严重) 两种, 而 cwnd 对二者的处理方式也不相同
处理 congestion: 不同的延迟可以有一个共同的处理逻辑: 在丢包的时候降低 cwnd 而在收到包的时候增大 cwnd, 最终理想情况下可以围绕 bottleneck 进行波动. 并且由于发送的最弱情况是断联即 rate=0, 因此窗口要能快速调整到 0, 因此我们可以使用如下两种阶段来稳定处理

  • slow start: 为了安全稳定起见, 初始发包速度很低并且增长相对缓慢
  • ramp up quick: 如果链路畅通则立刻增大发包速度
Slow Start

sender 开始十分缓慢 (CWND=1MSS), 以 exponential 的速度增长
每收到一个 ack 就将 cwnd+1 (每一个 RTT 就 double cwnd)
设置一个上界阈值 ssthresh 如果超过了就离开 exponential 增长阶段
阈值也应该随着网络状况动态更改

Type Rules
TCP_Tahoe cwnd = 1 on 3 dupACKs
TCP_Reno cwnd = 1 on timeout; cwnd /= 2 on dupACKs
TCP_newReno TCP_Reno + Fast Recovery
TCP_SACK Incorporate Selective Acknowlegement