# 运输层

# 运输层服务

网络层提供了主机之间的逻辑通信, 运输层为运行在不同主机上的进程提供了进程间的逻辑通信;

  • 对于应用程序来说, 把报文交给运输层就可以与其他主机上的进程进行通信;
  • 而数据实际是由更下层的链路层和物理层进行传输的, 但是对于应用程序来说, 它们并不在乎数据究竟是谁传输的;

运输层协议只工作在端系统中, 它们把来自进程的报文移动到网络层;

  • 像应用层一样, 运输层也不管报文段在网络层是被如何传输的;
  • 它们只知道网络层可以把数据送到另外一个主机上;

运输层存在多种运输层协议, 每种协议为应用程序提供不同的服务:

  • TCP: 可靠的, 面向连接的服务;
  • UDP: 不可靠的, 无连接的服务;

根据应用程序业务上的需求, 开发者选择合适的运输层协议;

# 多路复用 & 多路分解

在主机中, 可能同时运行着多个应用程序, 每个应用都有一个『 进程 』, 同时一个进程对应着一个或多个『 套接字 socket 』. 通过『 套接字 』应用程序获得『 运输层 』传来的数据, 同时还能够向『 运输层 』传入数据;

主机上有很多的套接字, 为了让『 运输层 』能够区分它们. 『 套接字 』需要有唯一标识. 主机上可以为不同应用的进程分配独享的『 端口号 』, 进程对应的『 套接字 』也使用这个端口号作为自己的唯一标识;


在主机上, 『 运输层 』从不同的套接字中收集报文, 并在报文上加上运输层的『 首部信息 』从而生成报文段, 再把报文段向下传递到『 网络层 』, 这个过程叫做『 多路复用 multiplexing 』

有两个首部字段是用来指示该报文段所需交付到的套接字:

  • 源端口号字段 source port number field 』
  • 目的端口号字段 destination port number field 』

在接收端,『 运输层 』检查接收到的『 报文段 』的首部信息, 找到对应字段指示出的『 目的端口号 』然后把报文段传递给以这个端口号作为标识的套接字. 这个过程称为『 多路分解 demultiplexing 』

『 源端口号字段 』对于『 接收端 』的作用是, 当接收端需要向发送端发送『 响应 』时, 『 源端口号字段 』就变成了响应报文段中的『 目的端口号字段 』

2020-06-03-11-42-45

# UDP 的多路复用 & 多路分解

假设使用 UDP 协议作为运输层的协议, 创建套接字时, 我们可以选择让运输层自动给其分配一个端口, 也可以自己指定一个端口;

  • 一般来说, 客户端应用, 让运输层自动分配端口;
  • 服务端应用, 开发者自己制定端口;

一个 UDP 套接字是由一个『 二元组 』来进行标识的;

  • 该二元组包含一个『 目的 IP 地址 』和一个『 目的端口号
  • 如果两个 UDP 报文段来自于不同的主机, 它们有不同的『 源 IP 地址/源端口号 』, 但是具有相同的『 目的 IP 地址 』和『 目的端口号 』, 那么这两个报文段也会被传向同一个『 目的套接字 』, 然后被定向到同一个『 目的进程 』;
🌰 例子:

下面 👇 通过一个例子精确地描述 UDP 的复用与分解:

  • 假定在主机 A 中的一个进程具有 UDP 端口 19157,它要发送一个应用程序数据块给位于主机 B 中的另一进程,该进程具有 UDP 端口 46428;
  • 主机 A 中的『 运输层 』创建一个『 运输层 』报文段,其中包括应用层的报文和『 运输层 』的首部字段:
    • 源端口号字段: 19157;
    • 目的端口号: 46428;
  • 『 运输层 』将得到的报文段传递到网络层。『 网络层 』将该报文段封装到一个 IP 数据报中,并尽力而为地将报文段交付给接收主机;
  • 如果该报文段到达接收主机 B,接收主机『 运输层 』检查到该报文段中的『 目的端口号 』 46428, 并将该报文段交付给端口号 46428 所标识的套接字;
  • B 需要回发一个报文段给 A 时,B 将 A 传送来的报文段中的『 源端口号 』中作为响应报文段的『 目的端口号 』;

2020-06-03-15-39-06

# TCP 的多路复用 & 多路分解

