Socket API and Transport Layer Protocol

1. Transport-Layer Made Easy


我们先要明确传输层的作用是什么。网络协议栈有五层:应用层、传输层、网络层、数据链路层和物理层,每一层的职责都是为上层提供服务。所以,传输层存在的意义就是封装网络层,为应用层提供逻辑上的通信服务。

应用层进程会使用一个虚拟端口来请求使用传输层所提供的逻辑通信,规避掉底层传递信息的实现细节,使得应用开发者可以专注于应用逻辑。极大地简化了网络编程和应用开发。

1.1 Network Ports

在互联网中,我们用 IP 地址来标识一台主机,用端口号来标识主机上的某个特定服务或应用程序的编号,这里的端口号是虚拟的。端口号与设备的IP地址一起工作,IP地址负责定位设备,而端口号则负责定位设备上的具体应用程序。你可以用一个 pair(IP, Port) 来唯一地标识互联网上的通信端点。

我们举一个快递驿站的例子。我们把社区比作主机,把房子比作进程,每个社区都会有一个快递驿站负责社区间快递的邮递。而驿站只是一个快递寄存和分发的地方,也就是说驿站依托邮递公司把快递传输到不同社区中,随后快递寄存在社区的驿站中并分发不同的快递。

这里,快递驿站其实就都需要传输层协议,快递公司就相当于网络层协议。通过社区的地址(IP 地址),邮递公司可以把快递(数据)送到目的社区(主机)那里。随后,驿站会录入包裹信息并给不同的人发送信息进行包裹的分发。在传输层协议中,这一步被称为复用/解复用。

1.2 Multiplexing and Demultiplexing

当一台主机上的多个进程需要通过网络发送数据时,传输层通过端口号标识不同进程的数据来源。传输层将来自不同端口的数据整合为段(segment)或数据报(datagram),并交给网络层处理,这一过程称为多路复用。

同样的,当传输层从网络层接收到数据时,它会通过解析协议头部中的端口号信息来确定数据的目标进程,然后通过 Socket 接口 将数据分发到对应的进程。这一过程称为多路解复用。

1.3 Sockets: Interface for Applications

在应用层,系统为我们提供了操作窥视传输层和网络层的 API socket。Socket不同于端口号,它是在应用层对端口号的抽象与扩展。Socket结合了 IP 地址和端口号和协议,形成了网络通信的端点。Socket和端口号通过绑定函数来关联。

不同协议的 socket 也是不同的。在 UDP 协议中,socket 由 IP 和 端口号二元组所标识。而在 TCP 中,socket 是一个四元组,包括源 IP 地址、源端口号、目标 IP 地址 和 目标端口号。在一定程度上,socket 标识解释了不同协议的特点。

我们下面详细地给出 socket 接口的函数原型:

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
/* 
Parameters:
	1. Domain: address format; 
		- AF_INET: IPv4
		- AF_INET6: IPv6
		- AF_UNIX/AF_LOCAL: Unix domain sockets
		- AF_PACKET: Low-level packet interface
	    - AF_NETLINK: Kernel/user-space communication
	    - AF_X25: ITU-T X.25 protocol
	    - AF_AX25: Amateur radio AX.25 protocol
	    - AF_BLUETOOTH: Bluetooth communication
	    - AF_CAN: Controller Area Network protocols
	    - AF_TIPC: Transparent Inter-process Communication
	    - AF_ALG: Cryptographic algorithms (Linux-only)
	    - AF_ECONET: Acorn Econet protocol
	    - AF_RDS: Reliable Datagram Sockets (cluster communication)
	2. Type: what kind of data;
		- SOCK_STREAM: Reliable, two-way, connection-oriented
		- SOCK_DGRAM: Connectionless, datagram socket
		- SOCK_RAW: Provides direct access to lower-layer protocols (IP headers)
		- SOCK_SEQPACKET: Similar to SOCK_STREAM but preserves message boundaries
		- SOCK_RDM: Reliable datagram messages (not widely supported)
		- SOCK_PACKET: Obsolete, used for raw packets at the device driver level
	3. Protocol: how data is transported; 0 for type inference
		- IPPROTO_TCP: Transmission Control Protocol
		- IPPROTO_UDP: User Datagram Protocol
		- IPPROTO_RAW: Raw IP packets
		- IPPROTO_ICMP: Internet Control Message Protocol (Network layer)
		- IPPROTO_SCTP: Stream Control Transmission Protocol
		- IPPROTO_IGMP: Internet Group Management Protocol (Network layer)
		- IPPROTO_EGP: Exterior Gateway Protocol
		- IPPROTO_PIM: Protocol Independent Multicast
		- IPPROTO_IPV4
		- IPPROTO_IPV6
Return value: 
	- Return a socketfd on success.
	- -1 on failure.
*/

