前言

  • 面向连接的、可靠的字节流服务:

    • 必须先连接,再交互数据;
    • TCP 不对发送的字节流做任何解释和标识符插入;
  • TCP 连接:

    • socket: 编程接口,一个 IP 地址和一个端口号唯一标志一个 socket
    • ($2^{32}-1$): TCP 字节流的最大序号,循环反复; 每个被传输的字节都被计数
    • 流量控制:每一端对窗口大小进行声明; 起始于确认序号字段指明的值(默认为 4096)
  • TCP 头部

    • 2 字节源端口号+2 字节目的端口号;
    • 4 字节序号
    • 4 字节确认序号
    • {4 位首部长度,保留 6 位,6 位标志位}+2 字节窗口大小
    • 2 字节检验和+2 字节紧急指针
    • 可选选项
  • 一些注意点:

    • TCP 中仅有两方进行通信,广播和多播不能用于 TCP
    • 应用层数据会被 TCP 分割成合适的数据块,称为报文段(segment)
    • TCP 首部最长为 60 字节(4 位首部长度最大值为 15,单位为 4 字节,故最长为$4\times15=60$)

TCP 连接建立和终止

  • tcpdump:

    • 格式:源 > 目的:标志({'S', 'SYN'; 'F', 'FIN'; R, 'RST'; 'P', 'PSH'; '.', '全部置零'})
  • 三次握手:
    A 端先发SYN, B 端接收到之后,发送SYN+ACK,A 端再接收到以后,发送ACK; 此时 TCP 连接建立

    • ACK的序号始终是接收到的SYN的序号加 1(即下一个期望接收的序号)
    • ACK序号即为 TCP 首部的确认序号字段
    • SYN需要消耗一个序号,ACK则不必
  • 四次挥手
    A 端先发FIN,B 端接收到后随即发送ACK,随后 B 端的 TCP 服务器和上层应用进行交互,交互完毕后,B 端发送FIN,A 端接收后再发送ACK

    • 四次挥手而不是三次的原因:因为实际中FINACK并非合并在一起,服务端也需要由上层应用决定何时发送FIN
    • 发送FIN通常是应用层关闭的结果
  • 最大报文段长度(MSS)

    • 默认值为 536 字节
    • 如果没有分段发生,则尽可能大一点比较好(直至达到 MTU 长度)

TCP 半连接

  • 调用close()是完全断开连接;shutdown()则可以提供半连接的能力
  • 和四次挥手中的区别在于:B 端在发送ACK和发送FIN之间,还会向 A 端发送应用数据
  • 半关闭的典型应用:rsh命令rsh <request> <service-address> <input>

TCP 连接状态转移

TCP状态转换图

  • ESTABLISHED状态:双方可以进行数据传递
  • FIN_WAIT_2状态:主动关闭的一方在收到ACKFIN之间的状态
    • 处于该状态的时候,不会发送任何数据,直到接收了FIN:因为处于这个状态则说明了,本端发送的FIN包已经被彼端确认了
  • TIME_WAIT状态:也称为2MSL状态,MSL 指报文段的最大生存时间
    • MSL 肯定是有限的,因为 IP 数据报有 TTL 的存在
    • 主动关闭的一方在发送完最后一个ACK之后,必须停留 2MSL 个时间,这样可以有能力重发最后一个ACK(以防ACK丢失)
      • 处于TIME_WAIT状态期间,该端口不能再被使用(原则上); 对于客户端而言,这通常不是问题(因为客户端端口可以变);但对于服务端而言,如果出现了TIME_WAIT状态,那么这段时间,这个熟知端口就相当于被废掉了
      • SO_REUSEADDR: 该选项可以避开这一限制

TCP 复位报文段(RST)

  • 有序释放(orderly release): 发送FIN;因为FIN必须在所有排队数据发送完毕之后再发送,所以有序
  • 异常释放(abortive release): 发送RST在中途释放连接
  • 何时进行异常释放?“只要当基准连接出现错误”, 具体来说包括:
    • 向一个不存在的端口发送连接请求:此时对端会发送RST
    • 需要抛弃所有待发数据,立刻终止一个异常连接
  • 一些注意事项
    • RST的接收端不会对这个包产生任何响应,而是直接终止(复位)整个连接

TCP 处理同时打开连接

  • 通信双方同时发送SYN请求 TCP 连接;这种情况下只会打开一个连接
  • 一个同时打开的连接需要执行 4 次握手
    TCP同时打开
  • 双方同时关闭连接:
    • 双方同时发FIN
    • 双方同时发ACK

TCP 并发服务器中的须知

  • 请求队列
    • TCP 会在内核中维护一个TCP 连接的队列,
    • 当应用层忙于处理其他事务时,新的连接请求会被响应,建立成功的 TCP 连接会被放入队列之中等候应用层处理
    • 队列容量会有上限,超出上限之后,TCP 不再对新的连接请求执行任何响应
      • 这促使了客户端重发SYN包,
      • 当 TCP 队列有了空间之后,自然会重新处理这些新的请求

TCP 交互数据流

  • 交互数据大多数都是小包(例如 Telnet 之类的应用)
  • 如果没有算法去限制小包,则网络中将会充斥着一半以上数量的 TCP 报文段,其所含数据字节远远小于包头
  • 小包问题在交互式应用中尤其明显
  • 小包问题在互联网而不是局域网更可能导致麻烦
  1. 数据捎带 ACK(经受时延 ACK)
  • 即本端收到数据之后,并不是立刻发送 ACK,而是等待一段时间
    • 直至本端有数据需要发送
    • 直至等待超出上限(一般为 200ms)
  1. Nagle 算法
  • 为了在一定程度上解决小包问题
  • 核心:单个 TCP 连接在任意时刻只能存在一个未确认的包
    • 这意味着任意一端想要发送数据的时候,都得等待之前已经发送的数据的ACK确认
    • 在等待的过程中就有可能多次积累需要发送的数据,将其打包成单个数据包进行发送
  • 弊端:在高实时要求的环境中,Nagle 算法可能会造成明显时延
  • TCP_NODELAY: socket 编程中用于禁掉 Nagle 算法的选项

