TCP (Transmission Control Protocol),即传输控制协议,是 Internet 协议套件 (Internet Protocol Suite) 中的核心协议之一,位于传输层。它提供可靠的、面向连接的、基于字节流的全双工通信服务。TCP 协议确保了数据能够按序、无差错地从一个应用进程传输到另一个应用进程。

核心思想:在不可靠的 IP 层之上,通过一系列机制(如序号、确认、重传、流量控制、拥塞控制)构建一个高度可靠、有序的数据传输通道。


一、为什么需要 TCP?

在网络模型中,IP 协议(网络层)提供了尽力而为 (best-effort) 的数据报服务,它不保证数据包的到达、顺序或不重复。然而,大多数应用(如网页浏览、文件传输、电子邮件)都需要一个可靠的数据传输服务。TCP 正是为了弥补 IP 协议的这些不足而设计的,它在应用层和网络层之间提供了一个可靠的、虚拟的通信管道。

TCP 的主要职责包括:

  1. 可靠性:确保数据无损、无错地到达目的地。
  2. 有序性:确保数据包以正确的顺序交付给接收方。
  3. 流量控制:防止发送方发送数据过快,导致接收方缓冲区溢出。
  4. 拥塞控制:防止发送方发送数据过快,导致网络整体性能下降甚至崩溃。
  5. 面向连接:在数据传输前,发送方和接收方需要建立连接;传输结束后,需要释放连接。

二、TCP 报文段结构

TCP 报文段 (segment) 是 TCP 层进行数据传输的基本单位。每个 TCP 报文段都包含一个 TCP 头部和数据部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+-------------------------------------------------------------+
| Source Port (16 bits) | Destination Port (16 bits) |
+-------------------------------------------------------------+
| Sequence Number (32 bits) |
+-------------------------------------------------------------+
| Acknowledgment Number (32 bits) |
+-------------------------------------------------------------+
| Data Offset | Reserved | Flags (9 bits) | Window Size (16 bits) |
+-------------+----------+----------------+--------------------------+
| Checksum (16 bits) | Urgent Pointer (16 bits) |
+-------------------------------------------------------------+
| Options (variable, 0-320 bits) |
+-------------------------------------------------------------+
| Padding (for 32-bit alignment) |
+-------------------------------------------------------------+
| |
| Data (Payload) |
| |
+-------------------------------------------------------------+

2.1 头部字段详解

  1. 源端口号 (Source Port, 16 bits):发送方的端口号,用于标识发送数据的应用进程。
  2. 目的端口号 (Destination Port, 16 bits):接收方的端口号,用于标识接收数据的应用进程。
  3. 序号 (Sequence Number, 32 bits)
    • TCP 是面向字节流的,一个 TCP 连接传输的数据流的每个字节都编有序号。
    • 序号字段的值指的是本报文段所发送的数据的第一个字节的序号
    • 用于解决网络包乱序和重复问题。
  4. 确认号 (Acknowledgement Number, 32 bits)
    • 期望收到对方下一个报文段的第一个字节的序号。
    • 若确认号为 N,则表示发送方已成功接收到序号 N-1 之前的所有数据。
    • 只有当 ACK 标志位为 1 时,确认号字段才有效。
  5. 数据偏移 (Data Offset, 4 bits)
    • 表示 TCP 头部长度,以 4 字节为单位。
    • 最小值为 5 (无选项字段),最大值为 15 (即 15 * 4 = 60 字节)。
  6. 保留 (Reserved, 6 bits):保留为今后使用,目前必须置为 0。
  7. 标志位 (Flags, 6 bits)
    • URG (Urgent):紧急指针有效。表示此报文段包含紧急数据,应优先处理。
    • ACK (Acknowledgement):确认号字段有效。这是最常用的标志。
    • PSH (Push):推送功能。请求接收方立即将数据交付给应用层,而不必等待缓冲区满。
    • RST (Reset):复位连接。用于异常终止连接或拒绝非法的报文段。
    • SYN (Synchronize):同步序号。用于在建立连接时同步序号,即发起连接请求。
    • FIN (Finish):终止连接。用于释放一个连接。
  8. 窗口大小 (Window Size, 16 bits)
    • 发送方用于通知接收方,自己当前可接受的数据量 (字节数)
    • 用于实现流量控制,防止发送方发送速度过快导致接收方来不及处理。
  9. 校验和 (Checksum, 16 bits)
    • 由发送方计算,接收方验证。
    • 用于检测报文段在传输过程中是否出现差错。
    • 覆盖整个 TCP 报文段 (头部和数据)。
  10. 紧急指针 (Urgent Pointer, 16 bits)
    • 只有当 URG 标志位为 1 时才有效。
    • 指出紧急数据在报文段中的偏移量,配合序号字段指示紧急数据结束的位置。
  11. 选项 (Options, 可变)
    • 常见选项:最大报文段长度 (MSS)、窗口扩大因子 (Window Scale)、时间戳 (Timestamps)、选择性确认 (SACK) 等。
  12. 填充 (Padding, 可变):确保 TCP 头部长度是 4 字节的整数倍。