1.3.1 Address Family

socket API 的第一个字段是用于定义套接字使用的通信域,也就是协议族。协议族决定了 socket API 的后续地址数据结构的格式和套接字能够进行的通信范围。常见的用于网络通信的字段选项有 AF_INET, AF_INET6

AF_INET 用于定义一个 IPv4 的地址族,即基于 32 位 IP 地址的网络通信。使用时,我们会将 IPv4 的相关地址信息存储到结构体 struct sockaddr_in 中。

AF_INET6 用于定义一个 IPv6 的地址族,它和 AF_INET 的作用是一样的,只不过支持 128 位 IP 地址,地址空间大大扩展,可支持更多的设备。甚至有说法说你可以给地球上的每粒沙分配一个 IPV6 的地址。

1.3.2 Socket Type

第二个字段用来定义套接字如何传输数据,套接字的行为。这里我们介绍三种不同的套接字:SOCK_STREAM, SOCK_DGRAMSOCK_SEQPACKET。我们拿一个表格比较它们的特性:

可靠性/顺序性 面向连接 拥塞控制/流控制 传输方式 消息边界
SOCK_STREAM 支持 支持 连续的字节流
SOCK_DGRAM 不支持 不支持 独立的数据报
SOCK_SEQPACKET 支持 支持 独立的”消息流“

1.3.3 Transmission Protocol

第三个参数用于指定具体的协议。这些协议中有传输层的协议,也有网络层的协议。一般,你可以将此位设置为 0 让系统自己推断选择哪个协议。但你也可以显式地制定出来。这里,我们关注三个和传输层相关的协议,TCP, UDP 和 SCTP。

2. UDP: No-Frill, Unreliable Protocol


UDP是最简单的传输层协议,它的唯一作用就是提供传输层的多路复用和解多路复用的功能。没有可靠的传输服务,也不保证数据有序到达。在建立 UDP 的网络连接时,UDP 也不维护连接,所以 UDP 是一个无连接的传输层协议。

IP 协议提供尽力而为的服务,而 UDP 实际上也并不提供说明服务。所以实际上 UDP 算得上一个最小的传输层协议,在 IP 的基础上提供尽力而为的服务。所以,你会看到 UDP 的 RFC 768 文档实际上也非常短小,只有两页。

2.1 UDP Header

UDP 实际上好像什么也没有提供,复用和解复用是它的职责。除了传输层的职责,UDP 还会简单地校验传过来的数据,尽量避免 flip bits 的干扰。下面是 UDP 头:

0                     15                     31
+----------------------+----------------------+
|  Src Port (16 bits)  |  Dest Port (16 bits) | 
+-----------------------+---------------------+
|  Length (16 bits)    |  Checksum (16 bits)  |
+----------------------+----------------------+
|                                             |
|                  Contents...                |
|                                             |
+----------------------+----------------------+

在 UDP 头中,我们看到了四个字段:源端口、目的端口、长度、校验和。这四个字段各 16 字节。

2.1.1 Ports

源端口和目的端口不难理解,这是传输层协议用来实现多路复用/解复用功能的必要项。

2.1.2 Length

UDP 是面向数据报传输的协议。UDP 的头中维护一个长度字段,这个字段用于指示整个数据报(即头部和数据)的总字节数。通过长度字段,你可以分辨一个 UDP 数据报的边界在哪里,这样,接收方可以准确地知道如何从接收到的字节流中提取完整的 UDP 数据报。

由于 UDP 天然保留消息边界,应用层无需担心出现粘包或拆包的问题。这与 TCP 不同,TCP 是面向流传输的协议,不保留消息边界,因此可能导致粘包或拆包问题,需要应用层协议进行额外处理。我们留在学习 TCP 的时候进行介绍。

2.1.3 Checksum

UDP 校验和为传输层提供了一种简单的纠错机制,用于检查从源到目的的数据传输过程中是否有噪音污染。如果在传输过程中出现干扰,在 UDP 下,接收端就会直接丢弃该数据段,一定程度上避免了把错误的数据传给应用进程。

下面,我们来学习这种校验和是如何发挥作用的。UDP 在计算校验和时,会在 UDP 报文段的头部前加入一个伪头部 (Pseudo header) 。

0                     15                     31
+----------------------+----------------------+
|               Source IP address             | 
+----------------------+----------------------+
|            Destination IP address           |
+-----------+----------+----------------------+
|    Zero   | Protocol |      UDP Length      |
+----------------------+----------------------+

