bigwolftime / gitmentCommentsPlugin

0 stars 0 forks source link

TCP 协议(1) - 简介 #47

Open bigwolftime opened 2 years ago

bigwolftime commented 2 years ago

https://bigwolftime.github.io/TCP/

一. TCP 报文段的首部格式

TCP 是面向字节流的, 但 TCP 传送的数据单元是报文段. 报文段分为首部和数据部分. 以下是关于 TCP 报文段的首部.

源端口 / 目的端口

分别占用 2byte.

序号

在 TCP 字节流的传输过程中, 每一个字节会按照顺序编号, 要传送的字节流的起始序号要在建立连接时设置, 此字段即表示要 发送 的数据第一个字节的序号. 占用 4byte(范围: [0, 2 ^ 32 - 1]), 达到最大值后会归零重新计数.

ex: 报文段的序号为 301, 携带 100byte 数据, 可知最后一个字节的序号为 400, 下一个报文段的序号值应为 401.

确认号

4byte, 指的是: 期望收到对方下一个报文段的第一个数据字节的序号.

以上一个为例, 计算出的下一个报文段的序号为 401, 那么就可以将确认号标记为 401, 表示: 希望下一个报文段的序号 为 401.

数据偏移

占用 4bit, 指示: 报文段的数据起始处 与 报文段的起始处 的距离. 可以理解为 TCP 报文段的首部长度(因为 TCP 分为: 首部和数据, 即: 数据的起始地址 - 报文段的起始地址 = 首部长度).

TCP 报文段首部的前 20byte 固定长度, 但是选项和填充的数据长度可变的, 所以此处需要读取偏移量才能定位到数据.

有一点需注意: 数据偏移的单位为: 4byte(32bit), 例如: 0011(B) 表示首部长度为 4byte 3 = 12byte. 4bit 可以表示的的范围: [0, 15], 所以 TCP 报文的首部最大长度为: 4byte 15 = 60byte, 其中首部的前 20byte 长度固定, 所以选项和填充部分的最大长度为 40byte.

保留

占用 6bit, 保留今后使用.

TCP Flags

SYN: Synchronize Sequence Number. 在 TCP 握手阶段发送. SYN=1, ACK=0 表明这是一个连接请求报文段, 若对方 同意连接, 则会再响应报文中置 SYN=1, ACK=1.; ACK: 确认号的标记, 1 表示确认号字段有效, 0 表示确认号无效. TCP 规定: 连接建立后所有报文段的 ACK 需置 1; FIN: 完成数据的传输, 要求释放连接. TCP 挥手过程发送; RST: reset. 表示在 TCP 连接过程出现错误, 需释放当前连接. 还可以用此字段拒绝非法的报文或者连接; URG: 报文中含有紧急数据, 需提高优先级传送. ex: 程序在远程主机上运行, 用户发送 Ctrl + C 的中断命令, 此时若 不标记为紧急数据, 那么此指令会被存储在 TCP 缓存的末尾, 等到所有的数据被处理完毕才会交给应用程序, 这样就会浪费 许多时间. PSH: 两个程序通信时, 有时需要输入一个命令后立即收到对方的响应, 此时可以将 PSH 置为 1, 接收方将 PSH 标记为 1 的报文段尽快地交付给应用进程, 而不是等缓存满了再处理;

关于 URG / PSH: 两字段的作用都是提高报文的优先级, 但是实现的方式不同.

URG(紧急位): 数据报文直接交付给应用进程, 不会进入缓冲区;
PSH(急迫位): 此类型报文会进入到缓冲区后, TCP 立刻将缓冲区的数据交给应用进程.

窗口

窗口的大小为 16bit, 可表示的最大值为: 65535. 窗口指的是: 发送报文段的一方的接收窗口, 告诉发送方目前允许发送的数据 量. 可以理解为: 接收方的处理能力有限, 为了能够控制速率, 需要告诉发送方自己当前的处理能力, 发送方会根据此值控制 发送窗口大小, 以达到流量控制的目的. 其值是动态变化的.

校验和

占用 2byte. 校验范围包括报文段的首部和数据两部分.

紧急指针

占用 2byte, 当 URG = 1 时有意义, 指示报文段中紧急数据的末尾位置.

选项

选项部分的长度可变, 最高可传输 40byte, 没有选项需要传输时, TCP 报文段首部长度为 20byte.

二. TCP 可靠传输的实现

介绍以字节为单位的滑动窗口. 假设 A 为数据的发送方, B 为数据的接收方:

1

假定 A 收到了 B 的确认报文段, 其窗口大小为 20byte, 确认号是 31, 则 A 可以根据这两个字段确定出发送窗口的参数:

图中标记的发送窗口的后沿, 表示此前的数据已发送成功并收到了确认, 这些数据可以丢弃掉无需保留; 发送窗口的前沿, 则表示数据不允许发送. 发送窗口由前沿和后沿共同决定的.