三、TCP 的核心机制

3.1 面向连接:三次握手建立连接 (Three-Way Handshake)

在数据传输之前,TCP 需要在客户端和服务器之间建立一个逻辑连接。这个过程通过三次握手完成。

步骤详解:

  1. SYN (同步):客户端发送一个 SYN 报文段,包含一个随机生成的初始序号 (ISN) x。客户端进入 SYN_SENT 状态。
  2. SYN-ACK (同步-确认):服务器收到 SYN 后,发送一个 SYN-ACK 报文段。其中包含服务器的 ISN y,同时 ACK 字段置 1,确认号x+1 (确认已收到客户端的 SYN)。服务器进入 SYN_RCVD 状态。
  3. ACK (确认):客户端收到 SYN-ACK 后,发送一个 ACK 报文段。其中 ACK 字段置 1,确认号y+1 (确认已收到服务器的 SYN)。客户端和服务器都进入 ESTABLISHED 状态,连接建立成功,可以开始数据传输。

为什么是三次握手而不是两次?
主要是为了防止历史连接请求(旧的重复连接请求报文段)干扰。如果只有两次握手,客户端发送的连接请求在网络中滞留,服务器收到后会回复 ACK。如果客户端在等待 ACK 时超时重发了 SYN,并且之前的 SYN 报文段在网络中游荡了一段时间后也到达服务器,服务器会误认为客户端要建立新的连接,发送 ACK。但此时客户端可能已经关闭了连接,或者正在与另一个服务器通信。这会导致服务器单方面建立一个实际上不存在的连接,浪费资源。三次握手可以确保客户端和服务器都明确对方能够正常收发数据,避免了这种“死锁”情况。

Golang 示例:简单 TCP 客户端和服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// server.go
package main

import (
"fmt"
"net"
"time"
)

func handleConnection(conn net.Conn) {
defer conn.Close()
fmt.Printf("处理新的客户端连接:%s\n", conn.RemoteAddr().String())

// 模拟数据接收
buffer := make([]byte, 1024)
for {
conn.SetReadDeadline(time.Now().Add(5 * time.Second)) // 设置读取超时
n, err := conn.Read(buffer)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
fmt.Printf("读取超时,客户端可能已关闭连接:%s\n", conn.RemoteAddr().String())
break
}
fmt.Printf("读取错误:%s, 连接关闭:%s\n", err.Error(), conn.RemoteAddr().String())
break
}
if n == 0 { // 客户端关闭连接
fmt.Printf("客户端已关闭连接:%s\n", conn.RemoteAddr().String())
break
}
received := string(buffer[:n])
fmt.Printf("从 %s 接收到:%s\n", conn.RemoteAddr().String(), received)

// 模拟数据发送
response := fmt.Sprintf("服务器收到您的消息:%s", received)
_, err = conn.Write([]byte(response))
if err != nil {
fmt.Printf("写入错误:%s\n", err.Error())
break
}
}
fmt.Printf("连接处理结束:%s\n", conn.RemoteAddr().String())
}

func main() {
listenAddr := "localhost:8080"
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
fmt.Printf("无法监听:%s\n", err.Error())
return
}
defer listener.Close()
fmt.Printf("TCP 服务器正在监听:%s\n", listenAddr)

for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("接受连接错误:%s\n", err.Error())
continue
}
go handleConnection(conn) // 为每个新连接启动一个 goroutine
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// client.go
package main

import (
"bufio"
"fmt"
"net"
"os"
"strings"
"time"
)

func main() {
serverAddr := "localhost:8080"
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
fmt.Printf("连接服务器失败:%s\n", err.Error())
return
}
defer conn.Close()
fmt.Printf("成功连接到服务器:%s\n", serverAddr)

reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("请输入消息 ('exit' 退出): ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)

if input == "exit" {
fmt.Println("正在关闭连接...")
break
}

// 发送数据
_, err = conn.Write([]byte(input))
if err != nil {
fmt.Printf("发送数据失败:%s\n", err.Error())
break
}

// 读取服务器响应
conn.SetReadDeadline(time.Now().Add(5 * time.Second)) // 设置读取超时
buffer := make([]byte, 1024)
n, err := conn.Read(buffer)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
fmt.Println("读取服务器响应超时")
} else {
fmt.Printf("读取服务器响应失败:%s\n", err.Error())
}
break
}
fmt.Printf("收到服务器响应:%s\n", string(buffer[:n]))
}
fmt.Println("客户端程序退出。")
}