UDP 校验和的计算过程十分简单,这种校验方式叫做反码求和。其过程如下:

  1. 从“伪头部”开始,按每 16 位当作一个数,逐次求和,最终得出一个 32 位的数。
  2. 如果这个 32 位的数的高 16 位不为 0 ,则进行“回卷”操作。
  3. 重复第 2 步直到高 16 位为 0 。
  4. 最终,将低 16 位取反,得到校验和,填入 checksum 字段中。

简单举个例子,我们要将数据从 IP source:192.168.2.1,Port src:8080 传递到 IP dest:192.168.2.2,Port source:9090。这里的8位协议字段 (protocol) 是 17 ,表示UDP协议。再假设我们要传 4字节的数据字段(0x1234 55aa)。

小端方式表示的UDP segment如下:

Pseudo Header:

0-7bits 8-15bits 16-23bits 24-31bits
Source IP addr. 0000 0001(1) 0000 0010(2) 1010 1000(168) 1100 0000(192)
Destination IP addr. 0000 0010(2) 0000 0010(2) 1010 1000(168) 1100 0000(192)
Zero & Protocol & length 0000 0000 0001 0001 0000 0100 0000 0000

UDP Header:

0-7bits 8-15bits 16-23bits 24-31bits
src port & dest port 1001 0000 0001 1111 1000 0010 0010 0011
length & checksum 0000 0100 0000 0000 0000 0000 0000 0000

Segment Body:

Body data 0-7bits 8-15bits 16-23bits 24-31bits
0x1234 55aa 1010 1010 0101 0101 1000 0011 0001 0010
	 0000 0010 0000 0001      Pseudo Header Starts HERE
	+1100 0000 1010 1000
	————————————————————
	 1100 0010 1010 1001(SUM)
	+0000 0010 0000 0010
	————————————————————
	 1100 0100 1010 1011(SUM)
	+1100 0000 1010 1000
	————————————————————
   1 1000 0101 0101 0011(SUM)
	+0001 0001 0000 0000 +1
    ————————————————————
	 1001 0110 0101 0100(SUM)
	+0000 0000 0000 0100
	————————————————————
	 1001 0110 0101 1000(SUM)
	+0001 1111 1001 0000      UDP Header Starts HERE
	————————————————————
	 1011 0101 1110 1000(SUM)
	+0010 0011 1000 0010
	————————————————————
	 1101 1001 0110 1010(SUM)
	+0000 0100 0000 0000
	————————————————————
	 1101 1101 0110 1010(SUM)
	+0101 0101 1010 1010      UDP Body Starts HERE 0x1234 55aa
	————————————————————
   1 0011 0011 0001 0100(SUM)
    +0001 0010 1000 0011 +1
    ————————————————————
     0100 0101 1001 1000
    ^
    ————————————————————
     1011 1010 0110 0111(CHECKSUM) 

在接收端,传输层会将UDP segment中每 16 位相加并加上进位,最终与 CHECKSUM 相或,若结果不是 0xFFFF(即全1)说明报文段出错,应舍弃。

2.2 Create A UDP Socket

我们发现,SOCK_DGRAM 和 UDP 极为匹配,所以我们用下面的方法来创建一个 UDP 的 socket :

// IPv4
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

3. TCP: Stream-Oriented Reliable Delivery


相比 UCP ,TCP 给人的第一印象就是它是靠得住的传输层协议。因为它提供可靠有序字节流传输,如此,应用程序就可以确保自己能够收到的数据是完整有序的。因此,HTTP 超文本传输协议、FTP 文件传输协议、SMTP 电子邮件、DASH 流媒体协议等都是在 TCP 传输层协议上实现的。

除了可靠性,TCP 还有许多特性。比如 TCP 维护点对点的连接(四元组),提供全双工的单播 (unicast) 数据传输。面向流的传输服务(没有消息边界)。此外,TCP 还提供拥塞控制、流控制。我们下面一点点来介绍。你可以在 RFC 9293 获得更多关于 TCP 的知识。

3.1 TCP Header

TCP 提供的特性大多都体现在 TCP 头部中。TCP 头部包含多个字段,用于确保可靠的数据传输、流量控制、错误检测等功能。以下是 TCP 头部的结构:

0                              15                               31
+-------------------------------+--------------------------------+
|      Source Port (16)         |      Destination Port (16)     | 
+-------------------------------+--------------------------------+
|                        Sequence Number (32)                    |
+-------------------------------+--------------------------------+
|                     Acknowledgment Number (32)                 |
+-------------------------------+--------------------------------+
|Header |Unused |C|E|U|A|P|R|S|F|                                |
|Length |       |W|C|R|C|S|S|Y|I|   Receiver Window Size (16)    |
| (4)   | (4)   |R|E|G|K|H|T|N|N|                                |
+-------------------------------+--------------------------------+
|    Internet Checksum (16)     |    Urgent Data Pointer (16)    |
+-------------------------------+--------------------------------+
|                   Options (0-40 bytes, as needed)              |
+-------------------------------+--------------------------------+
|                         Data (payload)                         |
+-------------------------------+--------------------------------+

