TCP从零单排

眉清目秀原来是平铺直叙

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 字节流服务 ------ 像水流一样

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<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 连接建立

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<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位字,求和,取反码。

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<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):

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<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 示例:设置接收窗口

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 拥塞控制参数

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: 滑动窗口<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 是互联网的基石,理解它就像理解快递系统的运作原理------从下单(握手)、打包(分段)、运输(传输)、签收(确认)到退货(挥手),每个环节都精心设计,确保数据安全、有序、高效地到达目的地!

相关推荐
RemainderTime28 分钟前
Spring Boot脚手架集成Sa-Token实现生产级RBAC权限管理
java·spring boot·后端·系统架构
llz_1124 小时前
web-第二次课后作业
前端·后端·web
红尘散仙9 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
卷毛的技术笔记11 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆11 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪11 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball61612 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_25183645712 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao12 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒13 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端