TCP 成块数据流

  • 成块数据的交互是依赖于很多因素的动态过程

  • 不存在单一的方法可以使得 TCP 进行成块数据的交互

  • 在 TCP 中,ACK 是累积的:ACK 表示的是一直到ACK-1的数据字节都已经被确认,而不仅仅单个数据包

  • 接收端的窗口大小通常可由应用层进程进行指定

    • 在网络状况以及接收端主机处理能力良好的情况下,适量增大窗口大小可以提升吞吐率
  • 滑动窗口

    • 左边沿和右边沿应该都是向右单向移动
    • 如果有 ACK 指示左边沿需要向左移动,则被认为是重复 ACK,直接丢弃;故左边沿不可能向左移动
    • 右边沿向左移动是强烈不赞成的
      滑动窗口可视化
  • PUSH标志

    • 目前的 TCP 编程接口不提供用户自行设置PUSH
    • PUSH的作用:提示接收方需要将这个带有PUSH的报文段和 TCP 缓冲区中的报文段立刻交付给应用层
    • PUSH由发送方的 TCP 层自动设置:当发送端清空了发送缓冲区的时候
  • 慢启动

    • 新分组进入网络的速率应该和另一端返回确认的速率相同
    • 拥塞窗口:发送方使用的流量控制(以报文段个数为单位)
    • 通告窗口:接收端使用的流量控制
  • 传播时延 VS. 发送时延

  • 带宽时延积:capacity(bit) = bandwidth(b/s) * round-trip-time(s)

  • URG:紧急标志(带外数据)

    • 虽然也被称作带外数据,但实际上还是在单条连接中传输的数据
    • 通过设置 ①**URG标志位以及 ②16bit 的紧急指针偏移**
      • 紧急指针指向的是紧急数据的最后一个字节的位置
    • 接收方将URG所在的位置(字节序号)和紧急指针所在的位置(字节序号)之间的所有数据都视为“紧急方式”(带外数据)
    • 即使接收方通告了接收窗口为 0,紧急数据依然可以被发送

TCP 拥塞控制

  • TCP 管理的 4 类定时器

    • 重传定时器(用于拥塞控制)
    • 坚持(persist)定时器:用于保持发送窗口的流动
    • 保活(keep-alive)定时器:用于探测空闲连接
    • 2MSL 定时器:即四次握手之后的 TIME_WAIT 状态
  • 超时重传时间上限:首次分组传输与复位(RST)信号传输之前的时间间隔约为 9min,不可更改

  • 超时重传的重点:

    • 测量 RTT:因为 RTT 会不断变化,所以 TCP 有义务根据网络的 RTT 去动态改变超时重传间隔(RTO);
      • 注意,当超时和重传发生时,在收到 ACK 之前,不能更改 RTO(因为无法确定这个 ACK 是属于重传数据还是之前的数据的)
  • 坚持定时器:

    • 当发送方需要发送数据但被通告了接收窗口为 0时,就需要设置坚持定时器;
    • 作用:防止死锁(① 某一时刻发送方的窗口为 0;② 接收方准备好接收数据,发送了 ACK 来更新发送方的窗口;③ 但该 ACK 丢失了;④ 之后接收方一直等待着发送方的数据,而发送方一直等待着接收方的 ACK)
    • 流程:当发送端长时间未收到 ACK 时,便每隔一段时间(不超过 1min)就发送一个 ACK(注意,该 ACK 会包含一个字节)给接收方,以进行问询。如果接收方返回了 ACK(该 ACK 是否会对发送方 ACK 中的那个字节进行接收并确认是由接收端缓冲区是否有空闲空间决定),则说明连接是正常的,反之,则连接有误,应该断开。
  • 糊涂窗口综合症:

    • 定义:其实就是发送方发送数据速率和接收方的接收速率不匹配导致的小包问题。
    • 举例:接收方接收速率很慢,导致缓冲区被占满;随后接收方处理完了 1 个字节的数据,然后 TCP 发送通告,告知它的接收窗口为 1;随后发送方就只能发送小包;
    • 解决思路:
      • 接收方:当空闲缓冲区足够大之后,再更新窗口;
      • 发送方:延迟发送数据,以积累足够多的数据构成大包

  • 慢开始:并不是说开始的慢,而是拥塞窗口 cwnd 从一个很小的值开始,并按指数增长;(慢慢的探测网络拥塞程度)

  • 拥塞避免:当“慢开始”中的 cwnd 大小超过阈值 ssthresh 之后,改为线性增长,每经过一个 TTI 加 1;

    • 出现拥塞情况:即未收到指定 ACK;此时则将 ssthresh 减至 cwnd 的一半,并将 cwnd 变为 1,然后重新启动“慢开始”
  • 快重传:当接收端收到失序的数据报后,连续发出重复 ACK;而发送端只要接收到连续 3 个重复 ACK,则立刻重发导致失序的丢失的数据报

  • 快恢复:在执行“快重传”的同时,将 ssthresh 减至 cwnd 的一半,并执行“拥塞避免”。(原因就是此时网络很可能其实没有出现拥塞,因此执行“拥塞避免”而不是“慢开始”)