TCP 套接字使用一个『 四元组 』进行标识;

  • 该四元组包含『 源 IP 地址 』,『 源端口号 』,『 目的 IP 地址 』,『 目的端口号
  • 两个具有不同『 源 IP 地址 / 源端口号 』, 但是有相同『 目的 IP 地址 』和『 目的端口号 』的报文段, 会被定向到不同的『 目的套接字 』
  • 服务器主机可以支持很多并行的 TCP 套接字, 每个套接字与一个进程相联系;
  • 通过这种标识方法, TCP 协议可以分辨来自不同主机的, 或者同一个主机不同端口发送过来的报文;

2020-06-03-15-38-52

# 无连接的运输 UDP

UDP 协议只是在运输层上为数据提供了最基本的多路复用/分解的服务. 以便让网络层和应用层之间可以传输数据;

# UDP 的特点

  • 无需建立连接: 使用 UDP 协议传输数据之前, 不需要在两端建立连接, 这省去了建立连接的时延;
  • 容忍丢失: UDP 协议只是把应用层的数据交给网络层进行传输, 而不在乎接收端是否真的收到. 对于很多实时应用, 为了追求低延迟, 它们可以容忍数据丢失;
  • 无连接状态: TCP 需要在端系统间维护连接状态, 而 UDP 不需要;
  • 分组首部开销小: 每个 TCP 报文段有 20 个字节, 而 UDP 只有 8 个字节;

2020-06-03-22-06-01

# UDP 报文段结构

2020-06-03-22-23-07

UDP 首部有 4 个字段, 每个字段由两个字节组成:

  • 源端口号, 目的端口号: 指定了发送端和接收端对应进程的端口;
  • 长度: 指示了 UDP 报文段的长度 ( 首部 + 数据 ) 以字节为单位;
  • 检验和: 用来检验在该报文段中是否存在差错;

应用层『 报文 』占用 UDP 报文段的『 数据字段

# UDP 校验和

UDP『 检验和 』提供了差错检测功能.

  • 其用于确定当 UDP 报文从发送端向目的端移动时, 其中的数据是否发生改变;

具体计算方法:

  • 『 发送方 』在运输层, 对报文段中的数据以 16 比特为单位将它们进行分割, 然后加在一起;
  • 之后对得出来的『 和 』进行『 反码 』运算, 求和时遇到的任何溢出, 都把它加到最后一位上, 这称为『 回卷 』
  • 最终得出的结果, 被放到了『 校验和 』字段;
  • 『 接收方 』在运输层, 将报文段的数据, 仍旧以 16 比特为单位将它们进行分割, 然后加在一起;
  • 得出来的结果, 与源端系统发送过来的『 校验和 』加在一起;
    • 如果没有差错发生, 则最终的和为 1111111111111111;
    • 如果其中任何一个比特位是 0 则说明有错误;
  • 虽然 UDP 提供差错检测, 但是它对于恢复差错无能为力:
    • 某种 UDP 实现, 会丢弃受损的报文段;
    • 有的 UDP 实现, 会将受损的报文段交给应用程序, 并发出警告;

# 可靠数据传输原理

下面 👇 介绍一般性的可靠数据传输实现原理.

可靠数据传输协议 reliable data transfer protocol 』, 为上层实体提供的服务抽象是:

  • 传输数据不会被损坏或丢失, 而且所有的数据都能按照发送顺序进行交付;

在实现可靠数据传输时, 假设『 较低层 』提供的都是『 不可靠的通信信道 』.

2020-06-03-23-01-53

# 构造可靠数据传输协议

下面 👇 让我们一步步地研究一系类协议, 它们一个比一个复杂, 最终得到一个无差错的, 可靠的数据传输协议;

# 1. 经完全可靠信道的可靠数据传输: rdt 1.0

最简单的情况, 我们假定底层的信道是完全可靠的.

  • 下图 👇 展示的是发送方和接收方的『 有限状态机 Finite State Machine, FSM 』
  • 图中每个 FSM 只有一个状态;
  • FSM 右边 👉 将引起状态变化的『 事件 』显示在了横线上方;
  • 将事件发生时所采取的『 动作 』显示在了横线下方;
  • FSM 左上的虚线箭头, 指向了 FSM 的初始状态;
  • 在图中, 发送端中 rdt 1.0 协议的更高层调用 rdt_send 方法来将数据传递给它, 之后经由 make_pkt 动作生成了一个分组;
  • 在接收端, rdt 1.0 协议的更低层通过 rdt_rcv 向其传递一个分组, 然后经由 extract 动作将分组中的上一层关心的数据取出, 并通过 deliver_data 传递给上一层;