3.2 可靠数据传输:序号、确认与重传

TCP 通过以下机制确保数据可靠传输:

  1. 序号 (Sequence Number)
    • TCP 为每个发送的字节分配一个序号。报文段中的序号是该报文段第一个数据字节的序号。
    • 接收方使用序号来识别重复的数据包,并按照正确的顺序重新组装乱序的数据包。
  2. 确认 (Acknowledgement - ACK)
    • 接收方成功收到数据后,会发送一个确认报文段 (ACK 标志位为 1),其中包含期望接收的下一个字节的序号(即已收到数据最后一个字节的序号加 1)。这被称为累积确认 (Cumulative ACK)
    • 例如,如果收到的 ACK 号为 N,表示 N-1 及其之前的所有数据都已成功接收。
  3. 重传 (Retransmission)
    • 超时重传 (Timeout Retransmission):发送方发送一个报文段后,会启动一个定时器 (RTO - Retransmission Timeout)。如果在定时器到期前没有收到对应的 ACK,发送方会认为该报文段丢失,并重传该报文段。RTO 是动态计算的,通常基于往返时间 (RTT - Round Trip Time)。
    • 快速重传 (Fast Retransmit):当发送方收到三个重复的 ACK (即收到了四次对同一个数据包的 ACK) 时,会立即重传丢失的报文段,而不需要等待 RTO 超时。这通常意味着网络中某个报文段丢失了,但后续的报文段到达了接收方。

3.3 流量控制 (Flow Control)

流量控制是为了防止发送方发送数据过快,导致接收方的缓冲区溢出。TCP 使用滑动窗口协议 (Sliding Window Protocol) 来实现流量控制。

  • 窗口大小 (Window Size):TCP 头部中的 16 位窗口字段,表示接收方当前可接收的字节数。
  • 接收方通告窗口 (Receiver Advertised Window - rwnd):接收方在 ACK 报文段中向发送方通告自己的接收缓冲区可用空间大小,即 rwnd。发送方被限制只能发送不超过 rwnd 字节的数据。
  • 零窗口探测 (Zero Window Probe):如果接收方通告的窗口大小为 0,发送方会停止发送数据。为了防止接收方窗口一直为 0 导致死锁,发送方会周期性地发送零窗口探测报文段,询问接收方窗口是否已打开。

3.4 拥塞控制 (Congestion Control)

拥塞控制是为了防止过多的数据注入到网络中,导致网络过载,从而降低吞吐量甚至造成网络崩溃。拥塞控制是全局性的,关注整个网络的承载能力;流量控制是端到端的,关注接收方的处理能力。

TCP 拥塞控制主要包括以下四个算法:

  1. 慢启动 (Slow Start)
    • 连接建立初期,为了避免立即向网络中发送大量数据导致拥塞,发送方会从一个很小的拥塞窗口 (Congestion Window - cwnd) 开始。
    • 初始 cwnd 通常为 1-10 MSS (Maximum Segment Size)。
    • 每当收到一个 ACK,cwnd 就会翻倍式增长 (指数增长)。
    • 直到 cwnd 达到慢启动阈值 (ssthresh)
    • 公式表示:cwnd = cwnd * 2 (每个 RTT)
  2. 拥塞避免 (Congestion Avoidance)
    • cwnd 达到 ssthresh 后,慢启动阶段结束,进入拥塞避免阶段。
    • 此时 cwnd 增长方式变为线性增长:每收到一个 ACK,cwnd 增加 1/cwnd 个 MSS。或者说,每个 RTT 周期 cwnd 增加 1 MSS。
    • 公式表示:cwnd = cwnd + MSS / cwnd (每个 ACK) 或 cwnd = cwnd + MSS (每个 RTT)
  3. 快速重传 (Fast Retransmit)
    • 前面提到,当发送方收到三个重复 ACK 时,立即重传丢失的报文段。
    • 这表明网络可能发生了轻微的拥塞,但并非严重到需要慢启动。
  4. 快速恢复 (Fast Recovery)
    • 与快速重传结合使用。当触发快速重传时(收到 3 个重复 ACK):
      • ssthresh 设置为当前 cwnd 的一半。
      • cwnd 设置为 ssthresh + 3 * MSS (因为收到了 3 个重复 ACK,每个 ACK 表示一个报文段离开了网络)。
      • 每收到一个重复 ACK,cwnd 增加 1 MSS。
      • 当收到新的 ACK (确认了重传的数据包),cwnd 设置为 ssthresh,进入拥塞避免阶段。
    • 如果发生超时事件(而不是 3 个重复 ACK),则认为拥塞更严重:
      • ssthresh 设置为 cwnd 的一半。
      • cwnd 设置为 1 MSS。
      • 重新进入慢启动阶段。

这些算法的协同工作,使得 TCP 能够在网络拥塞时自动调整发送速率,从而维护网络的稳定性和效率。

