眉清目秀原来是平铺直叙
TCP 协议:从握手到数据传输
序章:TCP 是什么?—— 网络世界的"可靠快递"
tcp是可靠的传输协议,结合日常生活把这些概念套进去,就想象自己如果碰到这样的问题会如何处理,这样更易理解,不用死记硬背。 想象你要寄一份重要合同给朋友:
- UDP 像普通明信片:便宜、快,但可能丢失、乱序、重复
- TCP 像顺丰快递:贵一点、慢一点,但保证送达、按顺序、不重复
TCP(Transmission Control Protocol,传输控制协议) 是互联网最核心的协议之一,位于传输层,提供面向连接、可靠、基于字节流的通信服务。
graph TD
A[应用层<br/>HTTP/FTP/DNS] --> B[传输层<br/>TCP/UDP]
B --> C[网络层<br/>IP]
C --> D[数据链路层<br/>以太网]
D --> E[物理层<br/>光纤/电缆]
style B fill:#90EE90
第一章:TCP 特点 —— 五大核心特性
mindmap
root((TCP<br/>五大特点))
面向连接
通信前先建立连接
像打电话要拨号
可靠传输
不丢包
不错乱
不重复
全双工通信
双方可同时发送和接收
像双向车道
字节流服务
不保留消息边界
像水流连续不断
点对点
一对一通信
不支持广播
1.1 面向连接 —— 先打电话,再说话
sequenceDiagram
participant A as 客户端
participant B as 服务端
Note over A,B: UDP:直接喊话<br/>"喂!有人吗?"
A->>B: TCP:先建立连接
B->>A: 三次握手完成
Note over A,B: 现在可以可靠通信了
A->>B: 发送数据
B->>A: 确认收到
1.2 可靠传输 —— 签收确认机制
TCP 通过四种机制保证可靠性:
- 校验和:检查数据是否损坏
- 确认应答 ACK:收到后回复"收到了"
- 超时重传:没收到确认就重发
- 序号机制:保证数据按序到达
1.3 全双工通信 —— 双向车道
graph LR
A[客户端] -->|发送数据| B[服务端]
B -->|同时发送| A
subgraph "全双工"
C[发送通道]
D[接收通道]
end
style C fill:#90EE90
style D fill:#90EE90
1.4 字节流服务 —— 像水流一样
// TCP 不保留消息边界
// 发送方分两次发送:"Hello" 和 "World"
conn.Write([]byte("Hello"))
conn.Write([]byte("World"))
// 接收方可能收到:
// 情况1: "HelloWorld"(粘包)
// 情况2: "Hel" + "loWorld"(拆包)
// 情况3: "Hello" + "World"(刚好)
// 解决方案:定义消息边界
// 方式1:固定长度
// 方式2:特殊分隔符(如 \n)
// 方式3:长度前缀(推荐)
1.5 点对点 —— 一对一专属
TCP 连接是一对一的,每个连接由四元组唯一标识:
(源IP, 源端口, 目的IP, 目的端口)
第二章:三次握手 —— 建立连接的"三步曲"
2.1 为什么需要三次握手?
故事类比:两个人在嘈杂的酒吧里确认对方能听到自己说话。
sequenceDiagram
participant Client as 客户端(A)
participant Server as 服务端(B)
Note over Client,Server: 第一次握手:A 确认自己能发,B 确认自己能收
Client->>Server: SYN=1, seq=x<br/>(我能听到你吗?我的序号是x)
Note over Client,Server: 第二次握手:B 确认自己能发能收,A 确认B能收发
Server->>Client: SYN=1, ACK=1, seq=y, ack=x+1<br/>(我能听到!我的序号是y,期待你的x+1)
Note over Client,Server: 第三次握手:A 确认自己能收,B 确认A能发
Client->>Server: ACK=1, seq=x+1, ack=y+1<br/>(我也能听到!给你x+1,期待你的y+1)
Note over Client,Server: 连接建立!双方确认:我能发,你能收;你能发,我能收
2.2 详细解析每个字段
| 握手次数 | 发送方 | 关键标志 | 含义 |
|---|---|---|---|
| 第一次 | 客户端 | SYN=1, seq=x | 请求同步,携带初始序号 x |
| 第二次 | 服务端 | SYN=1, ACK=1, seq=y, ack=x+1 | 同意同步,确认收到,携带自己的序号 y |
| 第三次 | 客户端 | ACK=1, seq=x+1, ack=y+1 | 确认收到,序号+1,准备传输数据 |
2.3 为什么不能两次握手?
Go 代码示例:TCP 连接建立
package main
import (
"fmt"
"net"
)
func main() {
// 服务端
go func() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("服务端:等待连接...")
conn, err := listener.Accept()
if err != nil {
panic(err)
}
fmt.Printf("服务端:连接建立!远端地址 %s\n", conn.RemoteAddr())
// 读取数据
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Printf("服务端收到:%s\n", string(buf[:n]))
conn.Write([]byte("Hello Client!"))
conn.Close()
}()
// 客户端
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
panic(err)
}
defer conn.Close()
fmt.Printf("客户端:连接建立!本地 %s -> 远端 %s\n",
conn.LocalAddr(), conn.RemoteAddr())
conn.Write([]byte("Hello Server!"))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Printf("客户端收到:%s\n", string(buf[:n]))
}
输出:
服务端:等待连接...
客户端:连接建立!本地 127.0.0.1:54321 -> 远端 127.0.0.1:8080
服务端:连接建立!远端地址 127.0.0.1:54321
服务端收到:Hello Server!
客户端收到:Hello Server!
第三章:四次挥手 —— 优雅分手的"四步曲"
3.1 为什么需要四次挥手?
故事类比:情侣分手,双方都要确认"我说完了"和"我听完了"。
sequenceDiagram
participant Client as 客户端(A)
participant Server as 服务端(B)
Note over Client,Server: A 想关闭连接,但 B 可能还有数据要发
Client->>Server: FIN=1, seq=u<br/>(A:我说完了,要关闭发送)
Server->>Client: ACK=1, seq=v, ack=u+1<br/>(B:知道了,但我还没说完)
Note over Client,Server: B 继续发送剩余数据...
Server->>Client: FIN=1, seq=w, ack=u+1<br/>(B:我也说完了,要关闭发送)
Client->>Server: ACK=1, seq=u+1, ack=w+1<br/>(A:知道了,等待2MSL后关闭)
Note over Client,Server: 连接完全关闭
3.2 详细解析
| 挥手次数 | 发送方 | 关键标志 | 状态变化 |
|---|---|---|---|
| 第一次 | 客户端 | FIN=1, seq=u | 客户端:FIN_WAIT_1 |
| 第二次 | 服务端 | ACK=1, seq=v, ack=u+1 | 服务端:CLOSE_WAIT;客户端:FIN_WAIT_2 |
| 第三次 | 服务端 | FIN=1, seq=w, ack=u+1 | 服务端:LAST_ACK |
| 第四次 | 客户端 | ACK=1, seq=u+1, ack=w+1 | 客户端:TIME_WAIT(等待2MSL后关闭) |
3.3 为什么客户端最后要等待 2MSL?
TCP 状态转换全景图:
stateDiagram-v2
[*] --> CLOSED
CLOSED --> SYN_SENT : 主动打开<br/>发送SYN
CLOSED --> LISTEN : 被动打开
LISTEN --> SYN_RCVD : 收到SYN<br/>发送SYN+ACK
SYN_SENT --> ESTABLISHED : 收到SYN+ACK<br/>发送ACK
SYN_RCVD --> ESTABLISHED : 收到ACK
ESTABLISHED --> FIN_WAIT_1 : 主动关闭<br/>发送FIN
ESTABLISHED --> CLOSE_WAIT : 收到FIN<br/>发送ACK
FIN_WAIT_1 --> FIN_WAIT_2 : 收到ACK
FIN_WAIT_1 --> TIME_WAIT : 收到FIN+ACK<br/>发送ACK(同时关闭)
FIN_WAIT_2 --> TIME_WAIT : 收到FIN<br/>发送ACK
CLOSE_WAIT --> LAST_ACK : 发送FIN
LAST_ACK --> CLOSED : 收到ACK
TIME_WAIT --> CLOSED : 等待2MSL
note right of TIME_WAIT
为什么TIME_WAIT?
1. 确保ACK到达
2. 防止旧报文干扰
end note
第四章:可靠传输 —— 四大保障机制
graph TD
A[TCP可靠传输] --> B[校验和]
A --> C[确认应答ACK]
A --> D[超时重传]
A --> E[序号机制]
A --> F[去重]
style A fill:#E1F5FE
4.1 校验和 —— 检查数据是否损坏
graph LR
A[发送方] -->|数据 + 校验和| B[网络传输]
B --> C[接收方]
C --> D{校验和匹配?}
D -->|是| E[数据正常<br/>回复ACK]
D -->|否| F[数据损坏<br/>丢弃不回复<br/>触发重传]
style E fill:#90EE90
style F fill:#FFB6C1
计算方式:将数据分成16位字,求和,取反码。
// 简化的校验和计算(示意)
func checksum(data []byte) uint16 {
var sum uint32
// 按16位字求和
for i := 0; i < len(data)-1; i += 2 {
sum += uint32(data[i])<<8 + uint32(data[i+1])
}
// 奇数字节
if len(data)%2 == 1 {
sum += uint32(data[len(data)-1]) << 8
}
// 回卷(将进位加回低位)
for (sum >> 16) > 0 {
sum = (sum & 0xFFFF) + (sum >> 16)
}
return uint16(^sum) // 取反
}
4.2 确认应答 ACK —— "我收到了"
sequenceDiagram
participant Sender as 发送方
participant Receiver as 接收方
Sender->>Receiver: 发送数据 [seq=100, len=50]
Note over Receiver: 收到字节100-149
Receiver->>Sender: ACK=1, ack=150<br/>(期待下一个字节150)
Sender->>Receiver: 发送数据 [seq=150, len=50]
Receiver->>Sender: ACK=1, ack=200
Note over Sender,Receiver: 累积确认:ack=200<br/>表示200之前的全部收到
累积确认的优势:
- ACK=200 表示 0-199 全部收到
- 即使 ACK=150 丢了,收到 ACK=200 就知道 150 也收到了
4.3 超时重传 —— 没收到确认就重发
graph TD
A[发送数据] --> B[启动定时器]
B --> C{收到ACK?}
C -->|是| D[取消定时器<br/>发送下一个]
C -->|否| E{超时?}
E -->|否| C
E -->|是| F[重传数据<br/>重启定时器]
F --> C
style D fill:#90EE90
style F fill:#FFE4B5
超时时间计算(RTO):
// 自适应超时算法(Jacobson/Karels算法)
type RTTEstimator struct {
srtt float64 // 平滑往返时间
rttvar float64 // 往返时间变化
rto float64 // 重传超时
}
func (e *RTTEstimator) Update(measuredRTT float64) {
const alpha = 0.875
const beta = 0.25
if e.srtt == 0 {
// 首次测量
e.srtt = measuredRTT
e.rttvar = measuredRTT / 2
} else {
// 更新平滑RTT
e.rttvar = (1-beta)*e.rttvar + beta*abs(e.srtt-measuredRTT)
e.srtt = (1-alpha)*e.srtt + alpha*measuredRTT
}
// RTO = SRTT + 4 * RTTVAR
e.rto = e.srtt + 4*e.rttvar
// 边界保护
if e.rto < 1 {
e.rto = 1
}
if e.rto > 60 {
e.rto = 60 // 最大60秒
}
}
4.4 序号机制 —— 保证顺序
graph LR
A[发送方发送] --> B[seq=100<br/>seq=200<br/>seq=150]
B --> C[网络乱序到达]
C --> D[接收方缓存]
D --> E[按序号排序]
E --> F[seq=100<br/>seq=150<br/>seq=200]
F --> G[交付应用层]
style B fill:#FFB6C1
style F fill:#90EE90
4.5 去重 —— 丢弃重复数据
graph TD
A[收到数据包] --> B{序号在接收窗口内?}
B -->|否| C[丢弃]
B -->|是| D{已收到?}
D -->|是| E[丢弃重复]
D -->|否| F[接收并缓存]
style C fill:#FFB6C1
style E fill:#FFB6C1
style F fill:#90EE90
第五章:流量控制 —— 防止"撑死"接收方
5.1 滑动窗口 —— 动态调节发送速度
graph LR
subgraph "发送窗口"
A1[已发送<br/>已确认] --> B1[已发送<br/>未确认]
B1 --> C1[允许发送<br/>未发送]
C1 --> D1[不允许发送]
end
subgraph "窗口滑动"
E1[收到ACK<br/>窗口右移] --> F1[可发送新数据]
end
style B1 fill:#FFE4B5
style C1 fill:#90EE90
5.2 接收方通告窗口 rwnd
sequenceDiagram
participant Sender as 发送方
participant Receiver as 接收方
Note over Receiver: 接收缓存:总大小 1000<br/>已用 300,剩余 700
Receiver->>Sender: ACK=1, ack=501, rwnd=700<br/>(我还能收700字节)
Sender->>Receiver: 发送 700 字节
Note over Receiver: 应用层读取 200 字节<br/>缓存剩余 400
Receiver->>Sender: ACK=1, ack=1201, rwnd=400<br/>(我还能收400字节)
Sender->>Receiver: 发送 400 字节
Note over Receiver: 应用层读取 500 字节<br/>缓存剩余 500
Receiver->>Sender: ACK=1, ack=1601, rwnd=500<br/>(窗口更新了!)
Go 示例:设置接收窗口
package main
import (
"fmt"
"net"
"syscall"
)
func main() {
listener, _ := net.Listen("tcp", ":8080")
conn, _ := listener.Accept()
// 获取底层 TCP 连接
tcpConn := conn.(*net.TCPConn)
// 设置接收缓冲区大小(影响窗口)
tcpConn.SetReadBuffer(65536) // 64KB
// 获取实际窗口大小
file, _ := tcpConn.File()
fd := file.Fd()
// 使用 syscall 获取 socket 选项
rcvbuf, _ := syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF)
sndbuf, _ := syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUF)
fmt.Printf("接收缓冲区: %d bytes\n", rcvbuf)
fmt.Printf("发送缓冲区: %d bytes\n", sndbuf)
}
5.3 零窗口与窗口探测
sequenceDiagram
participant Sender as 发送方
participant Receiver as 接收方
Receiver->>Sender: ACK, rwnd=0<br/>(我满了,别发了!)
Note over Sender: 停止发送,启动持续定时器
Sender->>Receiver: 窗口探测报文<br/>(你还满吗?)
Receiver->>Sender: ACK, rwnd=0<br/>(还是满的...)
Note over Sender: 持续定时器翻倍
Sender->>Receiver: 窗口探测报文
Note over Receiver: 应用层读取数据<br/>缓存有空间了
Receiver->>Sender: ACK, rwnd=1000<br/>(可以发了!)
Sender->>Receiver: 发送数据
第六章:拥塞控制 —— 防止"撑死"网络
流量控制:防止发送方过快压垮接收方 拥塞控制:防止发送方过快压垮网络
graph TD
A[拥塞控制] --> B[慢启动 Slow Start]
A --> C[拥塞避免 Congestion Avoidance]
A --> D[快重传 Fast Retransmit]
A --> E[快恢复 Fast Recovery]
style A fill:#FFEBEE
6.1 慢启动 —— 试探网络容量
graph LR
A["初始 cwnd=1 MSS"] --> B["每收到一个ACK<br/>cwnd += 1"]
B --> C["RTT 1: cwnd=1<br/>发1个"]
C --> D["RTT 2: cwnd=2<br/>发2个"]
D --> E["RTT 3: cwnd=4<br/>发4个"]
E --> F["RTT 4: cwnd=8<br/>发8个"]
F --> G["指数增长!<br/>直到阈值"]
style G fill:#90EE90
6.2 拥塞避免 —— 线性增长
graph LR
A[cwnd > ssthresh] --> B[每轮RTT<br/>cwnd += 1]
B --> C[线性增长<br/>更保守]
C --> D[直到拥塞]
style B fill:#FFE4B5
6.3 拥塞发生时的处理
graph TD
A[检测到拥塞] --> B{拥塞信号?}
B -->|超时| C[ssthresh = cwnd/2<br/>cwnd = 1<br/>慢启动]
B -->|3个重复ACK| D[ssthresh = cwnd/2<br/>cwnd = ssthresh<br/>快恢复]
style C fill:#FFB6C1
style D fill:#90EE90
6.4 快重传 —— 不等超时
sequenceDiagram
participant Sender as 发送方
participant Receiver as 接收方
Sender->>Receiver: seq=100
Sender->>Receiver: seq=200
Sender->>Receiver: seq=300(丢失!)
Sender->>Receiver: seq=400
Receiver->>Sender: ACK=300(重复,期待300)
Receiver->>Sender: ACK=300(重复)
Receiver->>Sender: ACK=300(重复!)
Note over Sender: 收到3个重复ACK<br/>不等超时,立即重传300
Sender->>Receiver: seq=300(快重传)
Receiver->>Sender: ACK=500(累积确认)
Note over Sender,Receiver: 快重传节省了<br/>等待超时的时间!
6.5 快恢复 —— 不降到1
graph LR
A[快重传后] --> B["cwnd = ssthresh<br/>不是1!"]
B --> C[拥塞避免<br/>线性增长]
C --> D[更快恢复<br/>传输速度]
style B fill:#90EE90
完整拥塞控制状态机:
stateDiagram-v2
[*] --> SlowStart : 连接建立/超时恢复
SlowStart --> CongestionAvoidance : cwnd >= ssthresh
SlowStart --> SlowStart : 每ACK<br/>cwnd *= 2
CongestionAvoidance --> CongestionAvoidance : 每RTT<br/>cwnd += 1
CongestionAvoidance --> FastRecovery : 3个重复ACK
FastRecovery --> CongestionAvoidance : 新ACK确认<br/>丢失包之后的数据
FastRecovery --> SlowStart : 超时
CongestionAvoidance --> SlowStart : 超时<br/>ssthresh=cwnd/2<br/>cwnd=1
note right of SlowStart
慢启动:指数增长
探测网络容量
end note
note right of CongestionAvoidance
拥塞避免:线性增长
谨慎利用带宽
end note
note right of FastRecovery
快恢复:不降到1
快速恢复传输
end note
Go 示例:查看 TCP 拥塞控制参数
package main
import (
"fmt"
"os/exec"
)
func main() {
// Linux 下查看当前拥塞控制算法
out, _ := exec.Command("sysctl", "net.ipv4.tcp_congestion_control").Output()
fmt.Printf("当前拥塞控制算法: %s", out)
// 输出: net.ipv4.tcp_congestion_control = cubic
// 查看可用的拥塞控制算法
out, _ = exec.Command("sysctl", "net.ipv4.tcp_available_congestion_control").Output()
fmt.Printf("可用算法: %s", out)
// 输出: reno cubic bbr ...
// 查看 TCP 连接状态
out, _ = exec.Command("ss", "-ti").Output()
fmt.Printf("TCP连接详情:\n%s", out)
// 输出包含: cwnd, ssthresh, rtt 等信息
}
TCP 完整生命周期
sequenceDiagram
participant App as 应用层
participant TCP as TCP层
participant IP as IP层
participant Net as 网络
Note over App,Net: === 连接建立 ===
App->>TCP: connect()
TCP->>TCP: 三次握手
TCP->>IP: SYN包
IP->>Net: 发送
Net-->>IP: SYN+ACK
IP-->>TCP:
TCP->>IP: ACK
IP->>Net: 发送
TCP-->>App: 连接建立!
Note over App,Net: === 数据传输 ===
App->>TCP: write(data)
TCP->>TCP: 分段 + 加序号 + 计算校验和
TCP->>TCP: 放入发送窗口
TCP->>IP: 数据包
IP->>Net: 发送
Net-->>IP: ACK(确认)
IP-->>TCP:
TCP->>TCP: 滑动窗口<br/>拥塞控制
Note over App,Net: === 连接关闭 ===
App->>TCP: close()
TCP->>TCP: 四次挥手
TCP->>IP: FIN包
IP->>Net: 发送
Net-->>IP: ACK
IP-->>TCP:
Net-->>IP: FIN
IP-->>TCP:
TCP->>IP: ACK
IP->>Net: 发送
TCP-->>App: 连接关闭
Note over TCP: TIME_WAIT<br/>等待2MSL
TCP vs UDP 对比总结
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接 | 无连接 |
| 可靠性 | 可靠(不丢、不重、有序) | 不可靠 |
| 传输方式 | 字节流 | 数据报 |
| 拥塞控制 | 有 | 无 |
| 流量控制 | 有 | 无 |
| 头部开销 | 20字节 | 8字节 |
| 速度 | 慢 | 快 |
| 适用场景 | 文件传输、HTTP、邮件 | 视频直播、DNS、游戏 |
graph LR
A[选择协议] --> B{需要可靠?}
B -->|是| C[TCP<br/>HTTP/FTP/SSH]
B -->|否| D{需要速度?}
D -->|是| E[UDP<br/>直播/游戏/DNS]
D -->|否| F[看场景]
style C fill:#90EE90
style E fill:#90EE90
面试高频
| 问题 | 答案要点 |
|---|---|
| 为什么三次握手? | 确认双方收发能力;防止历史连接 |
| 为什么四次挥手? | 全双工,双方各自关闭;被动方可能有数据要发 |
| TIME_WAIT作用? | 确保ACK到达;防止旧报文干扰新连接 |
| 滑动窗口原理? | 动态调整发送量,匹配接收方处理能力 |
| 拥塞控制四个算法? | 慢启动、拥塞避免、快重传、快恢复 |
| 流量控制 vs 拥塞控制? | 前者防接收方过载,后者防网络过载 |
tcp是可靠的传输协议,结合日常生活把这些概念套进去,就想象自己如果碰到这样的问题会如何处理,这样更易理解 TCP 是互联网的基石,理解它就像理解快递系统的运作原理——从下单(握手)、打包(分段)、运输(传输)、签收(确认)到退货(挥手),每个环节都精心设计,确保数据安全、有序、高效地到达目的地!