2020-06-03-23-22-35

# 2. 经具有比特差错信道的可靠数据传输: rdt 2.0

下面考虑分组在传播的过程中数据可能受损的情况.

为了让发送方知道哪些内容被正确地传送给了接收方, 数据传输协议需要建立『 重传机制 』, 具备这种机制的协议称为『 自动重传请求协议 automatic repeat request. ARQ 』

  • 接收者在收到正确数据时, 会回复给发送者一个『 肯定确认 positive acknowledgement 』
  • 接收者在收到受损数据时, 会回复给发送者一个『 否定确认 negative acknowledgement 』要求对方再重复发送一遍刚刚是数据

下图中展示了在添加了自动重传功能后, 发送端和接收端的有限状态机:

  • 在发送端中, 更高层调用 rdt_send 方法把数据传送到当前层后, 会计算出数据的『 校验和 』然后和数据一起打包成分组发送出去;
  • 发送出分组后, 发送方会等待来自接收方响应回的 ACK 或 NAK 分组;
    • 如果收到 ACK 代表分组被正确接收, 发送方继续接收上层传来的数据, 然后传输给接收方;
    • 如果收到 NAK 分组, 则发送方重新传送刚才发送的分组, 并等待接收方对于重传分组的 ACK 或 NAK 响应;
    • 在发送方等待 ACK 或 NAK 时, 不会从上层获得新的数据, 也不会发送新的数据给接收方, 具有这种行为的协议称为『 停等协议 stop-and-wait 』
  • 接收方仍旧只有一种状态, 当接收到分组后, 根据分组数据受损与否, 要么回答一个 ACK, 要么回答 NAK;

2020-06-04-00-40-37


上面 👆 所述的协议并没有考虑到, 如果接收方返回的 ACK 或 NAK 分组受损该怎么办. 受损的话, 发送方就不知道接收方是否正确接收到了刚才发送的分组.

解决方法是当接收方收到受损的 ACK 或 NAK 分组时, 就直接重传当前分组, 这种方法会在通信信道中引入『 冗余分组 duplicate packet 』

  • 冗余分组的问题是, 接收方不知道刚才发送的 ACK 或 NAK 响应是否被发送方正确接收, 也就无法知道现在发送方传来的数据是新的, 还是重传的.

  • 解决方案是, 发送方在发出的分组上添加一个『 分组编号 』字段, 接收方只要检查该序号是否与上一个分组序号相同, 则可判断该分组是不是一个新分组了.

分组序号字段的长度 1 比特就够了, 可能的值只有 0 和 1:

  • 与接收的上一个分组序号的数字相同, 代表是重传分组;
  • 数字不同代表是新的分组;

2020-06-04-09-15-17 2020-06-04-10-22-34

# 3. 经具有比特差错的丢包信道的可靠数据传输: rdt 3.0

现在假定除了比特受损外, 底层信道还会丢包;

假定发送方传输一个数据分组, 该『 分组 』或接收方对该分组的『 确认响应 』可能在传输中发生丢失, 无论哪情况下, 发送方都接收不到接收方发回的响应. 发送方也就无法判断就下来该执行的行为了.

有很多种方法可以解决丢包问题. 这里讲的方法是『 基于时间的重传机制

  • 在发送方传输出一个分组时, 启动一个『 倒数定时器 countdown timer 』
  • 如果在定时器设置的时间内, 发送方接收到了接收方响应, 则认为发送成功;
  • 如果当定时器到期后仍没有收到接收方的响应, 发送方就重传分组数据;

2020-06-04-10-33-37

因为分组的序号一直在 0 和 1 之间来回交替, 所以这种协议也被称为『 比特交替协议 alternating bit protocol 』

2020-06-04-12-26-09

# 流水线可靠数据传输协议

上面所讲的可靠数据传输协议是一个『 停等协议 』, 每次发送出数据后, 都要等待响应, 而期间不传输新的数据.

可以想象这样的协议肯定带来了很大的『 时延 』, 而且对于信道的『 利用率 』很低;


为了解决上述问题, 协议可以采用『 流水线机制 pipelining 』其不使用停等机制, 允许发送方发送多个分组而不需等待确认响应.

2020-06-04-12-27-01

2020-06-04-12-40-52

