传输层协议 (TCP & UDP) 概述

1 传输层协议的特性

传输层是两端互联传输数据过程中非常重要的一层,其重要特性如下:

  • 提供网络中不同主机上用户进程之间的数据通信、可靠与不可靠的传输;
  • 传输层报文段的数据检测;
  • 失败重传机制;
  • 流量控制机制;
  • 拥塞控制机制。

这些特性是靠各协议实现的,所以传输层协议就是上述功能的核心。

传输层协议有 TCP、UDP、SPX、NetBIOS、NetBEUI 等,其中 TCP 和 UDP 是使用范围最广的传输层协议。

2 TCP 协议

TCP (Transmission Control Protocol) 译为传输控制协议,它是面向连接的全双工传输层协议,提供可靠的传输服务。

TCP 头信息的数据结构如下图所示:

TCP 头信息

该结构图从上往下,每一行 32 位数据后紧跟着下一行 32 位数据,其各部分内容如下:

  • Source port (16 bit):识别发送连接端口;
  • Destination port (16 bit):识别接收连接端口;
  • Sequence number (seq, 32 bit):一次 TCP 通信过程中某一个传输方向上的字节流的每一个字节的编号,通过这个编号确认发送的数据顺序,比如现在的序列号为 100,发送了 1000 个字节后,下一个序列号便是 1100;
  • Acknowledge number (ack, 32 bit):用来响应 TCP 报文段,给收到的 TCP 报文段的序号 +1;
  • Data offset (ack, 4 bit):用来表示头部信息有多少个字节,从它有 4 位可以看出 TCP 头部最大长度为 60 字节;
  • Reserved (3 bit):保留位,用于协议扩展;
  • 标志位 (9 bit)
    • NS:它是一个随机和,用于防止 TCP 发送端的数据包标记被意外或恶意改动,可以有效排除潜在的 ECN 滥用情况;
    • CWR:该数据位与拥塞控制有关,是拥塞窗口减少请求量的标志,用来表示它收到了一个设置 ECE 标志的 TCP 报文段,并且会在拥塞控制机制中做出对应的处理,以实现对网络拥塞情况的控制;
    • ECE:拥塞控制相关数据位,用来表示两端之间的通信是否存在网络拥塞
      • 若 ECE = 1,则会通知对端,从对端到本端的网络有阻塞。
      • 如果收到的数据包的 IP 地址头部携带的 ECE 为 1,则 TCP 的头部携带的 ECE 也会被设为 1。
    • URG:该标志位代表紧急指针是否有效,当 URG = 1 时,表明客户端进行数据传输时该 TCP 报文段中有紧急数据要传输。此时传输层会把紧急数据插入到报文段数据的最前面,在紧急数据后面的数据依旧是普通数据,该标志位需要与 Urgent pointer (紧急指针) 配合使用;
    • ACK:该标志位用于确认 Sequence Number 是否有效
      • 当 ACK = 1,确认序号有效;
      • 当 ACK = 0,确认序号无效;
      • TCP 规定,建立连接后,所有传输的报文段的 ACK 必须为 1。
    • PSH:该标志位表示通知服务器应该立刻从 TCP 接收缓冲区中将数据带走;
    • RST:该标志位表示由于一些异常情况,需要重新建立连接;
    • SYN:该标志位表示该 TCP 数据报文段为建立连接请求;
    • FIN:该标志位表示该 TCP 数据报文段为断开连接请求。
  • Window size (16 bit):TCP 流量控制特性所需要的数据位,用来告诉对端 TCP 缓冲区还能容纳多少字节数据;
  • Checksum (16 bit):校验和用于数据的错误检测,它由客户端生成并填充,服务器将对报文段进行 CRC 算法校验,以检验 TCP 报文段在传输过程中是否损坏;
  • Urgent pointer (16 bit):紧急指针是一个正向的偏移量,它和 Sequence number 的值相加表示最后一个紧急数据的下一个字节的序号,用于标记哪一段数据是紧急数据,方便传输层将这些紧急数据插到报文段的最前面。

2.1 TCP 三次握手