3.1.1 Port Numbers

和 UDP 一样,TCP 是一个传输层协议,它依赖下层 IP 协议的 IP 地址来寻到网络中的主机。在 TCP 头中,它还需要提供主机上的需要源端口号 (Source Port)目的端口号 (Destination Port) 这两个字段将源主机源端口的数据转发给目的主机目的端口的应用上。这是作为传输层协议最重要的两个字段。

3.1.2 Sequence Number and Acknowledgment Number

之后的 64 位,我们有序列号 (Sequence Number)确认号 (Acknowledgment Number)。这两个字段各占据 32 位,用于数据包的排序和确认,是 TCP 实现可靠性传输的依据。

序列号是发送数据段的第一个数据字节的序列号,在 SYN 标志未设置时,表示该数据段中第一个字节相较于初始序列号的偏移量。如果 SYN 标志被设置位 1,那么序列号就为初始序列号 (Initial Sequence Number),段中的第一个数据字节序列号就为 ISN + 1 。

确认号用于接收方向发送方反馈数据已成功接收。接收方收到数据段后,需要将 ACK 标志位设置为 1 来表示该数据段包含有效的确认号。确认号的值表示发送方期望接收的下一个序列号。一旦连接建立,确认号就会始终设置为 1 以保证数据传输的可靠性。

3.1.3 Data Offset

TCP 头部数据偏移字段,也叫头部长度字段,表示 TCP 头部包含多少个 32 位字(4 字节)。因为 TCP 的头长度不定,在 20-60 字节之间。除了固定的 20 字节的头之外,还提供 Options 字段来支持一些时间戳、窗口缩放、选择性确认等高级功能。头部长度字段有 4 位,可以表示的最大值是 15 。所以 TCP 头最大为:

3.1.4 Reserved Bits

TCP 头部有 4 位保留位,用于为将来预留新的控制位。在实现上,这四个位必须为 0,在暂未实现这些控制位前,接收方在处理头部时必须忽略这四位。

3.1.5 Control Bits

TCP 控制位 (Control Bits) 也叫标志位 (Flags)。它们是 TCP 强大的原因之一,目前分配的控制位有:CWR, ECE, URG, ACK, PSH, RST, SYN, FIN。

  1. CWR (Congestion Window Reduced):拥塞窗口减少位,用于主动的 TCP 拥塞控制。
  2. ECE (ECN-Echo):显示拥塞控制通知回显位,用于被动的 TCP 拥塞控制(网络层通知)。
  3. URG (Urgent):表示紧急指针有效,表示发送方希望接收方优先处理的数据。
  4. ACK (Acknowledgment):确认号有效。
  5. PSH (Push):发送方请求接收方立即处理该数据(用于实时交互)。
  6. RST (Reset):强制终止 TCP 连接。
  7. SYN (Synchronize):同步接收方发送方之间的 ISN 序列号。
  8. FIN (Finish):表示发送方不再发送数据,优雅关闭 TCP 连接。

3.1.6 Window Size

当发送方向接收方发送数据时,接收方会维护一个缓冲区来存储和排序数据,以确保数据按正确顺序到达。这个缓冲区被称为滑动窗口 (Sliding Window),它用于控制数据流,防止发送方过快发送数据而导致接收方处理不过来。

窗口大小必须是一个无符号数,如果窗口大小被误解为负值,TCP 就无法正常工作。在建议的实现方式中,协议栈应当预留 64 位来存储 32 位的发送窗口和 32 位的接收窗口,确保所有的窗口相关计算都使用 32 位数值计算。

发送窗口用来限制发送方可以发送但未确认的数据量。接收窗口用于表示接收方能够接受的最大数据量。TCP 默认的窗口大小为 65535 字节(16 位字段),但为了支持更大的窗口,TCP 可以通过设置头部长度来引入窗口缩放选项 (Window Scaling Option) 来让窗口大小扩展到 字节(也就是 1GB)。

3.1.7 Checksum

和 UDP 一样,TCP 的校验和计算方法也是通过 16 位的回滚求和来获取 16 位校验和字段。在计算时,TCP 头部前面会加入一个伪头部,下面是 IPv4 的 TCP 伪头部(96 位,IPv6 320 位):

0                              15                               31
+-------------------------------+--------------------------------+
|                          Source Address                        |
+----------------------------------------------------------------+
|                        Destination Address                     |
+----------------------------------------------------------------+
|      zero      |     PTCL     |            TCP Length          |
+----------------------------------------------------------------+
					    IPv4 TCP Pseudo Header