为了使用流水线机制, 协议需要做如下修改:

  • 增加分组序号的可选范围: 因为同时在传的分组数量变多了, 所以分组序号的范围也要加大;
  • 发送方和接收方需要缓存多个分组: 发送方需要缓存多个已经发出, 但未得到确认响应的分组. 同时接收方也需要缓存部分已正确接收的分组;
  • 差错恢复机制: 解决流水线的差错恢复有两种基本方法:
    • 回退 N 步 Go-Back-N. GBN;
    • 选择重传 Selective Repeat, SR;

# 回退 N 步

在『 回退 N 步 ( GBN ) 协议 』中流水线中已发送但是未确认的分组不得超过某个最大允许数 NN.

2020-06-04-13-04-18

  • 基序号 base 』定义为最早的未确认分组的序号, 它之前的序号都是已经发送并且得到确认的分组;
  • 下一个序号 nextseqnum 』定义为最小的未使用序号, 即将要被发送出去的分组可以使用这个序号;
  • 大于 base+Nbase + N 的序号都是不能使用的;

# GBN 发送方

  • 上层调用 rdtr_send 想要发送数据时, 发送方先检查是否有可用的序号, 如果有就打包一个分组, 并发送出去. 如果没有可用序号, 也就是已发送但未确认的分组数量为 NN, 则发送方把数据返回给上层;
  • 在 GBN 协议中, 发送方对接收方返回的序号为 nn 的分组, 采用『 累计确认 cumulative acknowledgment 』的方式, 该响应表明接收方已经正确接收包括序号为 nn 及其以前的分组;
  • GBN 协议仍旧使用『 定时器 』来解决分组丢失:
    • 其只使用一个定时器, 它用来给『 基序号 』分组计时;
    • 当在流水线中的第一个已发送未确认分组的定时器超时, 则重发所有已发送但是未确认的分组, 这也是协议名字『 回退 N 步 』的由来;
    • 当发送方收到一个 ACK 响应时, 但是仍旧有已发送未确认分组, 则定时器重新启动, 给新的『 基序号 』分组计时;
    • 当流水线中已经没有已发送未确认分组时, 则定时器终止;

2020-06-04-13-00-43

# GBN 接收方

  • 接收方发送给上层的数据必须是有序的;
  • 假如成功接收了一个失序分组, 也就是这个分组之前的某些分组还没有收到, 那么接收方需要丢弃这个正确接收的失序分组;
  • 接收方需要维护的唯一变量就是下一个按序接收的分组的序号, 它只接收有序分组, 而抛弃失序分组;
  • 这样做的缺点是, 传输过程中发生错误时, 发送方需要重复传很多传输正确的数据;

2020-06-04-13-00-59

下图 👇 展示了一个 N=4N = 4 的 GBN 协议运行情况:

2020-06-04-13-33-18

# 选择重传

选择重传 Selective Repeat, SR 』通过让发送方仅重传那些怀疑在接收方出错的分组, 而避免不必要的重传.

  • 在 SR 协议下的发送方, 每个已发送未确认分组都有一个自己的定时器;
  • 接收方如果收到失序的分组, 会返回 ACK 给发送方, 并且将其先进行缓存. 直到接收到的分组可以有序排列后, 才交付给上层;

2020-06-04-15-59-14


SR 协议有个问题, 发送方和接收方并不能够正确判断自己发出的分组, 哪些被接收了, 哪些没有.

🌰 举例:

  • 有 4 个分组序号 0 1 2 3, 窗口长度为 3;
  • 发送序号为 0 1 2 的分组, 并且接收方正确接收且确认了. 接收方的窗口向后移动三格, 落在第 4 5 6 个分组上, 它们的序号分别是 3 0 1;

考虑下面 👇 两种情况:

  • 情况一:
    • 所有返回的三个 ACK 确认分组都丢失了, 发送方的计时器超时后, 准备先重传序号为 0 的分组;
  • 情况二:
    • 假设发送方接收到 0 分组的 ACK 确认, 发送方窗口向前移一格, 并且发送序号为 3 的新分组;
    • 之后又接收到了 1 分组的 ACK 确认, 然后继续重复上个步骤, 发送序号为 0 的新分组;

上述 👆 两种情况下, 接收方最后都收到了来自于发送方传来的, 序号为 0 的分组. 但是它并不能够判断, 这是一个重传分组, 还是一个初次传输的新分组;

为了避免这种问题, 对于 SR 协议而言, 窗口长度必须小于或等于序号空间大小的一半;

2020-06-04-16-04-22

# 总结

校验和: 用于检测在一个传输分组中的比特错误;

