TCP从零单排

眉清目秀原来是平铺直叙

TCP 协议:从握手到数据传输

序章:TCP 是什么?------ 网络世界的"可靠快递"

tcp是可靠的传输协议,结合日常生活把这些概念套进去,就想象自己如果碰到这样的问题会如何处理,这样更易理解,不用死记硬背。 想象你要寄一份重要合同给朋友:

  • UDP 像普通明信片:便宜、快,但可能丢失、乱序、重复
  • TCP 像顺丰快递:贵一点、慢一点,但保证送达、按顺序、不重复

TCP(Transmission Control Protocol,传输控制协议) 是互联网最核心的协议之一,位于传输层,提供面向连接、可靠、基于字节流的通信服务。

graph TD A[应用层
HTTP/FTP/DNS] --> B[传输层
TCP/UDP] B --> C[网络层
IP] C --> D[数据链路层
以太网] D --> E[物理层
光纤/电缆] style B fill:#90EE90

第一章:TCP 特点 ------ 五大核心特性

mindmap root((TCP
五大特点)) 面向连接 通信前先建立连接 像打电话要拨号 可靠传输 不丢包 不错乱 不重复 全双工通信 双方可同时发送和接收 像双向车道 字节流服务 不保留消息边界 像水流连续不断 点对点 一对一通信 不支持广播

1.1 面向连接 ------ 先打电话,再说话

sequenceDiagram participant A as 客户端 participant B as 服务端 Note over A,B: UDP:直接喊话
"喂!有人吗?" 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 字节流服务 ------ 像水流一样

go 复制代码
// 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 连接是一对一的,每个连接由四元组唯一标识:

scss 复制代码
(源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
(我能听到你吗?我的序号是x) Note over Client,Server: 第二次握手:B 确认自己能发能收,A 确认B能收发 Server->>Client: SYN=1, ACK=1, seq=y, ack=x+1
(我能听到!我的序号是y,期待你的x+1) Note over Client,Server: 第三次握手:A 确认自己能收,B 确认A能发 Client->>Server: ACK=1, seq=x+1, ack=y+1
(我也能听到!给你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 连接建立

go 复制代码
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]))
}

输出:

