12.3 可重复的网络诊断实验
这一课只在本机回环接口上制造故障。目标不是记住某台机器的输出,而是练习如何定义阶段、设置 deadline、保留证据并写出有限结论。
实验边界
- 只使用
127.0.0.1和本机临时端口; - 不扫描公网或未经授权的主机;
- 不修改防火墙、路由或系统 DNS;
- 不使用
curl -k掩盖证书错误; - 每个后台进程都记录 PID 并在结束时停止。
不同系统命令选项可能不同。下文 shell 以 Linux/bash 为例,Python 需要 3.11 或兼容版本。
实验一:区分拒绝、连接成功与响应超时
创建 stall_server.py:
import socket
import time
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", 0))
listener.listen()
host, port = listener.getsockname()
print(port, flush=True)
connection, peer = listener.accept()
with connection:
print(f"accepted={peer}", flush=True)
time.sleep(10)运行它并读取操作系统分配的端口:
python3 stall_server.py >stall.log 2>&1 &
server_pid=$!
for _ in {1..50}; do
server_port=$(sed -n '1p' stall.log)
test -n "$server_port" && break
sleep 0.1
done
test -n "$server_port"先验证监听:
ss -lnt "sport = :$server_port"再请求这个“接受连接但不返回 HTTP”的服务:
curl --verbose --connect-timeout 1 --max-time 2 \
"http://127.0.0.1:$server_port/"预期:TCP 很快连接成功,HTTP 响应在总 deadline 内没有到达。这里的 timeout 发生在连接之后。
停止服务,再请求同一端口:
kill "$server_pid"
wait "$server_pid" 2>/dev/null || true
curl --verbose --connect-timeout 1 --max-time 2 \
"http://127.0.0.1:$server_port/"在普通本机 TCP 栈中,未监听端口通常很快返回 Connection refused。它与前一次“已连接、等不到响应”的阶段不同。
这个实验没有模拟 SYN 被静默丢弃。连接超时依赖路由、防火墙和重传计时,不能安全地用任意公网地址稳定复现。
实验二:用抓包验证错误阶段
终端 A 在回环接口采集,只抓该临时端口:
sudo tcpdump -i lo -nn -s 0 "tcp port $server_port" -c 20 -w loopback-lab.pcap终端 B 分别执行“服务运行时请求”和“服务停止后请求”。然后读取摘要:
tcpdump -nn -r loopback-lab.pcap回答这些问题:
- 两次请求是否都发出了 SYN?
- 哪一次完成了三次握手?
- 拒绝场景中是否出现 RST?
- 响应超时场景中,客户端是否发送了 HTTP 请求字节?
- FIN 或 RST 由哪一端发出?这与
curl的 deadline 有何关系?
回环接口上的包形态和普通以太网接口不完全相同,但 TCP 状态转换仍可用于本实验。
实验三:测量一个本地 HTTP 请求
启动标准库 HTTP 服务:
python3 -m http.server 0 --bind 127.0.0.1 >http.log 2>&1 &
http_pid=$!不同 Python 版本的日志格式并不适合作为稳定的端口发现接口。为了得到可重复实验,可以改用下面的小服务输出端口:
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
server = ThreadingHTTPServer(("127.0.0.1", 0), SimpleHTTPRequestHandler)
print(server.server_port, flush=True)
server.serve_forever()将其保存为 http_server.py 后运行:
kill "$http_pid" 2>/dev/null || true
wait "$http_pid" 2>/dev/null || true
python3 http_server.py >http.log 2>&1 &
http_pid=$!
for _ in {1..50}; do
http_port=$(sed -n '1p' http.log)
test -n "$http_port" && break
sleep 0.1
done
test -n "$http_port"执行五次请求:
for run in {1..5}; do
curl --silent --show-error --output /dev/null \
--write-out "run=$run code=%{http_code} connect=%{time_connect} starttransfer=%{time_starttransfer} total=%{time_total}\n" \
"http://127.0.0.1:$http_port/"
done在这个本地 HTTP/1.x 实验里,DNS 与 TLS 不参与。即使如此,首次请求、文件系统缓存、调度和服务日志也会带来波动。不要用五个样本宣称某项优化普遍有效;本实验只训练测量字段和阶段解释。
结束时清理:
kill "$http_pid"
wait "$http_pid" 2>/dev/null || true一个带 deadline 的 Python 诊断器
下面的程序分别记录 DNS、TCP 和 TLS。它不是监控系统,但比一个没有 timeout 的 socket.connect() 更适合作为教学骨架。
from __future__ import annotations
import socket
import ssl
import time
from dataclasses import dataclass
@dataclass(frozen=True)
class Stage:
name: str
milliseconds: float
detail: str
def diagnose(host: str, port: int = 443, timeout: float = 3.0) -> list[Stage]:
stages: list[Stage] = []
started = time.monotonic()
addresses = socket.getaddrinfo(
host,
port,
type=socket.SOCK_STREAM,
proto=socket.IPPROTO_TCP,
)
stages.append(Stage(
"dns",
(time.monotonic() - started) * 1000,
f"answers={len(addresses)}",
))
family, socktype, proto, _, sockaddr = addresses[0]
started = time.monotonic()
raw = socket.socket(family, socktype, proto)
raw.settimeout(timeout)
try:
raw.connect(sockaddr)
stages.append(Stage(
"tcp",
(time.monotonic() - started) * 1000,
f"peer={sockaddr}",
))
context = ssl.create_default_context()
started = time.monotonic()
with context.wrap_socket(raw, server_hostname=host) as tls:
stages.append(Stage(
"tls",
(time.monotonic() - started) * 1000,
f"version={tls.version()} alpn={tls.selected_alpn_protocol()}",
))
except BaseException:
raw.close()
raise
return stages
if __name__ == "__main__":
for stage in diagnose("example.com"):
print(stage)它仍有明确限制:
- 只尝试
getaddrinfo()返回的第一个地址,没有实现 Happy Eyeballs; - DNS 调用本身没有独立的 Python socket deadline;
- 没有发送 HTTP 请求;
- 一次样本不能代表长期性能;
- 异常输出需要在真实工具中进一步结构化。
教学程序应主动说明这些边界,而不是因为“运行成功”就把它包装成生产级探针。
关于端口扫描
nmap 对资产清点和故障核查很有用,也可能触发告警、违反授权范围或给脆弱服务造成压力。只扫描明确归你管理或书面授权的目标,并约定源地址、目标网段、端口范围、时间窗和速率。
即使扫描结果写着 open、closed 或 filtered,它也是基于探测响应的分类,不是应用健康证明。生产服务还需用协议级请求、身份校验和服务端证据确认。
验收清单
- [ ] 能把 connect timeout 与 response timeout 分开;
- [ ] 能用
ss证明本地监听地址,而不是笼统说“端口开了”; - [ ] 能从本地 pcap 指出 SYN、SYN-ACK、ACK、RST/FIN;
- [ ] 能说明一次抓包的接口、namespace、时间窗和过滤器;
- [ ] 能解释 curl 各时间点是累计值;
- [ ] 能为每个结论写出至少一个尚未排除的替代解释。
本章总结
第 12 章建立了一条完整诊断链:先定义症状与阶段,再用名称解析、路由、传输、TLS 和 HTTP 证据逐层收敛;需要深入时,在正确观测点做有界抓包;最后用本地实验验证自己是否真的能区分错误阶段。
下一章进入 IPv6。届时同样的诊断思路仍然适用,但地址选择、邻居发现、ICMPv6 和双栈回退会带来新的变量。