定时器: 用于超时/重传一个分组. 可能因为该分组(或其 ACK) 在信道中丢失了。由于当一个分组延时但未丢失(过早超时),或当一个分组已被接收方收到但从接收方到发送方的 ACK 丢失时,可能产生超时事件,所以接收方可能会收到一个分组的多个冗余副本;

序号: 用于为从发送方流向接收方的数据分组按顺序编号。所接收分组的序号间的空隙可使接收方检测出丢失的分缀。具有相同序号的分组可使接收方检测出一个分组的冗余副本;

确认: 接收方用于告诉发送方一个分组或一组分组已被正确地接收到了 。确认报文通常携带着被确认的分组或多个分组的序号。确认可以是逐个的或累权的,这取决于协议;

否定确认: 接收方用于告诉发送方某个分组未被正确地接收。否定确认报文通常携带着未被正确接收的分组序号;

窗口, 流水线: 发送方也许被限制仅发送那些序号落在一个指定范围内的分组。通过允许一次发送多个分组但未被确认,发送方的利用率可在停等操作模式的基础上得到增加。窗口长度可根据接收方接收 & 缓存报文的能力、网络中的拥塞程度或两者情况来进行设置;

# 面向连接的运输 TCP

# TCP 连接

TCP 是『 面向连接 connection-oriented 』的, 两个端系统在用 TCP 协议发送数据前, 必须通过『 三次握手 』建立 TCP 连接;

  • 连接建立时, 双方会初始化很多与 TCP 连接相关的状态变量. 这些状态变量完全保存在两个端系统中;
  • 在两个主机的运输层中间的网络元素 ( 路由器, 交换机 ... ) 完全不关心两端间的 TCP 连接. 它们只关心传输的数据报;

TCP 连接提供『 全双工服务 full-duplex service 』也就是两端间可以互相传数据;

TCP 连接是『 点对点 point-to-point 』的, 一个发送方只能连接一个接收方. 一对多连接是不可以的;

TCP 会先将应用层传来的数据放在『 发送缓存 send buffer 』里, 之后 TCP 会在方便的时候以报文段的形式, 从缓存中取出数据进行发送;

  • TCP 可以从缓存中取出放入报文段的数据数量受限于『 最大报文段长度 Maximum Segment Size, MSS 』
  • 而 MSS 的大小又由『 最大链路层帧长度 』也称为『 最大传输单元 Maximum Transmission Unit, MTU 』来计算得出, MSS 的大小要保证一个 TCP 报文段, 被封装到网络层的数据报中时 ( 加上网络层首部 ), 数据报的大小适合单个链路层帧的长度;
  • ⚠️ 注意, MSS 指的是报文段里的应用层数据的最大长度, 不包括 TCP 的首部字段. 这个术语名称经常容易造成混淆;

2020-06-05-11-42-51

# TCP 报文段结构

TCP 报文段由『 首部字段 』加『 数据字段 』组成.

下图 👇 显示了首部字段的结构:

  • 源端口号 』『 目的端口号 』被用于多路复用和多路分解;
  • 校验和字段 』用于判断数据是否在传输中发生字节错误;
  • 序号字段 sequence nmber field 』和『 确认号字段 acknowledgment number field 』这两个字段用于建立可靠的数据传输服务;
  • 接收窗口字段 receive window field 』 用以控制流量, 指示接收方愿意接受的字节数量;
  • 首部长度字段 header length field 』指示首部字段的长度. 因为 TCP 选项字段的存在, TCP 首部字段的长度是可变的. 当选项字段为空, 默认 TCP 首部长度为 20 字节;
  • 选项字段 options field 』定义了一些选项, 具体参照 RFC 854 和 RFC 1323 文档;
  • 标志字段 flag field 』用以表示一个报文段的发送目的 ( ACK, PSH, RST, etc. )

2020-06-06-13-46-54

# 序号

  • TCP 把传输的数据看作是一个有序的字节流;
  • TCP 传输的报文段的『 序号 』是建立在字节流之上的;
  • 一个报文段的序号是, 该报文段首字节在字节流中的编号;

🌰 举例, 假如一个数据流是由长度为 500,000 个字节的文件组成. 其『 最大报文段长度 MMS 』为 1000 个字节;

  • TCP 将数据流切割成 500 个报文段;
  • 数据流中的首字节编号为 0;
  • 因此第 1 个报文段的序号为 0;
  • 第 2 个报文段的序号为 1000;
  • 第 3 个报文段的序号为 2000;
  • 以此类推;