所谓三次握手 (Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器最少需要发送 3 个包。

进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号 (Init Sequense Number, ISN) 为后面的可靠性传输做准备。

三次握手过程如下图:

TCP 三次握手
  1. 首先,服务器调用 listen() 函数初始化接收队列,开始监听指定端口;
  2. 然后,客户端调用 connect() 函数触发三次握手过程;
  3. 第一次握手:
    • 首先客户端将本地 socket 状态设置为 TCP_SYN_SENT,然后选择一个可用端口准备发送数据;
    • 然后客户端构造 SYN 报文段发送给服务器:
      • SYN 报文段中的 SYN 标志位为 1;
      • Sequence number 版本号为本报文段所发送的数据的第一个字节的随机序号;
    • 最后启动重传定时器,如果超时后没有收到服务器 ACK,就重新发送。
  4. 第二次握手:
    • 服务器收到 SYN 报文段后,首先判断半连接队列是否满了,如果已满且未开启 tcp_syncookies,就丢弃此包;
    • 如果半连接队列未满就接着判断全连接队列,如果全连接队列已满且 young_ack1 数量大于 1,同样也丢弃此包;
    • 如果两个连接队列都符合要求,就构造 SYN ACK 报文段发给客户端:
      • ACK 报文段中的 SYN 标志位也为 1(因为其本质也是个握手包);
      • 设定 ACK 报文段的 Sequence number 字段;
      • 将 ACK 报文段的 Acknowledge number 字段设置为客户端 SYN 报文段的 Sequence number 字段 +1。
    • 最后将当前握手信息添加到半连接队列,设置连接状态为 SYN_RCVD,并开启计时器。如果某个时间段内还收不到客户端的第三次握手,就重传 SYN ACK。
  5. 第三次握手: 客户端收到 SYN ACK 后,清除重传定时器,同时将 socket 状态设置为 ESTABLISHED,最后开启保活计时器并发出第三次握手 ACK 报文段确认:
    • 第三次 ACK 报文段的 Acknowledge number 同样为服务器 ACK 报文段的 Sequence number + 1
  6. 服务器收到第三次握手 ACK 后:
    • 首先判断全连接队列是否已满,如果满了就修改一下计数器,直接丢弃握手包;
    • 如果未满,就将连接从半连接队列删除,然后创建子 socket 添加到全连接队列;
    • 最后将连接状态设置为 ESTABLISHED
  7. 至此,客户端与服务器的握手就完成了,服务器此时可以调用 accept() 接收客户端请求。

2.2 TCP 四次挥手

建立一个 TCP 连接需要三次握手,而终止一个 TCP 连接要经过四次挥手,这是由于 TCP 的半关闭 (half-close) 特性造成的,TCP 提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

TCP 连接的释放需要发送四个包(执行四个步骤),因此称为四次挥手 (Four-way handshake),客户端或服务器均可主动发起挥手动作。这里我们以客户端请求断开连接为例:

TCP 四次挥手

刚开始双方都处于 ESTABLISHED 状态,然后客户端发起断开连接请求,四次挥手过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文段:
    • FIN 报文段的 FIN 标志位置 1;
    • 报文段中会指定一个序列号 Sequence number,并停止发送新数据;
    • 此时客户端处于 FIN_WAIT1 状态,等待服务器的确认。
  • 第二次挥手:服务器收到 FIN 报文段之后,发送 FIN ACK 报文段:
    • FIN ACK 报文段把客户端的 Sequence number +1 作为 ACK 报文段的 Acknowledge number,表明已经收到客户端的报文段了;
    • 同时 FIN ACK 会设定自己的 Sequence number
    • 此时服务器处于 CLOSE_WAIT 状态(防止客户端没有收到 FIN ACK,又重发 FIN 请求)。
  • 客户端收到 FIN ACK 后,进入 FIN_WAIT2 状态,等待服务器发出的连接释放报文段。
  • 服务器 CLOSE_WAIT 超时后依旧没有收到新的 FIN 请求,说明 FIN ACK 发送成功,此时客户端到服务器的 TCP 半连接就正常关闭了。
  • 第三次挥手:如果服务器处理完了所有的请求,可以断开连接了,和客户端的第一次挥手一样,也发送一个 FIN 报文段:
    • 服务器 FIN 报文段的 FIN 和 ACK 标志位均置 1;
    • 报文段中会指定一个序列号 Sequence number
    • 报文段中的 Acknowledge number 依旧是第二次挥手时的序列号;
    • 此时服务器处于 LAST_ACK 的状态,等待客户端的确认。
  • 第四次挥手:客户端收到 FIN 请求之后,一样发送一个 FIN ACK 报文段作为应答:
    • 客户端 FIN ACK 报文段的 FIN 和 ACK 标志位均置 1;
    • 以服务器 FIN 报文段中的 Acknowledge number 作为客户端 FIN ACK 报文段的 Sequence number
    • 以服务器 FIN 报文段中的 Sequence number +1 作为 Acknowledge number
    • 然后客户端进入 TIME_WAIT 状态。
  • 服务器收到 FIN ACK 应答报文段后,进入 CLOSE 状态。
  • 等待 2MSL 后,客户端也进入 CLOSE 状态(防止服务器没收到 FIN ACK,又重发了 FIN 请求)。

3 UDP 协议

UDP (User Datagram Protocol) 是一种面向无连接的协议,该协议关注的是传输速度,并不保证传输的可靠性,也无法避免收到重复数据的情况。UDP 的头部数据结构如下:

UDP 头信息

从上图可以看出,UDP 的头信息非常简单,源端口号和目的端口号与 TCP 一致,除此之外,UDP 只有数据包长度和校验和两部分内容。

UDP 头部信息比 TCP 少得多,对传输过程的控制也少的多,因此 UDP 传输速度要快于 TCP,但是可靠性上差了很多。

现在绝大部分软件都是基于 TCP 的,因为传输过程的可靠性是大部分场景所必须的,但随着技术的发展,后续依靠 UDP 实现可靠性传输越来越成为主流,例如现在的 HTTP3 以及 QUIC 就是这么做的,这些协议将可靠性保证下放到了应用层,既保证了可靠性,又方便了可靠性算法的灵活实现。


  1. young_ack 是半连接队列里保持着的一个计数器,记录的是刚有 SYN 到达,没有被 SYN_ACK 重传定时器重传过,同时也没有完成过三次握手的 sock 数量。 ↩︎


欢迎关注我的公众号,第一时间获取文章更新:

微信公众号

相关内容