3.5 全双工通信

TCP 连接是全双工的,意味着数据可以在两个方向上同时传输。每个方向都拥有独立的发送和接收缓冲区。

3.6 面向字节流

TCP 不关心应用层发送的数据块边界,它将所有数据视为一个无结构的字节流。当应用进程向 TCP 传输数据时,TCP 会将数据切分成合适的报文段大小 (通常受限于 MSS),然后向下传递给 IP 层。接收方 TCP 收到数据后,将其放入接收缓冲区,应用进程可以以任意大小读取这些字节。

四、TCP 连接的释放:四次挥手 (Four-Way Handshake)

当数据传输完成,双方需要关闭 TCP 连接,这个过程称为四次挥手。

步骤详解:

  1. FIN (终止):客户端应用进程通知 TCP 准备关闭连接。客户端发送一个 FIN 报文段,包含一个序号 u。客户端进入 FIN_WAIT_1 状态。
  2. ACK (确认):服务器收到 FIN 后,发送一个 ACK 报文段,确认号u+1。服务器进入 CLOSE_WAIT 状态。此时,服务器仍可以向客户端发送数据(半关闭状态)。
  3. FIN (终止):当服务器也没有数据要发送时,服务器应用进程也通知 TCP 准备关闭连接。服务器发送一个 FIN 报文段,包含一个序号 v。服务器进入 LAST_ACK 状态。
  4. ACK (确认):客户端收到服务器的 FIN 后,发送一个 ACK 报文段,确认号v+1。客户端进入 TIME_WAIT 状态。
    • 客户端在 TIME_WAIT 状态会等待 2MSL (Max Segment Lifetime - 最大报文段寿命) 的时间,以确保服务器收到了最后的 ACK 报文段,并处理网络中可能存在的延迟或重传的报文段。
    • 2MSL 之后,客户端才真正关闭连接。
    • 服务器收到最后的 ACK 报文段后,立即关闭连接。

为什么是四次挥手而不是三次?
因为 TCP 是全双工的,每个方向的传输都需要独立关闭。客户端发送 FIN 只是表示它没有数据要发送了,但服务器可能还有数据要发送给客户端。因此,服务器会先回复一个 ACK (表示收到客户端的关闭请求),然后等待自己发送完所有数据后,再发送 FIN 请求关闭自己的发送方向。所以,关闭通常需要两个 FIN 和两个 ACK,共四次挥手。

五、TCP 与 UDP 对比

特性 TCP (传输控制协议) UDP (用户数据报协议)
连接类型 面向连接 (Connection-Oriented) 无连接 (Connectionless)
可靠性 可靠 (Reliable):有确认、重传、序号 不可靠 (Unreliable):无确认、无重传
有序性 有序 (Ordered):保证数据按序到达 无序 (Unordered):数据可能乱序到达
数据边界 面向字节流 (Byte Stream):无消息边界 面向数据报 (Datagram-Oriented):保留消息边界
流量控制 有 (使用滑动窗口)
拥塞控制 有 (慢启动、拥塞避免、快速重传、快速恢复)
头部开销 较大 (至少 20 字节) 较小 (8 字节)
速度 较慢 (因可靠性机制和连接建立/关闭开销) 较快 (传输效率高)
适用场景 文件传输、网页浏览 (HTTP/HTTPS)、电子邮件 (SMTP/POP3) 实时应用 (视频会议、VoIP)、DNS 查询、网络管理 (SNMP)

六、安全性考虑

尽管 TCP 自身提供了可靠性,但它并非天生安全。一些常见的攻击包括:

  1. SYN Flood 攻击:攻击者发送大量伪造源 IP 的 SYN 报文段,使得服务器创建大量半开连接,耗尽资源。
  2. TCP RST 攻击:攻击者伪造 RST 报文段,强制终止正常的 TCP 连接。
  3. 会话劫持 (Session Hijacking):攻击者通过窃听 TCP 序号和确认号,伪装成合法用户,劫持现有连接。
  4. IP Spoofing (IP 欺骗):攻击者伪造源 IP 地址,但 TCP 的三次握手和序号机制使得伪造完整的连接相对困难。

为了增强 TCP 连接的安全性,通常会在应用层或传输层之上使用加密协议,如 TLS/SSL (Transport Layer Security / Secure Sockets Layer),它提供数据加密、身份认证和消息完整性验证。

七、总结

TCP 协议是互联网的基石之一,其复杂而精巧的机制在不可靠的网络环境中构建了可靠、高效的通信服务。它通过序号、确认、重传确保数据无错有序;通过滑动窗口实现流量控制;通过慢启动、拥塞避免等算法实现拥塞控制。理解 TCP 的工作原理对于网络编程、性能优化以及问题排查至关重要。