2020-06-05-12-09-49

# 确认号

因为 TCP 是全双工的, 所以主机 A 向主机 B 发送数据时, 也可能会接收来自 B 主机的数据;

  • 主机 B 送达主机 A 的每个报文段中, 都包含一个『 序号 』用于标识 B 流向 A 的数据;
  • 而主机 A 发送给主机 B 的报文段中的『 确认号 』标识了主机 A 期望从主机 B 收到的下一个报文段的序号;

🌰 举例说明:

  • 主机 A 已经收到来自主机 B 的编号为 0 ~ 535 的所有字节, 主机 A 期望从主机 B 继续接收到编号为 536 及其之后的所有字节, 那么下次主机 A 发送的报文段中的『 确认号 』就是 536;
  • 如果主机 A 没有接收到『 确认号 』为 536 的报文段, 而是接到了一个『 确认号 』为 900 的报文段, 说明 536 报文段丢失了;
  • 那么, 主机 A 在给主机 B 的报文段中的『 确认号 』仍旧为 536;
  • TCP 只确认该流中从 0 至第一个丢失字节为止的字节, 所以 TCP 协议采用的是『 累计确认
  • 对于刚才的『 确认号 』为 900 的失序报文, TCP 可以采用之前说的两种解决方案:
    • 回退 N 步: 直接丢弃失序报文段, 让发送方重传;
    • 选择重传: 先保存失序报文段, 等待缺失的报文段;

# 往返时间的估计与超时

TCP 协议是采用『 超时/重传 』机制来处理报文段丢失的. 『 超时时隔 』的设置必须大于该连接的传输报文的『 往返时间 RTT 』

# 往返时间的估计

一个报文段的『 往返时间 RTT 』是从报文段被发出, 到发送方收到对该报文的确认响应之间的时间差;

大多数 TCP 实现会在某些时刻测量报文段传输的 RTT, 这被称为『 样本往返时间 SampleRTT 』

  • 在任意时刻, TCP 仅会为一个『 已发送但未确认 』报文段测量 SampleRTT;
  • 不会为重传的报文段测量 SampleRTT, 因为可能路径上有缓存, 会影响往返时间的测算;

测得 SampleRTT 后, 会将其与之前测得的所有 SampleRTT 计算一个平均值, 这个值被称为『 估计往返时间 EstimatedRTT 』计算公式如下 👇:

  • EstimatedRTT 的值由以前的 EstimatedRTT 和新测算的 SampleRTT 的值加权计算而来;
  • α\alpha 的值通常取 0.1250.125
  • 在这个公式中, 一个先前的 SampleRTT 的权重会在更新的过程中一步步地衰减;
EstimatedRTT=(1α)EstimatedRTT+αSampleRTTEstimatedRTT = (1 - \alpha) * EstimatedRTT + \alpha * SampleRTT

测量每次测得的『 样本往返时间 SampleRTT 』与『 估计往返时间 EstimatedRTT 』的偏离程度可以反应出往返时间的波动程度;

  • 如果波动大, 则 DevRTT 的值大. 如果波动小, 则 DevRTT 的值小.
  • 下面 👇 公式中的 β\beta 通常取值 0.250.25
DevRTT=(1β)DevRTT+βSampleRTTEstimatedRTTDevRTT = (1 - \beta) * DevRTT + \beta * |SampleRTT - EstimatedRTT|

# 设置和管理重传超时间隔

设置的超时间隔应该略微大于 EstimatedRTT;

具体大多少, 取决于每次 SampleRTT 的波动大小:

  • 波动大时, 余量就大, 反之亦然;
TimoutInterval=EstimatedRTT+4DevRTTTimoutInterval = EstimatedRTT + 4 * DevRTT
  • 最初, TimoutInterval 可以设置为 1 秒;
    • 出现超时后, TimoutInterval 就加倍;
  • 一旦有报文被成功收到, 就计算出了最初的 EstimatedRTT;
    • TimoutInterval 就改用上面的公式计算;

# 流量控制

如果应用层从运输层接收缓存中, "读取速度" 慢于发送端的 "发送速度", 则会导致 "接收缓存溢出";

需要通过『 流量控制 』机制来让发送速度与接收速度相匹配;

TCP 协议中, 发送方维护一个称为『 接收窗口 receive window 』的变量;

  • 变量的值: 接收方还有多少可用的缓存空间;
  • 因为 TCP 是全双工的, 所以两端都维护个 "接收窗口" 变量;

