计算机网络
一、TCP/IP 协议栈
1.1 OSI 七层模型 vs TCP/IP 四层模型
OSI 七层模型是学术上的理想参考模型,TCP/IP 四层模型是事实上的工业标准:
1 | +--------------------+--------------------+ |
OSI 七层做减法,TCP/IP 四层做加法——前者先定义完整模型再实现协议,后者先有协议再抽象模型。TCP/IP 将表示层和会话层合并到应用层,现实中 TLS、JSON/Protobuf 等逻辑直接在应用代码中处理,不需要独立协议层。
1.2 分层设计思想
每层只关心与对等层的逻辑通信:
| 层级 | 数据单位 | 核心协议 | 寻址方式 | 典型设备 |
|---|---|---|---|---|
| 应用层 | Message/Stream | HTTP, DNS, SMTP | 域名/URI | Nginx, Tomcat |
| 传输层 | Segment/Datagram | TCP, UDP | 端口号(16bit) | 操作系统内核 |
| 网络层 | Packet | IP, ICMP | IP 地址(32/128bit) | 路由器 |
| 链路层 | Frame | Ethernet, ARP | MAC 地址(48bit) | 交换机, 网卡 |
核心收益:解耦(切换 HTTP/2 不需改动 TCP 层)、可替换(IPv4→IPv6 上层无感知)、标准化(不同厂商设备在同一层面互操作)。
1.3 封装与解封装
数据逐层添加/剥离头部,过程如下:
1 | 发送端(封装) 接收端(解封装) |
MTU 与 MSS:链路层 MTU 通常 1500 字节,减去 IP 头(20)和 TCP 头(20),MSS 通常 1460 字节。TCP 通过 MSS 协商避免 IP 分片——任何一个分片丢失都需重传整个 IP 包,性能急剧下降。
二、TCP 传输控制协议
1 | netstat -s | grep -i "listen.*overflow" |
2.2 四次挥手
1 | 主动关闭方 被动关闭方 |
为什么是四次?
TCP 是全双工通信,每个方向需单独关闭。被动方收到 FIN 仅表示对方不再发数据,自己可能还有数据未发完,因此先回 ACK,等数据发完再发 FIN——FIN 和 ACK 分开发送,所以是四次。若被动方恰好也没有数据要发了,可合并 FIN+ACK,表现为”三次挥手”,但逻辑上仍是四次。
TIME_WAIT 与 2MSL
主动关闭方发送最后 ACK 后进入 TIME_WAIT,持续 2MSL(Linux 默认 60s):
- 确保最后 ACK 被收到:若 ACK 丢失,可接收重发的 FIN 并重发 ACK
- 让旧连接的所有报文消失:防止旧连接延迟报文被新连接错误接收
TIME_WAIT 过多的危害:客户端端口耗尽。解决:
1 | sysctl -w net.ipv4.tcp_tw_reuse=1 # 客户端端口快速回收 |
服务端应让反向代理(Nginx)作为主动关闭方,保护好应用服务器。
CLOSE_WAIT 过多排查
CLOSE_WAIT 表示收到 FIN 但应用层未调用 close()——是应用层 Bug:
1 | ss -tan state close-wait | awk '{print $4}' | awk -F: '{print $1}' | sort | uniq -c | sort -rn |
| 原因 | 修复 |
|---|---|
| finally 块未关闭连接 | try-finally 保证 close() |
| 连接池泄露 | 用完归还,超时回收 |
| 异步回调未处理 | complete/error 路径都关闭 |
2.3 TCP 状态机速查
| 状态 | 含义 | 常见出现场景 |
|---|---|---|
| LISTEN | 等待连接请求 | 正常 |
| SYN_SENT | 已发送 SYN | 连接目标不可达 |
| SYN_RCVD | 收到 SYN,已回复 SYN+ACK | 半连接队列中 |
| ESTABLISHED | 已建立连接 | 正常 |
| FIN_WAIT1 | 已发 FIN | 短暂过渡 |
| FIN_WAIT2 | 收到 ACK,等对方 FIN | 对方应用层未 close() |
| CLOSE_WAIT | 收到 FIN,等应用层 close() | 应用层 Bug |
| LAST_ACK | 已发 FIN,等 ACK | 短暂过渡 |
| TIME_WAIT | 等待 2MSL | 短连接过多 |
2.4 滑动窗口与流量控制
为什么需要滑动窗口? 停等协议每发一个包等一个 ACK,吞吐量极低。滑动窗口允许连续发送多个报文段:
1 | 已确认 | 已发送未确认 | 可发送未发送 | 不可发送 |
流量控制(rwnd):接收方在 ACK 头中的 Window 字段(16bit, 启用 Window Scale 可达 1GB)通告接收窗口。rwnd = 接收缓冲区 - 已缓存未读取。
零窗口探测:当 rwnd=0,发送方启动 Persist Timer 周期性发送 1 字节探测报文。
糊涂窗口综合征:接收方每次只消耗几个字节,导致大量小包。解决方案:接收方不通告微量窗口(Clark 方案),发送方使用 Nagle 算法。
2.5 拥塞控制
拥塞控制关心网络容量,流量控制关心对方容量。
(1) 慢启动:cwnd 初始 1~10 个 MSS(Linux 3.0+ init_cwnd=10),每收到 ACK cwnd+=1 MSS,指数增长,直到达 ssthresh。
(2) 拥塞避免:cwnd≥ssthresh 后,每 RTT cwnd+=1 MSS,线性增长。
(3) 快速重传:收到 3 个重复 ACK 时不等超时直接重传。为什么是 3 次?乱序也会产生 1-2 个 Dup ACK。
(4) 快速恢复:ssthresh=cwnd/2, cwnd=ssthresh+3, 重传丢包, 此后每收一个 Dup ACK 窗口膨胀 cwnd+=1, 收新 ACK 后 cwnd=ssthresh 进入拥塞避免。
实际发送窗口 = min(rwnd, cwnd)
(5) BBR 简介:传统算法基于丢包判断拥塞,但缓冲区膨胀(Bufferbloat)导致高延迟无丢包。BBR 通过持续探测**最大带宽(BtlBw)和最小 RTT(RTprop)**建模管道瓶颈,在跨洋链路等有一定丢包的环境下性能优于 CUBIC。启用:
1 | sysctl -w net.core.default_qdisc=fq |
2.6 TCP 选项
| 选项 | 作用 | 说明 |
|---|---|---|
| MSS | 协商最大数据段 | 通常 1460(以太网),避免 IP 分片 |
| SACK | 选择性确认 | 告知已收到的具体段,避免整窗口重传 |
| Timestamp | 精确 RTT + 防序列号回绕 | PAWS 机制 |
| Window Scale | 窗口缩放(左移0-14位) | 突破 65535 字节限制,典型 8MB |
SACK 对长肥管道尤其重要:无 SACK 时一个窗口内丢一个段需重传其后所有段(Go-Back-N),有 SACK 只重传丢失的段。
2.7 Nagle 算法与延迟确认
Nagle:一个连接上最多只能有一个未确认的小包(< MSS),在该包 ACK 到达前不能发其他小包。目的是减少小包数量。
TCP_NODELAY:禁用 Nagle,低延迟场景必须设置:
1 | Socket socket = new Socket(); |
延迟确认(Delayed ACK):收到数据后等 200ms(Linux),期待有反向数据捎带 ACK 或合并后续段的 ACK。
死亡组合:Nagle(等 ACK) + Delayed ACK(等 200ms) → 双向死锁 200ms。几乎所有实时通信场景都应设置 TCP_NODELAY。
2.8 Keep-Alive 机制
TCP Keep-Alive 是系统层面的保活,默认 2 小时才开始探测,对于应用层太慢:
1 | # 仅用于检测死连接,不用于应用层心跳 |
应用层心跳更可控:能检测应用层僵死,跨代理和 NAT 更可靠:
1 | // Netty |
三、UDP 用户数据报协议
UDP 无连接、不可靠、面向报文,头部仅 8 字节(源端口、目的端口、长度、校验和)。
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接,全双工 | 无连接 |
| 可靠性 | 确认/重传/排序 | 不保证 |
| 顺序 | 有序交付 | 不保证 |
| 速度 | 慢(握手/拥塞控制) | 快 |
| 头部大小 | 20~60 字节 | 8 字节 |
| 控制 | 流量控制+拥塞控制 | 无(应用层负责) |
| 适用 | HTTP, SSH, MySQL | DNS, VoIP, 游戏, QUIC |
| 传输模式 | 字节流 | 报文 |
UDP 适用场景:
- DNS:单次请求-响应,报文 < 512B(EDNS0 4096),超限回退 TCP
- 实时音视频:丢几帧不影响,重传增加延迟
- 在线游戏:状态更新频繁,丢一两个包关系不大
- QUIC/HTTP3:在 UDP 之上实现可靠性+多路复用+加密,避免 TCP 队头阻塞
四、HTTP 超文本传输协议
4.1 HTTP/1.1
持久连接(Keep-Alive):HTTP/1.1 默认复用 TCP 连接,减少握手和慢启动开销。
管道化(Pipelining):一次发多个请求不等响应,但有队头阻塞(HOL Blocking)——第一个响应慢则后续全部阻塞,主流浏览器默认禁用。
队头阻塞:单 TCP 连接上按序处理,是 HTTP/1.1 最大性能瓶颈。
分块传输(Chunked):响应体大小未知时,Transfer-Encoding: chunked 分块发送:
1 | 200 OK |
每块:十六进制长度\r\n数据\r\n。
4.2 HTTP/2
二进制协议,核心改进:
1 | Stream 1 (HEADERS + DATA) ← /index.html |
多路复用:多 Stream 在一条 TCP 连接上交错传输帧,解决 HTTP 层面的 HOL Blocking。
头部压缩(HPACK):静态字典(61 个常见头部) + 动态字典(通信中增量更新) + 哈夫曼编码,可压缩 85% 以上。
服务器推送(Server Push):服务端主动推送客户端尚未请求的资源。但生产中使用率很低(浏览器缓存命中时浪费带宽),Chrome 已移除支持。
HTTP/2 的 TCP 队头阻塞:解决了 HTTP 层面 HOL Blocking 但 TCP 层面仍有——底层 TCP 丢包时整个链路阻塞,所有 Stream 等重传。这是 HTTP/3 改用 QUIC 的核心动机。
4.3 HTTP/3
基于 QUIC(UDP 之上),在用户态实现可靠传输+多路复用+TLS 1.3 加密:
| 特性 | HTTP/2 (TCP+TLS) | HTTP/3 (QUIC) |
|---|---|---|
| 传输层 | TCP(内核态) | UDP+QUIC(用户态) |
| 连接迁移 | 不支持(四元组绑定) | 支持(Connection ID) |
| 多路复用 | TCP 层 HOL Blocking | Stream 独立,无 HOL |
| 握手 | 2~3 RTT | 0-RTT / 1-RTT |
| 加密 | TLS 可选 | 默认加密(TLS 1.3 内建) |
连接迁移:QUIC 通过 Connection ID 标识连接,不受 IP/端口变化影响,网络切换(WiFi→4G)无需重连。
0-RTT:若客户端曾连接过服务端(缓存 PSK),第一个包即带业务数据。但存在重放攻击风险,仅适用幂等请求。
生产:内网优先 HTTP/2+gRPC,公网可在 CDN/网关启用 HTTP/3。
4.4 常见状态码
| 状态码 | 含义 | 后端关注 |
|---|---|---|
| 200 | OK | - |
| 201 | Created | RESTful POST 返回 |
| 204 | No Content | DELETE 常见 |
| 206 | Partial Content | 断点续传/Range |
| 301 | Moved Permanently | SEO 影响,浏览器缓存 |
| 302 | Found | 登录后跳转 |
| 304 | Not Modified | 协商缓存命中 |
| 400 | Bad Request | 参数校验 |
| 401 | Unauthorized | JWT 过期 |
| 403 | Forbidden | 权限不足 |
| 404 | Not Found | 资源/路由不匹配 |
| 429 | Too Many Requests | 触达限流 |
| 499 | Client Closed | Nginx 自定义:客户端断开 |
| 500 | Internal Server Error | 未捕获异常 |
| 502 | Bad Gateway | 上游不可达/非法响应 |
| 503 | Service Unavailable | 熔断/过载 |
| 504 | Gateway Timeout | 上游超时 |
4.5 Cookie、Session、Token
Cookie:Set-Cookie 响应在客户端存小段数据(4KB)。HttpOnly(防 XSS)、Secure(仅 HTTPS)、SameSite(防 CSRF)。
Session:服务端状态,Cookie 只存 Session ID。登录后服务端创建 Session(存 Redis/DB),客户端带 Session ID Cookie。
JWT:无状态认证,Header.Payload.Signature。
| 方案 | 优点 | 缺点 |
|---|---|---|
| Session | 服务端可控,可主动失效 | 有状态,扩展需共享存储 |
| JWT | 无状态,天然分布式 | 无法主动失效,体积大 |
生产实践:双 Token 模式——短期 Access Token(JWT, 15min) + 长期 Refresh Token(Opaque, 存服务端可撤销)。
4.6 跨域(CORS)
浏览器的同源策略限制跨域资源访问。CORS 通过新增 HTTP 头声明允许的源:
1 | GET /api/data |
预检请求(Preflight):非简单请求(如 Content-Type: application/json)先发 OPTIONS 检查:
1 | OPTIONS /api/data |
注意:Allow-Origin: * 与 Allow-Credentials: true 互斥。可通过反向代理统一处理 CORS。JSONP(仅 GET)已基本被 CORS 取代,生产不要使用。
4.7 HTTP 缓存
强缓存——浏览器直接用缓存,不发请求:
| 头 | 含义 |
|---|---|
Cache-Control: max-age=3600 |
缓存 3600 秒(优先级 > Expires) |
public |
可被代理/CDN 缓存 |
private |
仅浏览器缓存 |
no-cache |
每次协商(不是不缓存) |
no-store |
完全不缓存 |
immutable |
资源永不变化,不验证 |
协商缓存——发条件请求,服务端判断:
1 | # 基于时间 |
| 维度 | ETag | Last-Modified |
|---|---|---|
| 精度 | 内容变化即变 | 秒级,同秒修改检测不到 |
| 开销 | 计算 ETag 消耗 CPU | 开销小 |
生产实践:带 hash 的静态资源(app.a1b2c3.js)→ Cache-Control: max-age=31536000, immutable;HTML 入口 → no-cache;API 数据 → private, max-age=0。
五、HTTPS
5.1 TLS 1.2 握手 (ECDHE)
1 | 客户端 服务端 |
ECDHE 密钥交换:双方各自生成临时 ECDHE 密钥对→交换公钥→各自算共享密钥→结合随机数用 PRF 派生会话密钥。E(“Ephemeral”) 表示每次握手的密钥对临时生成,提供前向安全性(Forward Secrecy)——证书私钥泄露也不影响历史会话。
RSA 密钥交换由客户端用证书公钥加密 Premaster Secret——证书私钥泄露则所有历史会话可解密,无前向安全。生产环境应禁用 RSA 密钥交换。
5.2 TLS 1.3 握手
1 | 客户端 服务端 |
1-RTT:ClientHello 中直接带 DH 密钥分享参数,无需额外往返。
0-RTT:客户端缓存 PSK,在 ClientHello 带 Early Data 直接通信。注意存在重放攻击风险,仅用幂等请求。
关键改进:移除 RSA 密钥交换、静态 DH、SHA-1/MD5、CBC 模式、RC4;仅保留 AEAD 套件;大部分握手消息加密。
5.3 证书链与 CA
1 | 根 CA (Root CA) — 自签名,预置在操作系统/浏览器 |
验证:用上级证书公钥验证下级签名 → 递归到根 CA(信任锚点)。证书吊销通过 CRL/OCSP。OCSP Stapling 让服务端握手时附带 OCSP 响应,避免客户端额外查询。
5.4 中间人攻击防御
MITM 核心:攻击者分别与客户端和服务端建立加密连接,在中间解密篡改。
证书锁定(Certificate Pinning):客户端预置证书/公钥指纹,握手时验证匹配。即使攻击者获取合法 CA 签发的伪造证书也能被检测。
1 | CertificatePinner pinner = new CertificatePinner.Builder() |
Pinning 管理成本高(证书更换需客户端发版)。现代实践倾向 DNS CAA 记录(指定哪些 CA 可为该域名签发) + Certificate Transparency(公开可审计)。
七、其他重要主题
7.1 WebSocket
通过 HTTP 升级握手建立全双工 WebSocket 连接:
1 | 客户端 → 服务端: |
Sec-WebSocket-Accept = Base64(SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")),RFC 6455 固定魔数,防误升级。
帧格式:FIN(1bit) + opcode(Text=1, Binary=2, Close=8, Ping=9, Pong=10) + MASK(客户端→服务端必须掩码,服务端可选) + Payload。
心跳:协议内置 Ping/Pong 帧保活:
1 | ctx.writeAndFlush(new PingWebSocketFrame(Unpooled.EMPTY_BUFFER)); |
7.2 TCP 粘包与拆包
TCP 是字节流,无消息边界:
1 | 发送: write(AAA) write(BBB) → 接收可能: AAABBB(粘包) / AA ABBB(拆包+粘包) / ... |
解决方案:
| 方案 | 原理 | 示例 |
|---|---|---|
| 固定长度 | 每消息 N 字节 | FixedLengthFrameDecoder |
| 分隔符 | 特殊字符分隔 | \r\n(Redis, SMTP), \0 |
| 长度字段 | 固定头含消息长度 | gRPC, Dubbo, Netty |
1 | // Netty 最通用的方案 |
最佳实践:魔术字(0xCAFEBABE) + 长度字段 + 消息体。魔术字用于快速校验和协议识别。
7.3 零拷贝(Zero Copy)
文件传输场景的数据路径对比:
1 | 传统: 磁盘→内核缓冲区→(CPU拷贝)→用户缓冲区→(CPU拷贝)→Socket缓冲区→网卡 |
1 | // Java NIO 零拷贝 |
| 技术 | CPU 复制 | 适用场景 | 限制 |
|---|---|---|---|
| read+write | 2 | 通用 | 性能最差 |
| mmap+write | 1 | 大文件 | mmap 有开销 |
| sendfile | 0(SG-DMA) | 文件→Socket | 不能改数据 |
| splice | 0 | 两个 fd 间 | Linux 特有,管道中转 |
Kafka、Nginx 大量使用 sendfile 推送静态文件/日志消费。
八、排查工具与思路
8.1 tcpdump
1 | # 抓 HTTP 流量 |
8.2 Wireshark
Server 上用 tcpdump -w 保存 pcap,本地分析。关键过滤器:
tcp.stream eq 0——追踪单条 TCP 流tcp.analysis.retransmission——分析重传tcp.analysis.out_of_order——分析乱序- Statistics → Flow Graph——TCP 流时间线
8.3 ss / netstat
1 | # 按状态过滤与统计 |
8.4 mtr
实时展示各跳丢包率和延迟:
1 | mtr -r -c 100 10.0.0.5 |
解读:某一跳丢包但后续不丢——该跳限制 ICMP 速率,非真丢包;某一跳及以后全丢——该跳可能是故障点。
8.5 常见问题排查思路
(1) 连接被拒绝(ECONNREFUSED)
1 | ss -tlnp | grep 端口 # 检查是否监听 |
(2) 连接超时(SYN 无响应)
1 | ping 目标IP |
(3) 大量 TIME_WAIT
1 | ss -tan state time-wait | wc -l |
(4) 大量重传
1 | ss -ti # 每个连接 TCP 信息(rtt,rto,cwnd,retrans) |
(5) 负载不均
1 | 原因:长连接聚拢/ DNS 缓存集中/ Keep-Alive timeout 过大 |
排查口诀:先看连通(ping/telnet)→再看握手(tcpdump SYN/RST)→再看传输(netstat -s/ss -ti)→再看应用(日志/监控)。
参考资料
- RFC 793 - Transmission Control Protocol
- RFC 7540 - HTTP/2
- RFC 9000 - QUIC
- RFC 8446 - TLS 1.3
- RFC 6455 - WebSocket Protocol
- RFC 1034/1035 - DNS
- 《TCP/IP 详解 卷一:协议》— Kevin Fall, W. Richard Stevens
- 《HTTP/2 in Action》— Barry Pollard
- 《图解 HTTP》— 上野宣