在反码求和时,需要计算包括 TCP 头部(包括伪头部)和数据段的所有 16 位字,确保所有的数据都为 16 位对其。如果一个数据段包含字节数不足以对其 16 位,则在最后一个字节右侧填充 0 以形成 16 位字进行校验。

3.1.8 Urgent Pointer

紧急指针是一个 16 位字段,用于指示紧急数据的位置(紧急数据后的第一个字节就是紧急指针指向的位置),只有在 URG 标志位被设置时才会触发。

3.1.9 Options

TCP 头部的数据偏移字段决定了 TCP 头部的长度。TCP头部长度以4字节为单位,最小值为5(对应20字节标准头)。当Data Offset > 5时,则存在选项字段。

选项字段有两种格式:

  • Case 1: 单字节选项(Kind = 0 或 Kind = 1)
  • Case 2: 多字节选项(Kind >= 2)
Kind Length Meaning Implementation
0 - End of Option List Option (EOL) MUST
1 - No-Operation (NOP) MUST
2 4 Maximum Segment Size (MSS) MUST
3 3 Window Scale SHOULD
4 2 SACK Permitted SHOULD
5 Vary SACK SHOULD
... ... ...
8 10 Timestamps SHOULD
... ... ...
27 8 Quick-Start Response OPTIONAL
28 4 User Timeout Option SHOULD
29 Vary TCP Authentication Option (TCP-AO) SHOULD
30 Vary Multipath TCP (MPTCP) SHOULD

单字节选项表示该选项字段仅有一个字节(Kind 字段),没有长度和数据。

多字节选项表示该选项字段包含 Kind(一个字节)、Length(一个字节)和可变长度的 Data (长度在 Length 中指定,长度为整个选项的长度)。

选项字段以 4 字节对其(如果选项长度为 3 字节,就需要额外添加 1 个 NOP)。

3.2 TCP Terminology

TCP 是面向连接的传输层协议,所以每一个 TCP 连接的建立都对应这操作系统 TCP 协议栈中一个数据结构的建立,用于存储连接的所有状态信息。这个数据结构就是 TCB (Transmission Control Block)。我们已经学过了 TCP 的头部,在 TCB 中,其核心的字段包括:

Field Category Specific Content
Connection Identifier Local IP/Port, Remote IP/Port (4-tuple uniquely identifying the connection)
Security Parameters IPsec Security Association (SA), TLS context (if encryption is enabled)
Buffer Management Send buffer pointer (outgoing data), Receive buffer pointer (received data), Retransmission queue pointer
Sequence Number Management Send sequence variables (SND.), Receive sequence variables (RCV.)
Congestion Control Congestion window (cwnd), Slow start threshold (ssthresh), Retransmission timeout (RTO)
Connection State Machine Current connection state (e.g., LISTENESTABLISHEDTIME_WAIT, etc.)

这里,我们关注 TCB 中的序列号管理相关的变量和 TCP 的连接状态机。

3.2.1 Sequence Number Management

TCP 是可靠性传输层协议,这意味着 TCP 需要实现一种机制使得传输双方能够确认对方有收到数据。所以无论是建立连接方还是被建立连接方,连接建立的双方都需要向对方发送并接受数据。表明自己收到了数据。

所以无论是连接的哪方,都需要维护:

  • 作为发送方序列号的相关变量
  • 作为接收方序列号的相关变量
  • 连接状态拥塞控制变量
Send and Receive Sequence Variables

下面给出了主机作为发送方和接收方需要维护的相关变量。其中 ISSIRS 是两个特殊的变量。连接建立时双方会交换初始序列号用于连接的初始化。自己的初始序列号就存在发送方的 ISS 变量中,对方的初始序列号存放在 IRS 变量中(因为交换的过程中自己作为接收方)。

Variable Description
SND.UNA send unacknowledged
SND.NXT send next
SND.WND send window
SND.UP send urgent pointer
SND.WL1 segment sequence number used for last window update
SND.WL2 segment acknowledgment number used for last window update
ISS initial send sequence number
Variable Description
RCV.NXT receive next
RCV.WND receive window
RCV.UP receive urgent pointer
IRS initial receive sequence number

TCP 是通过滑动窗口机制来实现流量控制和可靠传输的。通过滑动窗口,主机就可以知道在缓冲区中哪些数据是已经被收到的,哪些数据是已经发生但没有被收到的,哪些数据是没有发送但可以发送的,哪些消息是不允许发送的。