在接收方定义如下如下变量:

  • RcvBuffer: 接收缓存长度;
  • LastByteRead: 应用层从缓存中读取的数据流的最后一个字节的编号;
  • LastByteRevd: 已存入缓存中的数据流的最后一个字节的编号;

接收窗口用 rwnd 表示:

  • 其值等于 RcvBuffer - [LastByteRevd - LastByteRead]
  • 接收窗口 = 接收缓存长度 - 已接收但是未确认数据长度

2020-06-09-22-01-23

发送方只要把 "已发送但未确认" 的数据流控制在 "接收窗口" 的值之下, 就不会发生溢出了.

# TCP 连接管理

# 三次握手, 建立连接

客户端会以如下步骤与服务器建立 TCP 连接:

  1. 客户端向服务端发送 SYN 标志位为 1 的报文段. 表示请求建立连接;
  2. 服务器接收到后, 为 TCP 连接分配缓存和变量, 然后回传一个 SYNACK 标志位为 1 的响应报文段. 表示接收到请求, 同意建立连接;
  3. 客户端接收到后, 为 TCP 连接分配缓存和变量, 然后再向服务器响应一个 ACK 标志位为 1 的确认响应;

2020-06-09-22-18-48

# 四次握手, 断开连接

客户端会以如下步骤与服务器断开 TCP 连接:

  1. 客户端向服务器发送一个 FIN 标志位为 1 的报文段, 表示要断开连接;
  2. 服务器接收到后, 然后会发送 ACK 标志位为 1 的确认响应, 表示客户端到服务器的连接已经断开, 这时服务器不会再接收客户端发来的数据;
  3. 服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送 FIN 标志位为 1 的报文段, 请求断开连接;
  4. 客户端接收到后, 返回一个 ACK 确认响应. 然后会等待一段时间, 以防服务端没有收到确认报文. 若该时间段内服务器没有重发报文段的话,就正式断开连接, 释放资源;

2020-06-09-22-19-37

# TCP 状态变迁

在 TCP 连接的生命周期内, TCP 协议会经历各种状态的变迁;

2020-06-09-23-39-55

  • 当客户端的 TCP 进入 ESTABLISHED 状态时, TCP 连接已经建立, 就可以发送和接收有效载荷数据了;

2020-06-09-23-42-53

# 拥塞控制原理

网络拥塞 network congestion 』是指, 在分组交换网络中传送分组的数目太多时,由于分组交换机的资源有限, 接收到的分组数量超过了处理能力, 而造成网络传输性能下降的情况.

# 拥塞的原因 & 代价

# 情况 1

情况: 假设两条连接共享一个具有 "无限缓存" 的单跳路由;

  • 链路的最大传输速率为 RR;
  • 假设发送方 A 和 B 两台主机以相同速率发送数据, 随着速率增大, 每个接收方每秒接收到的字节数, 也不会超过 R/2R/2
  • 但是随着发送方的发送速率增大, 分组在路由器上的排队时延也越大;

代价: 当分组的到达速率接近链路容量时, 分组的排队时延会剧增;

2020-06-10-08-25-44

2020-06-10-08-25-53

# 情况 2

情况: 假定路由的缓存是有限的;

  • 那么当分组到达已满的缓存时, 就会被抛弃;
  • 发送方在超时后会重传分组;

代价: 发送方必须 "重传" 丢失的分组, 这会占用传输 "初始数据" 的链路资

# 情况 3

情况: 还有可能是:

  • 分组的排队时延太长, 发送方超时后就重传了分组;
  • 接收方可能会收到两个相同的分组, 后面重传的就会被丢弃;

代价: 因为时延太长, 发送方重传了并没有丢失的分组, 造成链路中传输了不必要的分组副本;

2020-06-10-08-26-08

2020-06-10-08-26-17

# 情况 4

情况: 多条连接共享多台路由器;

  • 在下图中 R2 路由器被 A-C 和 B-D 链路同时使用, 它们共同竞争 R2 上有限的缓存空间;
  • 如果 B-D 的传输速率大于 A-C, 那么 R2 的缓存空间就会储存更多的 B-D 的分组;
  • 极端情况下, 可能 B-D 的分组占满了 R2 的缓存, A-C 的分组全被丢弃, 以至于 A-C 的吞吐量为 0;
  • 那么 R1 路由器为 A-C 所转发到 R2 的分组就被浪费掉了;

