理解网络编程的起点是理解协议栈的分层思想。每一层隐藏了底层的实现细节,通过标准化的接口为上层提供服务。
一、OSI 七层模型 vs TCP/IP 四层模型
1.1 分层对照
┌──────────────┬──────────────────────────────────┬──────────────────┐
│ OSI │ 功能 │ TCP/IP │
├──────────────┼──────────────────────────────────┼──────────────────┤
│ 7 应用层 │ HTTP、FTP、SSH、gRPC、WebSocket │ │
│ 6 表示层 │ 数据格式、加密解密、压缩 │ 应用层 │
│ 5 会话层 │ 会话建立、管理、终止 │ │
├──────────────┼──────────────────────────────────┼──────────────────┤
│ 4 传输层 │ TCP、UDP、端口号 │ 传输层 │
├──────────────┼──────────────────────────────────┼──────────────────┤
│ 3 网络层 │ IP、ICMP、路由选择、子网划分 │ 互联网层 │
├──────────────┼──────────────────────────────────┼──────────────────┤
│ 2 数据链路层 │ MAC 地址、以太网、ARP、交换机 │ 网络接口层 │
│ 1 物理层 │ 网线、光纤、无线、比特流传输 │ │
└──────────────┴──────────────────────────────────┴──────────────────┘
1.2 分层数据交换逻辑
发送方 接收方
│ ▲
│ Application Data │ Application Data
▼ │
┌──────┐ ┌──────┐
│ HTTP │ ← 应用层:HTTP 请求报文 │ HTTP │
├──────┤ ├──────┤
│ TCP │ ← 传输层:Segment(段) │ TCP │
├──────┤ ├──────┤
│ IP │ ← 网络层:Packet(包) │ IP │
├──────┤ ├──────┤
│ ETH │ ← 链路层:Frame(帧) │ ETH │
└──────┘ └──────┘
│ ▲
▼ 物理层传输 │
二、数据封装与解封装
2.1 封装过程
应用层数据(Payload)
│
▼ + TCP Header(20-60 bytes)
传输层段(Segment)
│
▼ + IP Header(20 bytes)
网络层包(Packet)
│
▼ + ETH Header(14 bytes) + ETH Trailer(4 bytes)
链路层帧(Frame)
│
▼ 转化为比特流发送
物理层
2.2 各层 PDU(Protocol Data Unit)
| 层次 | 名称 | 标识 | 说明 |
|---|---|---|---|
| 应用层 | Data / Message | 无 | HTTP 请求/响应体 |
| 传输层 | Segment | Port | TCP 段/UDP 数据报 |
| 网络层 | Packet | IP 地址 | IP 数据报 |
| 链路层 | Frame | MAC 地址 | 以太网帧 |
| 物理层 | Bit | 电信号/光信号 | 比特流 |
2.3 Wireshark 实际观察
Frame 42: 1514 bytes on wire ← 链路层帧
Ethernet II, Src: aa:bb:cc:dd:ee:ff ← 以太网头部
Internet Protocol Version 4 ← IP 头部
Transmission Control Protocol ← TCP 头部
Hypertext Transfer Protocol ← HTTP 数据
三、IP 协议核心
3.1 IPv4 报文头部
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
3.2 子网划分
// Java 中计算子网
public class SubnetUtils {
public static void main(String[] args) {
// CIDR 表示法:192.168.1.0/24
// 网络地址:192.168.1.0
// 子网掩码:255.255.255.0(前 24 位为 1)
// 可用主机数:2^(32-24) - 2 = 254
int prefix = 24;
int hostBits = 32 - prefix;
int hostCount = (1 << hostBits) - 2; // 减去网络地址和广播地址
System.out.println("可用主机数: " + hostCount); // 254
// 判断 IP 是否在同一子网
String ip1 = "192.168.1.100";
String ip2 = "192.168.1.200";
String mask = "255.255.255.0";
int network1 = ipToInt(ip1) & ipToInt(mask);
int network2 = ipToInt(ip2) & ipToInt(mask);
System.out.println("同子网: " + (network1 == network2));
}
static int ipToInt(String ip) {
String[] parts = ip.split("\\.");
return (Integer.parseInt(parts[0]) << 24)
| (Integer.parseInt(parts[1]) << 16)
| (Integer.parseInt(parts[2]) << 8)
| Integer.parseInt(parts[3]);
}
}
3.3 IPv6 特点
| 特性 | IPv4 | IPv6 |
|---|---|---|
| 地址长度 | 32 位 | 128 位 |
| 地址数量 | 约 43 亿 | 3.4 × 10^38 |
| 头部长度 | 可变(20-60 bytes) | 固定(40 bytes) |
| 安全性 | IPsec 可选 | IPsec 内置 |
| 表示格式 | 点分十进制 | 冒号分隔十六进制 |
示例 IPv6 地址:
完整:2001:0db8:85a3:0000:0000:8a2e:0370:7334
压缩:2001:db8:85a3::8a2e:370:7334
四、ARP 协议
4.1 地址解析过程
主机 A(192.168.1.10)需要发送数据给主机 B(192.168.1.20)
1. A 检查 ARP 缓存,无 B 的 MAC 地址
2. A 发送 ARP 请求(广播):
"谁的 IP 是 192.168.1.20?告诉 192.168.1.10(MAC: aa:bb:cc:dd:ee:ff)"
3. 所有主机收到请求,只有 B 响应(单播):
"我是 192.168.1.20,MAC 是 11:22:33:44:55:66"
4. A 将 B 的 IP-MAC 映射存入 ARP 缓存
5. A 发送数据帧给 B
4.2 Java ARP 操作
// 通过 InetAddress 获取 MAC 地址(平台依赖)
InetAddress addr = InetAddress.getByName("192.168.1.1");
NetworkInterface ni = NetworkInterface.getByInetAddress(addr);
byte[] mac = ni.getHardwareAddress();
// 使用 ProcessBuilder 执行 arp 命令(Linux/macOS)
Process process = new ProcessBuilder("arp", "-a").start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
五、ICMP 与 Ping
// Java 中使用 ProcessBuilder 执行 ping
public class PingTest {
public static boolean ping(String host, int timeoutMs) throws Exception {
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "1", "-W", String.valueOf(timeoutMs / 1000), host);
Process process = pb.start();
boolean finished = process.waitFor(timeoutMs + 1000, TimeUnit.MILLISECONDS);
if (!finished) {
process.destroyForcibly();
return false;
}
return process.exitValue() == 0;
}
// 使用 isReachable(基于 ICMP 或 TCP echo)
public static boolean isReachable(String host, int timeoutMs) throws Exception {
return InetAddress.getByName(host).isReachable(timeoutMs);
}
public static void main(String[] args) throws Exception {
System.out.println(ping("baidu.com", 3000));
System.out.println(isReachable("baidu.com", 3000));
}
}
六、MTU 与分片
6.1 MTU(Maximum Transmission Unit)
| 接口类型 | 默认 MTU |
|---|---|
| 以太网 | 1500 bytes |
| PPPoE | 1492 bytes |
| IEEE 802.11 | 2304 bytes |
| 回环接口 | 65536 bytes |
| Jumbo Frame | 9000 bytes |
6.2 路径 MTU 发现
发送方 接收方
│ ─── 发送 DF=1(Don't Fragment)的包 ───→ │
│ │
│ ←── 收到 ICMP "Fragmentation Needed" ───│ MTU 只有 1400
│ │
│ ─── 发送 1400 bytes 的包 ─────────────→ │
│ │
// Java Socket 设置 MTU
Socket socket = new Socket();
socket.setReceiveBufferSize(65536);
socket.setSendBufferSize(65536);
// NIO SocketChannel
SocketChannel channel = SocketChannel.open();
channel.socket().setReceiveBufferSize(65536);
七、TCP 连接管理:三次握手与四次挥手
TCP 是面向连接的协议,连接的建立和关闭有严格的状态机。
7.1 三次握手(建立连接)
客户端 服务端
│ ─────── SYN=1, seq=x ───────────────► │ CLOSED → LISTEN
│ │
│ ◄── SYN=1, ACK=1, seq=y, ack=x+1 ──── │ LISTEN → SYN_RCVD
│ │
│ ─────── ACK=1, seq=x+1, ack=y+1 ────► │ SYN_RCVD → ESTABLISHED
│ │
ESTABLISHED │
为什么是三次而不是两次?
- 两次握手无法防止历史重复连接初始化(已失效的 SYN 包延迟到达)。
- 三次握手让双方都能确认对方具备收发能力(全双工)。
7.2 四次挥手(关闭连接)
客户端 服务端
│ ─────── FIN=1, seq=u ───────────────► │ ESTABLISHED → FIN_WAIT_1
│ │
│ ◄──────── ACK=1, ack=u+1 ──────────── │ CLOSE_WAIT
│ │ (服务端继续发未发完的数据)
│ ◄──────── FIN=1, seq=w ───────────── │ LAST_ACK
│ │
│ ─────── ACK=1, ack=w+1 ─────────────► │ CLOSED
│ │
TIME_WAIT (2×MSL) │
CLOSED │
TIME_WAIT 状态持续 2 倍最大段生存期(MSL,通常 60 秒),目的是:
- 确保最后一个 ACK 能被对方收到(丢失后对方重发 FIN 仍能响应)
- 让网络中延迟的旧数据包完全消散,避免影响新连接
大量 TIME_WAIT Socket 的调优:
# 允许端口复用(开发/测试环境)
sysctl net.ipv4.tcp_tw_reuse=1
# 快速回收 TIME_WAIT(Linux 4.12+ 已废弃 tcp_tw_recycle)
# 更推荐方案:连接池化 + 长连接
八、TCP 可靠传输:滑动窗口与拥塞控制
8.1 滑动窗口
TCP 通过滑动窗口实现流量控制(Flow Control),防止发送方淹没接收方:
发送方窗口:[已发送并确认] [已发送未确认] [可发送] [不可发送]
↑wnd ↑rwnd
接收方通过 TCP Header 中的 Window 字段通告剩余缓冲区大小
当接收方缓冲区满时,通告 rwnd=0,发送方停止发送;接收方处理完数据后发送窗口更新报文。
8.2 拥塞控制
拥塞控制防止发送方淹没网络(与流量控制的目标不同):
| 阶段 | 算法 | 行为 |
|---|---|---|
| 启动 | 慢开始(Slow Start) | cwnd 从 1 MSS 指数增长,每 RTT 翻倍 |
| 过渡 | 拥塞避免(Congestion Avoidance) | cwnd 线性增长,每 RTT +1 MSS |
| 丢包 | 快速重传 + 快速恢复 | 收到 3 个重复 ACK 后减半 cwnd |
| 超时 | 超时重传 | cwnd 直接降到 1,重新慢开始 |
现代 Linux 默认使用 CUBIC 拥塞控制算法(高带宽延迟网络的 BBR 正在普及):
# 查看当前拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 切换为 BBR(需要内核支持)
sysctl net.ipv4.tcp_congestion_control=bbr
九、DNS 解析过程
当你访问 www.example.com 时,域名的解析过程:
浏览器缓存 → 操作系统缓存 (/etc/hosts) → 本地 DNS 服务器 (递归)
│
└── 本地 DNS 向根服务器查询 .com NS
└── 向 .com TLD 服务器查询 example.com NS
└── 向 example.com 权威服务器查询 www A 记录
└── 返回 IP 地址
| 记录类型 | 用途 |
|---|---|
| A | IPv4 地址 |
| AAAA | IPv6 地址 |
| CNAME | 别名指向另一个域名 |
| MX | 邮件服务器 |
| TXT | 文本记录(SPF、DKIM、验证) |
| NS | 域名服务器 |
DNS 优化手段:
- DNS 预解析:
<link rel="dns-prefetch" href="//cdn.example.com"> - HTTPDNS:绕过运营商 Local DNS,App 直接调用 HTTP API 解析
- EDNS Client Subnet:让 CDN 根据用户真实 IP 调度最优节点
十、TLS 握手与 HTTPS 安全
HTTPS = HTTP + TLS(前身 SSL)。TLS 1.2 握手需要 2-RTT,TLS 1.3 优化到 1-RTT(甚至 0-RTT 会话恢复)。
10.1 TLS 1.2 握手流程
客户端 服务端
│ ── ClientHello (支持的加密套件、随机数) ──► │
│ ◄─ ServerHello + Certificate + ServerKeyExchange ── │
│ ── ClientKeyExchange + ChangeCipherSpec ──► │
│ ◄─ ChangeCipherSpec (Finished) ───────────── │
│ ── Finished ───────────────────────────────► │
│ │
加密通信开始(对称密钥协商完成)
10.2 TLS 1.3 改进
| 维度 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| RTT | 2 | 1 (或 0-RTT 恢复) |
| 支持的加密套件 | 多(含不安全算法) | 少(仅 AEAD) |
| 握手消息 | 明文传输部分参数 | 除 ClientHello 外全部加密 |
| 前向安全 | 取决于密钥交换方式 | 强制前向安全 |
# Nginx 启用 TLS 1.3
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
ssl_prefer_server_ciphers off;
十一、HTTP/2 与 HTTP/3 演进
| 特性 | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 传输层 | TCP | TCP | QUIC (基于 UDP) |
| 多路复用 | 无(队头阻塞) | 有(帧级别) | 有(Stream 独立) |
| 头部压缩 | 无 | HPACK | QPACK |
| 服务器推送 | 无 | 有 | 有 |
| 连接建立 | TCP + TLS = 2-3 RTT | TCP + TLS = 2-3 RTT | 0-1 RTT |
| 队头阻塞 | 应用层 | 解决 | 彻底解决(UDP 无连接级阻塞) |
HTTP/2 的多路复用解决了浏览器对同一域名并发连接数的限制(HTTP/1.1 通常为 6~8 条),但仍受 TCP 层队头阻塞影响——一个 TCP 包丢失会阻塞所有 Stream。
HTTP/3 使用 QUIC(基于 UDP 实现可靠传输),每个 Stream 独立的滑动窗口,一个 Stream 丢包不影响其他 Stream。但由于 UDP 在企业防火墙/运营商可能被限制,HTTP/3 的普及仍在进行中。
十二、常见网络故障排查速查
| 症状 | 可能原因 | 排查命令 |
|---|---|---|
| 无法访问网站 | DNS 解析失败 | dig +trace / nslookup |
| 连接超时 | 防火墙/路由不通 | traceroute / mtr |
| 连接被拒绝 | 目标端口未监听 | telnet host port / nc -zv |
| 高延迟 | 路由绕行/拥塞 | mtr --report |
| 间歇性丢包 | MTU 不一致/物理层问题 | ping -s 1472 测试大包 |
| HTTPS 证书错误 | 证书过期/SNI 不匹配 | openssl s_client -connect |
| 本地开发正常、远程异常 | CORS/防火墙/SNAT | curl -v 对比请求头 |
# 完整诊断示例:排查 api.example.com 访问异常
dig api.example.com # DNS 是否正常
ping -c 4 api.example.com # 网络层是否可达
traceroute api.example.com # 路由路径
nc -zv api.example.com 443 # 端口是否开放
curl -v -o /dev/null https://api.example.com/health # HTTP/TLS 层
ss -tin state established '( dport = :443 )' # 本地 TCP 状态
七、常用网络工具
| 工具 | 用途 | 示例 |
|---|---|---|
| ping | 测试连通性 | ping -c 4 baidu.com |
| traceroute | 路由追踪 | traceroute baidu.com |
| netstat | 查看连接 | netstat -tlnp |
| ss | 替代 netstat | ss -tlnp |
| tcpdump | 抓包 | tcpdump -i eth0 port 80 |
| Wireshark | 图形化分析 | 分析 pcap 文件 |
| telnet | 测试端口 | telnet baidu.com 80 |
| nc | 网络瑞士军刀 | nc -zv baidu.com 80 |
| curl | HTTP 请求 | curl -v https://api.example.com |
| nmap | 端口扫描 | nmap -sS 192.168.1.1 |
八、总结
| 层次 | 核心协议 | 关键概念 |
|---|---|---|
| 应用层 | HTTP/HTTPS、gRPC、DNS | 请求/响应、RPC |
| 传输层 | TCP、UDP | 端口、连接、可靠性 |
| 网络层 | IP、ICMP、ARP | 地址、路由、分片 |
| 链路层 | Ethernet、Wi-Fi | MAC、帧、交换机 |
协议栈的分层设计体现了软件工程中"关注点分离"的核心思想。每一层专注于自己的职责,通过标准化的接口与上下层交互,使得整个网络通信系统既灵活又可靠。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。