后沿的变化情况有 2 种: 不动(即未收到相应的的确认) 和 前移(收到了确认), 不能向后移动, 因为不能撤销已收到的确认, 而且之前的数据很有 可能已被丢弃.

前沿的变化情况也有 2 种: 不动(没有收到确认, 所以不用变; 或者对方的处理能力降低, 通知窗口变小, 此时仅移动后沿) 和 前移. 前沿也有可能向 后沿方向移动, 不过 TCP 的标准中并不建议这样做, 因为发送方此前可能发送了窗口中的许多数据, 现在要收缩的话可能会导致一些错误.

在没有收到 B 确认的情况下, A 可以将窗口内的数据都发送出去, 但还不能丢弃掉, 因为遇到超时情况需要重传. 发送窗口越大, 发送方就可以在收到 确认之前发送更多的数据, 但需要注意, 这也需要接收方有能力及时处理并发出确认报文.

2

假定 A 已经发送了窗口中的编号 31-41 的数据(灰色部分), 此部分已发送但未收到相应确认; 42-50 代表允许但尚未发送

3

A 接收到了 B 的部分确认报文(32, 33), 此时发送窗口后沿还不能移动, 因为没有收到 31 的确认报文(可能丢失或者网络滞留), 此时 B 的确认号 仍为 31(即期望收到的序号), 不能是 32 或者 33.

4

假定 B 接收到了序号 31 的数据, 将 31-33 的数据交付主机后, 给 A 发出确认报文, 其中确认号变为 34, 表示已经收到了序号 33 及其之前的数据.

5

A 收到了 B 的确认报文后, 将窗口的后沿移动至 34, 前沿移动至 53.

6

当 A 将窗口内的所有数据发送完毕, 但没有收到任何确认, 此时 A 的发送窗口已满, 必须停止发送等待 B 的确认, 如果一段时间后仍然没有收到确认 (超时时间由超时计时器控制), 则 A 会进行数据的重传过程.

当数据发送方的发送速率过快, 接收方处理不及时, 导致数据丢失或者发送方判断数据传输超时, 此时就需要流量控制, 通过上文的滑动窗口机制就可以 实现.

三. 超时重传时间的确认

TCP 的下层是互联网环境, 发送的报文段途经的路由不同, 网络速率也不同, 所以对超时重传的时间计算较为复杂. 重传时间定的太短会导致不必要的 重传, 增大网络开销; 定的太长又使网络的延迟增大.

TCP 中使用了一种自适应算法, 它记录一个报文段的发出时间和收到确认的时间, 其时间差为 RTT(报文段的往返时间). 由多个 RTT 进行加权平均得到 RTTs(平滑往返时间), 每测量到一个 RTT 样本时, 会进行如下计算得到 RTTs:

RTTs = (1 - a) (RTTs) + a (RTT), a 的取值范围: [0,1)

若 a 趋于 0, 则表示新旧的 RTTs 相差不大; 若 a 趋于 1 则表示新的 RTTs 值受新的 RTT 影响较大.

RFC 2988 推荐 a 取值 0.125

超时重传时间 RTO(Retransmission Time-Out) 应该大于 RTTs: RTO = RTTs + 4 * RTOd (RFC-2988), RTOd 表示 RTT 的偏差的加权平均值, 与 RTTs 和新的 RTT 样本有关.

RTOd 的计算方法:

第一次测量时: RTTd = 0.5 RTT; 以后的测量: RTTd = (1 - b) RTTd + b * | RTTs - RTT |, b < 1, 推荐取值 0.25

思考 1

假如发出一个报文段, 由于没有及时收到确认报文而重传, 一段时间后收到了确认, 问题来了: 如何判定此确认报文是对第一次报文的确认, 还是对 重传报文的确认? 无法区分.

如果收到的是对重传报文的确认, 但却当成了首次发送报文的确认, 则会导致 RTT 偏大, 进而影响 RTTs 的取值使其偏大; 若收到的是对首次报文的 确认, 但却当成了重传报文的确认, 则会导致 RTTs 变小, 进而 RTO 变小.

由此, Karn 提出: 计算 RTTs 时, 对于重传的报文, 其 RTT 样本一律丢弃, 这样得到的结果就相对准确.

思考 2

这又引来新的问题, 假设报文段的传输延时增大, 进行超时重传, 但根据 Karn 的结论, 重传报文的 RTT 会被丢弃, 这就导致 RTTs 和 RTO 得不到 更新. 因此又对此算法进行了修正:

报文每重传一次, 就将 RTO 增大到: 旧的重传时间 * 2; 直到不再发生重传时, 才会使用上面的计算公式.

四. 参考

《计算机网络》谢希仁 RFC Document