代价: 当一个分组在路径中的一点被丢弃时, 每个上游路由器用于转发该分组所花费的传输资源就被浪费了;

2020-06-10-08-26-31

# 拥塞控制方法

可根据 "网络层" 是否为 "运输层拥塞控制" 提供了显式帮助,来区分拥塞控制方法.

# 端到端拥塞控制

  • 网络层没有为运输层拥塞控制提供显式支持;
  • 在端系统的运输层只能通过对网络行为 ( 丢失, 时延 ) 的观察, 来判断拥塞的情况;
  • TCP 必须通过端到端的方法解决拥塞控制,网络层不会向端系统提供有关网络拥塞的反馈信息;

# 网络辅助的拥塞控制

  • 网络层构件 ( 路由器 ) 向 "发送方" 提供关于网络中拥塞状态的显式反馈信息;
  • 拥塞信息从网络反馈到 "发送方" 通常有两种方式:
    1. 路由器直接给 "发送方" 发送信息;
    2. 路由器标记或更新从 "发送方" 流向 "接收方" 的分组中的某个字段来指示拥塞的产生. 一旦 "接收方" 收到一个被标记的分组后,再由 "接收方" 告诉 "发送方" 网络拥塞的发生;

2020-07-18-15-54-39

# TCP 拥塞控制

TCP 所采用的方法是让每一个发送方根据所感知到的网络拥塞程度来调整其发送数据的速率。

# 拥塞窗口 & 拥塞感知

TCP 连接的 "发送方" 设置一个『 拥塞窗口, congestion window, cwnd 』来限制输出数据的速率.

  • 发送方中 "已发送未被确认" 的数据量不会超过 "拥塞窗口 cwnd" 与 "接收窗口 rwnd" 中的最小值;
LastByteSentLastByteAckedmin[cwnd,rwnd]LastByteSent - LastByteAcked \leq min[cwnd, rwnd]

2020-07-18-16-04-30


TCP 发送方通过是否发生 "超时" 或 "丢包" 来判断拥塞是否发生:

  • 如果 "发送方" 接收到了 ACK 报文, 表示网络路径无拥塞, 数据可以正常传输. 则增加 cwnd 的长度, 提升传输速率;
  • 如果 "超时" 或 "丢包" 发生了说明路径上有拥塞发生. 则减少 cwnd 的长度, 降低传输速率;

# TCP 拥塞控制算法

TCP 拥塞控制算法包括 3 个主要部分:

  1. 慢启动;
  2. 拥塞避免;
  3. 快速恢复。

"慢启动" 和 "拥塞避免" 是 TCP 的强制部分,两者的差异在于接收到 ACK 时增加 cwnd 长度的方式.

"快速恢复" 是推荐部分,对 TCP 发送方并非是必需的. 这里不讲了

# 慢启动

在慢启动 ( slow strut ) 状态,cwnd 的值以 1MSS ( 最大报文段长度 ) 开始并且每当传输的报文段首次被确认就增加 1MSS

每经过一轮传输, 拥塞窗口的长度就 加倍.

2020-07-18-17-12-50

当出现 "超时" 时, TCP 设置一个状态变量『 ssthresh 慢启动阙值 』为 cwnd / 2, 即检测到拥塞时, "拥塞窗口" 长度的一半. 然后将 cwnd 再设置为 1 并重新开始慢启动;

之后当 cwnd 的值等于 ssthresh 时,结束慢启动并且 TCP 转移到『 拥塞避免 』模式

# 拥塞避免

在拥塞避免状态下, TCP 采用一种更加保守的方式去增加 "拥塞窗口" 的长度.

每经过一轮传输 "拥塞窗口" 长度只增加 1MSS.

  • 一种通用的方法是对于 TCP 发送方无论何时到达一个新的确认,就将 cwnd 增加一个 MSS (MSS/cwnd)字节.

🌰 例如,如果 MSS 是 1460 字节并且 cwnd14600 字节,则在 1 个 RTT 内发送 10 个报文段。每个到达 ACK ( 假定每个报文段一个 ACK ) 增加 1/10 MSS 的拥塞窗口长度,因此在收到对所有 10 个报文段的确认后,拥塞窗口的值将增加了 1 个 MSS.

2020-07-18-17-56-27

与慢启动的情况一样,当拥塞发生时 ssthresh 的值被更新为 cwnd 值的一半。

上次更新: 7/19/2020, 6:05:50 AM