主题
界面
主题
从本节起进入网络部分 ——它解释"你的请求是怎么跑到另一台机器上的" ✓:
应用层 → 传输层 → 网络层 → 链路层 → 物理层
HTTP TCP IP 以太网 电信号★ 本节的做法:用裸 Socket 手工收发一次 HTTP ✓ ——这样能亲眼看到"应用层的文本协议"是怎么落在"传输层的字节流"上的 ✓✓
先看一组常见混淆 ✗ ——它们都源于"没分清哪一层负责什么":
| 常见说法 | 问题 |
|---|---|
| 「用 IP 加端口访问服务」 | 端口是传输层的,IP 是网络层的 —— 分属两层 ✓ |
| 「MAC 地址能定位到任意主机」 ✗ | MAC 只在局域网内有意义 ✓ |
| 「DNS 是网络层协议」 ✗ | 它是应用层服务(跑在 UDP 53 上)✓ |
| 「TCP 连接就是一根线」 ✗ | 它是双方内核里的一组状态,链路断了它并不知道 ⚠️ |
★ 分清层次,才能分清"问题出在哪一层" ✓✓:
ping 不通 → 网络层/链路层
端口连不上 → 传输层(或被防火墙拦)
连上了但报 404 → 应用层
能连上但很慢 → 可能是传输层重传、也可能是应用层慢| 层 | 职责 | 代表协议 | 地址/标识 |
|---|---|---|---|
| 应用层 ★★ | 定义"报文的含义" | HTTP、DNS、SMTP、FTP | URL、域名 |
| 传输层 ★★★ | 端到端的可靠/不可靠传输 | TCP、UDP | 端口号 ✓ |
| 网络层 ★★★ | 跨网络寻路(路由) | IP、ICMP、ARP | IP 地址 ✓ |
| 链路层 ★★ | 同一局域网内送达 | 以太网、WiFi | MAC 地址 ✓ |
| 物理层 | 把比特变成信号 | — | — |
★ 三条最容易搞错的边界 ✓✓:
| # | 要点 |
|---|---|
| ① | 端口是传输层的 ——IP 层只知道"送到哪台机器",不知道"给哪个程序" ✓ |
| ② | MAC 只在局域网有意义 ——跨网段时每一跳都会换 MAC,而 IP 基本不变(除 NAT)✓ |
| ③ | DNS 是应用层服务 ——它自己也要跑在 UDP/TCP 之上 ✓ |
发送方逐层加头 ✓:
你的数据: GET /api/demo HTTP/1.1 …
+ TCP 头: [源端口|目的端口|序号|确认号|标志位|…]
+ IP 头: [源 IP|目的 IP|TTL|协议号|…]
+ 以太网头: [源 MAC|目的 MAC|类型]
= 电信号接收方逐层剥头 ✓:
电信号 → 链路层剥掉 MAC 头 → 网络层剥掉 IP 头 → 传输层剥掉 TCP 头 → 交给应用★ 关键理解 ✓✓:
每一层只关心自己的头,不关心上层装了什么 ✓
所以 TCP 完全不知道自己在传 HTTP ——它照样能传邮件、传文件、传任意二进制 ✓✓
这就是"分层"的价值:可分头演进 ✓
本节的实测输出(NetworkLayersLesson):
【服务端】TCP 连接已建立,对端端口 = 1128
【服务端】收到的原始请求文本(前 6 行):
| GET /api/demo?from=raw HTTP/1.1
| Host: 127.0.0.1:1138
| User-Agent: RawSocketClient/1.0
| Accept: */*
【服务端】已回写 136 个字符的响应
【客户端】收到的原始响应文本:
| HTTP/1.1 200 OK
| Content-Type: application/json; charset=UTF-8
| Content-Length: 31
| Connection: close
|
| {"msg":"hello from raw socket"}★ 这张输出说明三件事 ✓✓:
| # | 说明 |
|---|---|
| ① | HTTP 不过是"按约定格式写的文本" ——没有任何魔法 ✓ |
| ② | TCP 只负责把这些字节按顺序、不丢地送到对端 ✓ |
| ③ | Content-Length: 31 是必需的 ——否则对方不知道"读到哪算读完" ✓✓(《TCP 与 UDP》 的粘包问题正源于此) |
实测:
【服务端】accept() 返回 → 握手完成,连接进入 ESTABLISHED
【客户端】new Socket(...) 返回耗时 8 ms★ new Socket(...) 这一行背后发生了三件事 ✓✓:
客户端 → 服务端: SYN(我想连)
服务端 → 客户端: SYN + ACK(可以,我也准备好了)
客户端 → 服务端: ACK(确认)★ 为什么是三次 ✓✓:
| 次数 | 能否满足 |
|---|---|
| 两次 ✗ | 服务端无法确认"客户端能收"(客户端可能没收到 SYN+ACK)✗ |
| 三次 ✓ | 双方都确认了"对方能收、也能发" ✓ |
| 四次 | 多余(服务端的 SYN 和 ACK 可以合并)✓ |
★ 由此得到一个重要工程结论 ✓✓:
建立连接要花 1.5 个 RTT ——本机是 8 ms,跨机房就是几十毫秒 ✗
所以"频繁建连"是性能杀手 → 必须用连接池 / 长连接 ✓✓
数据库连接池、HTTP keep-alive、HTTP/2 多路复用,根本动机都在这里 ✓
网络问题的排查顺序(从下往上) ✓✓:
| 步骤 | 命令 | 能发现什么 |
|---|---|---|
| ① 本机协议栈 | ping 127.0.0.1 | 网卡驱动是否正常 ✓ |
| ② 网关/链路 | ping 网关IP、arp -a | 局域网是否通 ✓ |
| ③ 路由与跨网 | ping 8.8.8.8 | 路由是否通(《DNS、IP、端口与 ARP》)✓ |
| ④ DNS | nslookup 域名 | 域名能否解析 ✓ |
| ⑤ 端口可达 | telnet IP 端口 / Test-NetConnection | 防火墙是否放行 ✓ |
| ⑥ 应用层 | curl -v | HTTP 状态码与头 ✓ |
★ 一条经验 ✓:
"连不上"和"连得上但慢"是两类问题 ✓✓
连不上:按上面的顺序从下往上查 ✓
慢:先看是"建连慢"还是"传输慢" ——
curl -w能拆出各阶段耗时 ✓(《HTTP 请求响应与 Servlet》 用过)
try (Socket socket = new Socket("127.0.0.1", 8080)) {
socket.getOutputStream().write(
"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
.getBytes(StandardCharsets.UTF_8));
String response = new String(socket.getInputStream().readAllBytes());
}curl 拆分阶段耗时 curl -o /dev/null -s -w "DNS:%{time_namelookup}s 连接:%{time_connect}s TLS:%{time_appconnect}s 首字节:%{time_starttransfer}s 总计:%{time_total}s\n" https://example.com★ 输出能直接告诉你"慢在哪一层" ✓✓
拔掉网线再插上 —— TCP 连接可能还在(如果超过了重传次数才会断)✗
更典型的是:**NAT 网关的超时静默回收连接,双方都不知道** ✗✗
所以长连接必须靠"心跳"来探活 ✓代码:labs/cs-basics/src/main/java/cn/kb/learn/cs/net/NetworkLayersLesson.java
运行:
cd labs
.\mvnw.cmd -pl cs-basics compile
java "-Dstdout.encoding=UTF-8" -cp cs-basics/target/classes cn.kb.learn.cs.net.NetworkLayersLesson实测输出(节选):
===== 1. 用手工 Socket 收发一次 HTTP =====
【服务端】TCP 连接已建立,对端端口 = 1128
| GET /api/demo?from=raw HTTP/1.1
| Host: 127.0.0.1:1138
【服务端】已回写 136 个字符的响应
===== 3. 三次握手在代码里的体现 =====
【服务端】accept() 返回 → 握手完成,连接进入 ESTABLISHED
【客户端】new Socket(...) 返回耗时 8 ms★ 三条可落地的结论 ✓:
| # | 结论 |
|---|---|
| ① | 排障要按层来:ping → telnet 端口 → curl ✓ |
| ② | 建连成本是 1.5 RTT,必须复用连接 ✓ |
| ③ | 应用层协议自己负责"消息边界"(Content-Length / 分隔符 / 长度字段)✓ |
Host 头去掉:观察 HTTP/1.1 服务端报 400 ★;Content-Length 去掉(在响应里):观察客户端一直等待(不知道读完没)✗;curl -v 访问同一个服务:对比它发的报文与你手写的差异 ★;netstat -ano | findstr <端口>:观察连接的四个要素(源 IP:端口 ↔ 目的 IP:端口) ★★;curl -w 拆分阶段耗时,观察 DNS / 建连 / 首字节 各占多少 ✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 把端口说成网络层的 ✗✗ | 端口是传输层的 ✓ |
| 2 | 以为 MAC 能全球定位 ✗✗ | 只在局域网内有效 ✓ |
| 3 | 把 DNS 归到网络层 ✗ | 它是应用层服务 ✓ |
| 4 | 以为 TCP 连接是"物理连接" ✗✗ | 它是内核里的状态,NAT 可能静默回收 ✓ |
| 5 | 忘了 Host 头 ✗ | HTTP/1.1 必需 ✓ |
| 6 | 忘了 Content-Length ✗✗ | 对方不知道读到哪算完 ✓ |
| 7 | 每请求新建连接 ✗✗ | 1.5 RTT 的成本 ✓ |
目标:亲手把"分层"从图变成你验证过的事实。
步骤:
NetworkLayersLesson,看原始报文 ✓;curl -v 访问同一个服务,对比两者发的头 ★;Content-Length: 999(大于实际),观察客户端卡住 ✗★;netstat -ano 看连接的四元组 ✓;telnet 手工敲一个 HTTP 请求(Linux/WSL 下 nc,Windows 下可用 telnet)★★。验收方式:能说出"一个请求从浏览器到服务端经过了哪几层、每层加了什么头"。
扩展任务:用 curl -w 做一次"分层耗时分析":
socket.getOutputStream().write("hello".getBytes());
// 服务端
int n = in.read(buffer);
System.out.println(new String(buffer, 0, n));服务端一定能一次读到 "hello" 吗? 5. (编程)用 ServerSocket + Socket 写一个最小 HTTP 服务:能返回固定 JSON,且带正确的 Content-Type 与 Content-Length。
Content-Type、Content-Length、空行、体 ✓new Socket() 背后是三次握手,成本 1.5 RTT ★★★;《TCP 与 UDP》:两种传输方式的取舍,以及"粘包"到底是怎么回事。
无,可直接开始