维护连接的任一方会维护一个作为发送方时用得到的发送缓冲区,窗口就通过上面的发送序列变量来划分。 SND.UNA 是一个指针指向已发送但未收到 ACK 的数据起始点,SND.WND 是由接收方告知的剩余缓冲区容量,SND.NXT 也是一个指针指向下一个待发送的数据位置(不允许大于 SND.UNA + SND.WND)。
TCP_Sender_Window.png
通过这三个变量,发送方就知道哪些数据接收方收到了,哪些没有收到。这里你可以看到,发送方的窗口是要小于缓冲区的。

此外,维护连接的任一方还会维护一个接收缓冲区。接收方会使用两个变量:RCV.NXTRCV.WND 来划分窗口。RCV.NXT 代表接收方期待的下一个字节序号,即接收窗口的起始位置。RCV.WND 代表接收方的可用缓冲区大小,决定了可以接收的数据范围。
TCP_Receiver_Window.png
接收方的缓冲区受 TCP 的拥塞控制和流量控制动态变化,以避免过载和占用过多的网络资源。

需要注意的是,这些序列号在实现时通常用一个 4 字节的 unsigned int 来存储。所以实际上的序列号范围是从 - 这么大。其中 ISS(初始序列号)相当于一个基准地址 (Base Address),而实际的序列号是基于 ISS 的偏移量计算得到的。(module

此外,TCB 中还会维护一个段变量,这是当前段的一些相关变量。发送方会维护 SEG.SEQ(当前段的起始序列号)和 SEG.ACK(表示确认序列号,用于确保数据正确排序并传输)。接收方会维护 SEG.ACK(用于确认当前段中收到的数据)和 SEG.WND(调整发送方的流量控制)。

Variable Description
SEG.SEQ segment sequence number
SEG.ACK segment acknowledgment number
SEG.LEN segment length (exclude TCP header)
SEG.WND segment window
SEG.UP segment urgent pointer

通过段变量,你就能知道当前段在缓冲区的哪个位置。

Segment Length Receive Window Test
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
>0 0 not acceptable
>0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
or
RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND

3.2.2 TCP State Machine

除了专门的维护连接状态的结构体 TCB 之外,最能体现 TCP 面向连接的就莫过于 TCP 的一众连接状态了,我们有:LISTEN, SYN-SENT, SYN-RECEIVED, ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAITCLOSED 状态。因为连接状态记录在 TCB 中,而 CLOSED 代表连接完全关闭的状态(TCB 已释放),所以 CLOSED 是一个虚构的状态。

下面是这些状态及其对应的的简要说明:

Connection State Description
LISTEN The server is waiting for incoming connection requests from any remote TCP peer.
SYN-SENT The client has sent a SYN request to initiate a connection and is waiting for a response.
SYN-RECEIVED The server has received a SYN request and responded with a SYN-ACK, waiting for final confirmation.
ESTABLISHED The connection is fully open, and data can be exchanged between the client and server.
FIN-WAIT-1 The active side has sent a FIN request to terminate the connection and is waiting for an acknowledgment.
FIN-WAIT-2 The active side has received an acknowledgment for its FIN request and is waiting for the other side to send its own FIN.
CLOSE-WAIT The passive side has received a FIN request and is waiting for the application to close the connection.
CLOSING Both sides have sent FIN requests simultaneously and are waiting for acknowledgments.
LAST-ACK The passive side has sent a FIN request and is waiting for an acknowledgment before closing the connection.
TIME-WAIT The active side has closed the connection and is waiting to ensure the other side received the final acknowledgment.
CLOSED No connection exists; the Transmission Control Block (TCB) has been removed.

TCP 的连接状态有限,而且 TCP 连接会根据事件从一个状态转换到另一个状态。所以我们说 TCP 是一个有限状态机。下面给出了简要的 TCP 状态机模型(不全面):

                            +---------+ ----------------\     active OPEN
                            |  CLOSED |                  \    -----------
                            +---------+ <------------\    \   create TCB
                              |     ^                 \    \  snd SYN
                 passive OPEN |     |   CLOSE          \    \
                 ------------ |     | ----------        \    \
                  create TCB  |     | delete TCB         \    \
                              V     |                     \    \
          rcv RST (note 1)  +---------+            CLOSE   \    \  
       -------------------->|  LISTEN |          ---------- |    |
      /                     +---------+          delete TCB |    | 
     /           rcv SYN      |     |     SEND              |    |
    /           -----------   |     |    -------            |    V
+--------+      snd SYN,ACK  /       \   snd SYN          +--------+
|        |<-----------------           ------------------>|        |
|  SYN   |                    rcv SYN                     |  SYN   |
|  RCVD  |<-----------------------------------------------|  SENT  |
|        |                  snd SYN,ACK                   |        |
|        |------------------           -------------------|        |
+--------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +--------+
   |         --------------   |     |   -----------
   |                x         |     |     snd ACK
   |                          V     V
   |  CLOSE                 +---------+
   | -------                |  ESTAB  |
   | snd FIN                +---------+
   |                 CLOSE    |     |    rcv FIN
   V                -------   |     |    -------
+---------+         snd FIN  /       \   snd ACK         +---------+
|  FIN    |<----------------          ------------------>|  CLOSE  |
| WAIT-1  |------------------                            |   WAIT  |
+---------+          rcv FIN  \                          +---------+
  | rcv ACK of FIN   -------   |                          CLOSE  |
  | --------------   snd ACK   |                         ------- |
  V        x                   V                         snd FIN V
+---------+               +---------+                    +----------+
|FINWAIT-2|               | CLOSING |                    | LAST-ACK |
+---------+               +---------+                    +----------+
  |              rcv ACK of FIN |                 rcv ACK of FIN |
  |  rcv FIN     -------------- |    Timeout=2MSL -------------- |
  |  -------            x       V    ------------        x       V
   \ snd ACK              +---------+ delete TCB         +----------+
     -------------------->|TIME-WAIT|------------------->|  CLOSED  |
                          +---------+                    +----------+

TCP 向用户暴露的 API 则要简单得多。用户不会在意连接如何建立、数据如何传输、连接如何关闭。所以在 socket API 中,用户可以使用 connect()send()recv()close() 函数与 TCP 进行交互。

3.3 Sequence Number

3.3.1 ISN and Connection Reuse

TCP 的连接由一对 socket 定义,连接是可以被复用的。每一次新的连接实例被称为该连接的不同代 (incarnation)。既然连接可以被复用,那就不可避免的会产生 TCP 如何识别到来的数据是之前连接代的数据还是新连接的数据?

为了应对这种情况,TCP 引入了初始序列号 (ISN)TIME-WAIT 状态。TIME-WAIT 状态限制了连接的复用速率,确保旧连接的数据不会影响新连接。在连接建立时,协议栈中的初始序列号生成器会随机生成一个初始序列号来进一步防止数据包与之前连接实例的混淆。同时也增添了安全性。TCP 规定的 ISN 计算方式如下:其中 M 是一个 32 位计数器,用于确保新的连接不会与旧连接冲突。这个计数器每 4ms 递增一次,需要 4.5h 的周期循环一遍(时钟驱动,避免 ISN 重复),这个值远远大于 TCP 的最大段生存时间。F() 是一个伪随机函数,用于确保初始序列号的随机性,确保安全。

3.3.2 Connection Establishing

当 ISN 生成之后,主机就会将其存储在 TCB 中的 ISS 字段中,随后便发送给 TCP peer。在 peer 收到之后,应当回发 ACK 报文表示收到数据,然后 peer 主机会生成一个 ISN 发给主机,主机会把 peer ISN 存放在 IRS 字段中并回发 ACK 报文表示收到。实际上这就是 TCP 连接建立的过程。

    1) A --> B  SYN my sequence number is X
    2) A <-- B  ACK your sequence number is X
    3) A <-- B  SYN my sequence number is Y
    4) A --> B  ACK your sequence number is Y