arduino 复制代码
服务端:等待连接...
客户端:连接建立!本地 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
(A:我说完了,要关闭发送) Server->>Client: ACK=1, seq=v, ack=u+1
(B:知道了,但我还没说完) Note over Client,Server: B 继续发送剩余数据... Server->>Client: FIN=1, seq=w, ack=u+1
(B:我也说完了,要关闭发送) Client->>Server: ACK=1, seq=u+1, ack=w+1
(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 : 主动打开
发送SYN CLOSED --> LISTEN : 被动打开 LISTEN --> SYN_RCVD : 收到SYN
发送SYN+ACK SYN_SENT --> ESTABLISHED : 收到SYN+ACK
发送ACK SYN_RCVD --> ESTABLISHED : 收到ACK ESTABLISHED --> FIN_WAIT_1 : 主动关闭
发送FIN ESTABLISHED --> CLOSE_WAIT : 收到FIN
发送ACK FIN_WAIT_1 --> FIN_WAIT_2 : 收到ACK FIN_WAIT_1 --> TIME_WAIT : 收到FIN+ACK
发送ACK(同时关闭) FIN_WAIT_2 --> TIME_WAIT : 收到FIN
发送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[数据正常
回复ACK] D -->|否| F[数据损坏
丢弃不回复
触发重传] style E fill:#90EE90 style F fill:#FFB6C1

计算方式:将数据分成16位字,求和,取反码。

go 复制代码
// 简化的校验和计算(示意)
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
(期待下一个字节150) Sender->>Receiver: 发送数据 [seq=150, len=50] Receiver->>Sender: ACK=1, ack=200 Note over Sender,Receiver: 累积确认:ack=200
表示200之前的全部收到

累积确认的优势:

  • ACK=200 表示 0-199 全部收到
  • 即使 ACK=150 丢了,收到 ACK=200 就知道 150 也收到了

4.3 超时重传 ------ 没收到确认就重发

graph TD A[发送数据] --> B[启动定时器] B --> C{收到ACK?} C -->|是| D[取消定时器
发送下一个] C -->|否| E{超时?} E -->|否| C E -->|是| F[重传数据
重启定时器] F --> C style D fill:#90EE90 style F fill:#FFE4B5

超时时间计算(RTO):

go 复制代码
// 自适应超时算法(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
seq=200
seq=150] B --> C[网络乱序到达] C --> D[接收方缓存] D --> E[按序号排序] E --> F[seq=100
seq=150
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[已发送
已确认] --> B1[已发送
未确认] B1 --> C1[允许发送
未发送] C1 --> D1[不允许发送] end subgraph "窗口滑动" E1[收到ACK
窗口右移] --> F1[可发送新数据] end style B1 fill:#FFE4B5 style C1 fill:#90EE90

5.2 接收方通告窗口 rwnd

sequenceDiagram participant Sender as 发送方 participant Receiver as 接收方 Note over Receiver: 接收缓存:总大小 1000
已用 300,剩余 700 Receiver->>Sender: ACK=1, ack=501, rwnd=700
(我还能收700字节) Sender->>Receiver: 发送 700 字节 Note over Receiver: 应用层读取 200 字节
缓存剩余 400 Receiver->>Sender: ACK=1, ack=1201, rwnd=400
(我还能收400字节) Sender->>Receiver: 发送 400 字节 Note over Receiver: 应用层读取 500 字节
缓存剩余 500 Receiver->>Sender: ACK=1, ack=1601, rwnd=500
(窗口更新了!)

Go 示例:设置接收窗口

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
(我满了,别发了!) Note over Sender: 停止发送,启动持续定时器 Sender->>Receiver: 窗口探测报文
(你还满吗?) Receiver->>Sender: ACK, rwnd=0
(还是满的...) Note over Sender: 持续定时器翻倍 Sender->>Receiver: 窗口探测报文 Note over Receiver: 应用层读取数据
缓存有空间了 Receiver->>Sender: ACK, rwnd=1000
(可以发了!) 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
cwnd += 1"] B --> C["RTT 1: cwnd=1
发1个"] C --> D["RTT 2: cwnd=2
发2个"] D --> E["RTT 3: cwnd=4
发4个"] E --> F["RTT 4: cwnd=8
发8个"] F --> G["指数增长!
直到阈值"] style G fill:#90EE90

6.2 拥塞避免 ------ 线性增长

graph LR A[cwnd > ssthresh] --> B[每轮RTT
cwnd += 1] B --> C[线性增长
更保守] C --> D[直到拥塞] style B fill:#FFE4B5

6.3 拥塞发生时的处理

graph TD A[检测到拥塞] --> B{拥塞信号?} B -->|超时| C[ssthresh = cwnd/2
cwnd = 1
慢启动] B -->|3个重复ACK| D[ssthresh = cwnd/2
cwnd = ssthresh
快恢复] 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
不等超时,立即重传300 Sender->>Receiver: seq=300(快重传) Receiver->>Sender: ACK=500(累积确认) Note over Sender,Receiver: 快重传节省了
等待超时的时间!

6.5 快恢复 ------ 不降到1

graph LR A[快重传后] --> B["cwnd = ssthresh
不是1!"] B --> C[拥塞避免
线性增长] C --> D[更快恢复
传输速度] style B fill:#90EE90

完整拥塞控制状态机:

stateDiagram-v2 [*] --> SlowStart : 连接建立/超时恢复 SlowStart --> CongestionAvoidance : cwnd >= ssthresh SlowStart --> SlowStart : 每ACK
cwnd *= 2 CongestionAvoidance --> CongestionAvoidance : 每RTT
cwnd += 1 CongestionAvoidance --> FastRecovery : 3个重复ACK FastRecovery --> CongestionAvoidance : 新ACK确认
丢失包之后的数据 FastRecovery --> SlowStart : 超时 CongestionAvoidance --> SlowStart : 超时
ssthresh=cwnd/2
cwnd=1 note right of SlowStart 慢启动:指数增长 探测网络容量 end note note right of CongestionAvoidance 拥塞避免:线性增长 谨慎利用带宽 end note note right of FastRecovery 快恢复:不降到1 快速恢复传输 end note

Go 示例:查看 TCP 拥塞控制参数

go 复制代码
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: 滑动窗口
拥塞控制 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
等待2MSL

TCP vs UDP 对比总结

特性 TCP UDP
连接 面向连接 无连接
可靠性 可靠(不丢、不重、有序) 不可靠
传输方式 字节流 数据报
拥塞控制
流量控制
头部开销 20字节 8字节
速度
适用场景 文件传输、HTTP、邮件 视频直播、DNS、游戏
graph LR A[选择协议] --> B{需要可靠?} B -->|是| C[TCP
HTTP/FTP/SSH] B -->|否| D{需要速度?} D -->|是| E[UDP
直播/游戏/DNS] D -->|否| F[看场景] style C fill:#90EE90 style E fill:#90EE90

面试高频

问题 答案要点
为什么三次握手? 确认双方收发能力;防止历史连接
为什么四次挥手? 全双工,双方各自关闭;被动方可能有数据要发
TIME_WAIT作用? 确保ACK到达;防止旧报文干扰新连接
滑动窗口原理? 动态调整发送量,匹配接收方处理能力
拥塞控制四个算法? 慢启动、拥塞避免、快重传、快恢复
流量控制 vs 拥塞控制? 前者防接收方过载,后者防网络过载

tcp是可靠的传输协议,结合日常生活把这些概念套进去,就想象自己如果碰到这样的问题会如何处理,这样更易理解 TCP 是互联网的基石,理解它就像理解快递系统的运作原理------从下单(握手)、打包(分段)、运输(传输)、签收(确认)到退货(挥手),每个环节都精心设计,确保数据安全、有序、高效地到达目的地!

相关推荐
2601_949816682 小时前
如何在 Spring Boot 中配置数据库?
数据库·spring boot·后端
开心就好20252 小时前
iOS应用上架全流程:从证书申请到发布避坑指南
后端·ios
SamDeepThinking2 小时前
程序员懂业务,到底要懂到什么程度
后端·程序员·团队管理
掘金者阿豪4 小时前
搭了一个白噪音服务,才意识到之前那些“助眠APP”有多浪费钱
后端
码事漫谈4 小时前
OpenSpec实战:AI编程告别“瞎写”
后端
DyLatte4 小时前
我做了个AI项目后才发现:会做事的人,正在输给会讲故事的人
前端·后端·程序员
deviant-ART5 小时前
java stream 的 findFirst 和 findAny 踩坑点
java·开发语言·后端
神奇小汤圆5 小时前
MySQL / MariaDB 主从复制架构实战指南
后端
用户6757049885025 小时前
【AI开发实战】从想法到上线,我用AI全栈开发了一款记账微信小程序
后端·aigc·ai编程