在这个过程中,TCP 的两个 peer 会同步对方的初始序列号 ISN。这是通过交换一个 "SYN" 段(即 synchronize 段)来实现的,这个特殊的数据段中的 SYN 控制位会被置为 1 来标识段类型。

所以 TCP 连接的建立过程就是交换 ISN 的过程。每次交换都需要发送 SYN 段并回发 ACK 段以示收到,所以完整的连接建立过程应当有四个步骤。因为第二步和第三步可以合在一起作为一个单独的报文回发,所以 TCP 连接的建立过程也叫三次握手 (Three-Way Handshake, 3WHS)。更详细的会在下面介绍。

    1) A --> B  SYN (ISN = X)
    2) A <-- B  SYN-ACK (ISN = Y, ACK = X+1)
    3) A --> B  ACK (ACK = Y+1)

3.3.3 TCP Quiet Time and Timestamp Option

RFC 793 规定了 TCP 数据段在网络中存活的最长时间 (MSL) 为 2 分钟(Linux 默认的 MSL 为 60 秒)。在状态机中,我们看到 TCP 连接关闭过程中(CLOSING -> TIME-WAIT -> CLOSED),主动关闭方必须等待 2*MSL 这么长的时间,为的是确保所有的旧数据都已消失,避免影响新连接。

在正常情况下,TCP 会跟踪即将发生的下一个序列号,以及最早等待确认的序列号,以避免在尚未收到确认之前使用重复的序列号。所以 TCP 采用一个非常大的序列号空间(4 GB)来避免使用重复序列号。在 2Mbps 的传输速率下,耗尽整个序列号空间需要 4.5 个小时,这么大的序列号空间很够用了。但是在传输速率为 1Gbps 或更大时,耗尽序列号空间仅需 34 秒甚至 0.3 秒(10Gbps)。

如此,TCP 的去重和排序算法就会失效。因而当主机由于某种原因导致连接状态丢失(关闭连接),那么为防止旧连接的延迟数据段干扰新连接,主机在重启后就必须等待静默时间 (quiet time) 后再创建连接(通常为 1 MSL)。这样就能完美的避免系统收到旧的数据段。

但通常情况下,无论是静默时间还是 TIME-WAIT 的 2*MSL 的等待时间,在实践中都不太会严格遵守。对于高速网络(1 Gbps 及以上),TCP 会采用时间戳选项 (Timestamp Option)防止序列号回绕 (PAWS) 机制,确保旧数据段不会干扰新连接。

3.4 Establishing and Closing a TCP Connection

3.5 TCP Segmentation

Stream-Oriented Data Transfer

TCP 是面向流传输的协议,相比之下,UDP 我们说是面向数据报(datagram) 传输的协议。在之前,我们提到消息边界,那么没有消息边界的流传输会给我们带来说明问题呢?

假如我们分别发送两次消息 Hello1Hello2,在接收端的 TCP 传输层缓冲区中,可能将这两次发送的数据合并为 Hello1Hello2 后再送给应用。而因为 UDP 头中维护一个 Length 字段,所以使用 UDP 我们反而不用担心这种粘包的问题。

这么做有什么好处么?流传输不会把数据分割为独立的数据包,这种设计可以让发送端自由决定如何组织发送数据,而接收端也可以以大小任意的块来读取数据,带来了更大的灵活性。而且因为没有消息边界, TCP 的流传输可以自然的适应各类场景。这就需要认为地定义消息边界。

Custom-Defined Message Bound

想要避免粘包或半包(一条 send() 消息被拆分成多次 recv())的问题,我们就需要通过在应用层定义协议来明确消息边界。常见的实现方法有:

  1. 固定长度法
    即发送方确保每条消息填充至固定的长度(如128 字节)。接收方每次读取 128 字节。
    优点:实现简单,解析高效。
    缺点:严重浪费网络带宽资源。(适用于包长固定的场景)
  2. 分隔符法
    使用特殊字符来标记消息结尾。接收方按照分隔符切分数据流。适用于文本协议。
  3. 长度前缀法
    在消息头部添加长度字段,明确后续的数据长度。但需要注意统一字节序。
  4. 使用提供消息边界的应用层协议(HTTP 等)。
Maximum Transmission Unit

无论是 TCP 还是 UDP,每一次最大传输的数据大小都受到 MSS 最大段大小的限制,而 MSS 又受到 MTU 最大传输单元的限制。这不难理解,因为网络中的数据是经过层层封装后以类似“包裹”一样来传输的。而链路层的帧最大受 MTU 大小的限制。由此,MSS 就等于 MTU 减去 TCP/IP 头部长度。

通常而言,MTU 大小是 1500 字节,因而 MSS 典型值就是1460字节 (TCP/IP) 或是 1472 (UDP/IP)。

Reliable, In-Ordered Data Transfer

现在,我们来学习一下 TCP 的可靠性和按序到达的数据传输是如何实现的。

TCP 的可靠性包括检查数据包(通过序列号)的丢失和错误检查(通过校验和),通过重传来提供可靠性

3.6 TCP Data Communication

Congestion Control

Flow Control

QUIC has no HOL, because it's stream parallelism

UDP vs. TCP

因为 UDP 不需要握手和对数据包进行排序,所以使用UDP时,应用上层能够更快的拿到数据,而不用等待。

  • ==Finer application-level control over what data is sent, and when. ==当传输层使用UDP,它可以将应用进程发送的报文封装后马上就发出去。而TCP的 congestion-control mechanism 一旦检测到有大的拥塞就会掐断传输,而且TCP的重传机制也使得TCP的即时性远远不如UDP。(TCP传输时的延迟)
  • No connection establishment.在传输数据前,TCP需要三次握手(three-way handshake)确认连接,UDP发送数据不需要握手。也因为TCP的这种握手延迟,Google的Chrome浏览器使用QUIC(Quick UDP Internet Connection)代替HTTP作为其传输层协议。(传播前的延迟)
  • No connection state.TCP维护通信两端上的连接状态,其中包括接收和发送缓冲区、拥塞控制参数、序列号和确认号等信息。服务器维护这些信息会消耗大量的资源,不适合高并发应用。而UDP恰恰相反,TCP的缺点反而是它的优点,不维护连接状态,传输速度快,适合高并发应用,但不保证数据传输的可靠性。
  • Small packet header overhead.TCP的段头有20个字节,作为对比,UDP只有8个。