
日期:2026-10-09|阶段一 · 基础与全局|实验:最小设备管理 API(云端部署)
说明:token 已脱敏;文中 IP 均为文档保留地址(203.0.113.0/24、198.51.100.0/24、10.0.0.0/8),不是真实地址。
1. 今日结论(一句话)
在腾讯云服务器上部署了设备管理 API,树莓派作为"设备"通过公网注册并持续心跳,Mac 作为管理端下发配置,配置通过心跳响应回到设备,跑通了第一条完整的 Device ↔ Cloud 控制链;并用 Wireshark 亲眼验证了 HTTP 明文鉴权头和 NAT。
2. 环境与角色
| 角色 | 机器 | 作用 |
|---|---|---|
| 云端 | 腾讯云 Ubuntu 轻量(广州三区,2 核 2G,公网 203.0.113.10,内网 10.0.0.5) | 运行 Flask API,监听 8081 |
| 设备 | 树莓派(家庭局域网,经路由器 NAT 出公网) | device_client.py:注册 + 每 10 秒心跳 + 重试 |
| 管理端 | Mac | curl 模拟 App / 运维;Wireshark 分析抓包 |
3. 完成情况
| 项目 | 状态 | 证据 / 备注 |
|---|---|---|
| 4.1 云端部署 API | ✅ | /health 本机与公网均返回 200 |
| 4.2 放行云防火墙 8081 | ✅ | Mac 公网 curl /health 返回 200 |
| 4.2 "未放行 → 超时"对比实验 | ⬜ 未记录 | 建议用未放行的 8099 端口补做 |
| 4.3-A 正常流程(注册、查询、PUT 配置) | ✅ 部分 | PUT 配置生效已由 Pi 心跳输出证明;其余命令输出未贴出 |
| 4.3-B 六种 4xx 逐个触发 | ⬜ 待做 | |
| 4.4 Pi 注册 + 心跳 | ✅ | register ok (201),心跳持续 200 |
| 配置经心跳下发 | ✅ | Pi 输出从 1280x720 / 15 变为 640x480 / 10 |
| 4.5-C 幂等键 | ⬜ 待做 | |
| 4.5-D 三种超时 / 拒绝 | ⬜ 待做(但已"意外"遇到一次 refused,见 §5) | |
| 4.5-E 服务停机时的重试 | ⬜ 待做(已看到重试输出,见 §5) | |
| 4.6 抓包 + Follow TCP Stream | ✅ | 看到明文 Authorization;Source 为家庭出口 IP |
| 4.7 systemd 常驻 | ⬜ 待做(强烈建议,见 §5) |
4. 核心知识点
4.1 一次 HTTP 交互的结构(抓包中的实物)
POST /devices/cam-pi-001/heartbeat HTTP/1.1 ← 请求行:方法 + 路径 + 版本
Host: 203.0.113.10:8081 ┐
User-Agent: python-requests/2.32.3 │
Accept: */* │ 请求头
Connection: keep-alive │
Authorization: Bearer <token> │
Content-Type: application/json ┘
Content-Length: 18
← 空行
{"cpu_temp": 38.5} ← 请求体
HTTP/1.1 200 OK ← 状态行
Content-Type: application/json
Content-Length: 55
Connection: close
← 空行
{"config":{"fps":10,"resolution":"640x480"},"ok":true}
易错点:第一行才是"请求行" ,下面的
Host:...Content-Type:才是"请求头"。
4.2 201 Created 与 Location
-
201 :请求成功且新建了资源(比 200 更精确)。
-
Location 头 :告诉调用方新资源的访问地址(如
/devices/cam-mac-test),调用方不用自己拼 URL。 -
用
curl -i才能同时看到状态行和响应头;不加-i只能看到 body。
4.3 REST 与状态码
-
资源用名词复数;动作类操作放在子资源下(
/devices/{id}/reboot)。 -
4xx = 调用方的问题;5xx = 服务方的问题。排查第一步先判断是哪一类。
-
401(你是谁?)与 403(你不能做这个);400(格式错)与 422(格式对但语义不合法)。
4.4 超时、重试、幂等
-
连接超时与读取超时含义不同;requests 里对应
ConnectTimeout/ReadTimeout,纯ConnectionError说明连接被快速拒绝或重置。 -
只对可安全重复的请求自动重试(GET / PUT / DELETE);POST 需要
Idempotency-Key。 -
指数退避 + 随机抖动 + 尊重
Retry-After。 -
注册接口把 409 视为"已注册"成功,避免"创建成功但响应丢了"的重试陷阱。
4.5 NAT(网络地址转换)
-
路由器把内网地址
192.168.x.x:端口改写成公网出口IP:端口,并记一张映射表。 -
设备主动往外发包才会建立映射,所以云端不能主动连设备;只能让设备来问,把指令放在心跳响应里带回。
-
映射有过期时间,心跳同时起到保活作用。
4.6 控制面 vs 媒体面
HTTP/API 属于控制面(注册、心跳、配置、指令);视频流属于媒体面(Day 09)。控制面故障表现为"连不上 / 配不了",媒体面故障表现为"有画面但卡顿 / 花屏"。
5. 遇到的问题与排查记录
问题 1:新开终端后命令报错(exit=3、Expecting value)
-
现象 :
curl $API/health报URL rejected: No host part in the URL(退出码 3);curl ... | python3 -m json.tool报Expecting value: line 1 column 1。 -
原因 :
export的变量只在当前终端窗口有效,新窗口里$API、$H为空。curl 实际请求的是/health,没有主机名;-s又把错误吞掉,json.tool收到空输入才报 JSON 解析错。 -
验证 :
echo "API=$API"输出为空。 -
处理 :重新 export;把变量写进
~/day04-env.sh,新窗口执行source ~/day04-env.sh。调试时用-sS代替-s,保留错误信息。 -
教训 :JSON 解析报"第 1 列第 0 字符",先怀疑上游没输出,而不是服务返回了错误数据。
问题 2:Pi 注册失败,ConnectionError 重试 5 次后放弃
-
现象 :
[retry 1..4] POST /devices: ConnectionError,最终giving up after 5 tries;curl退出码 7,8 ms 就失败。 -
排查过程:
- 异常类型是
ConnectionError而不是ConnectTimeout→ 不是"包被丢弃",排除云防火墙来源 IP 问题(防火墙是丢弃,表现为卡住超时、退出码 28)。 $API为空、无代理变量 → 排除客户端地址配置与代理干扰。- 当时一度怀疑 Pi 所在网络拦截了 8081。
- 在 Mac 上用完整地址测试,也是 exit=7(17 ms) → Pi 和 Mac 表现一致,网络路径无问题。
- 异常类型是
-
根因 :云服务器上前台运行的
python app.py已经停了(端口上没有服务,内核直接回 RST)。 -
处理:SSH 登录服务器重新启动。
-
教训:
-
"快速失败 + refused"= 端口上没有服务 ;"卡住直到超时"= 包被丢弃(防火墙 / 路由)。两者要先分开。
-
先用第二个客户端交叉验证,能迅速排除"网络路径问题"这种假设。
-
前台进程会随 SSH 会话结束而被杀,应当用 systemd(或至少 tmux)常驻。
-
问题 3:重启时 Command 'python' not found
-
原因 :新终端没有进入
~/day04,也没有source venv/bin/activate;Ubuntu 默认只有python3,python来自虚拟环境。 -
处理 :
cd ~/day04 && source venv/bin/activate && python app.py。
问题 4:重启时重新生成了 token
-
影响 :服务端
API_TOKEN变了,Mac 的$TOKEN与 Pi 的$API_TOKEN必须同步,否则全部 401。 -
要点 :
/health不需要 token,它通了不代表 token 正确 ;验证 token 要用curl -i $API/devices -H "$H"。 -
安全:token 已出现在聊天和截图中,实验结束后应重新生成。
问题 5:Pi 输出里 register ok (201) 出现多次
-
说明:同一进程内第二次注册应返回 409。若看到连续多次 201,要么服务在两次运行之间重启过(内存清空),要么是重复粘贴。
-
待办 :用
GET /devices确认设备数量,并主动构造一次 409 注册,验证"409 视为已注册"的逻辑。
6. 抓包发现(Wireshark)
| 观察 | 解读 |
|---|---|
Follow TCP Stream 里能直接读出 Authorization: Bearer ... |
HTTP 明文:路径上任何能看包的节点(同 Wi-Fi 设备、路由器、运营商、中间网络、云网络内部)都能拿到 token 并重放。云防火墙限制来源 IP 只限制"谁能连上",挡不住路径上偷看,必须用 HTTPS |
| Source 是家庭出口 IP,不是 192.168.x.x | 家庭路由器的 NAT;与 /whoami 结果互相印证 |
| Destination 是 10.0.0.5,不是 203.0.113.10 | 服务器一侧也有一层 NAT:云厂商做公网 IP ↔ 内网 IP 一对一转换 |
| 每次心跳源端口不同(如 15792 → 6593) | Flask 开发服务器响应带 Connection: close,每次心跳一个新 TCP 连接,路由器为每个新连接分配新的映射端口 |
| 一次心跳的完整序列:SYN / SYN-ACK / ACK / POST / ACK / 200 OK / FIN | 每次心跳都付出一次三次握手 + 四次挥手的代价(见思考题 1、持久连接优化) |
7. 接口异常排查表(结合今天的真实案例)
| 现象 | 问题层 | 先查什么 | 今天的案例 |
|---|---|---|---|
立刻失败,exit=7 / refused,requests 报 ConnectionError |
网络通,端口无服务 | `ss -tulpn | grep 8081`;服务是否被杀 |
卡住后超时,exit=28 / ConnectTimeout |
包被丢弃 | 云防火墙是否放行端口与来源 IP | (待用 8099 补做) |
| 连上后读取超时 | 服务端慢 / 阻塞 | 处理耗时、CPU / 内存 | (待做 /slow) |
| exit=3 / URL rejected | 本地命令问题 | echo $API |
新终端变量丢失 |
json.tool 报 Expecting value |
上游没有输出 | 去掉管道,用 -sS 单独看 curl |
同上 |
| 401 | 认证失败 | 头是否带上、两端 token 是否一致 | 重启后 token 改变需同步 |
| 404(设备相关) | 资源不存在 | 服务是否重启过(内存数据清空) | (待做:Pi 重新注册) |
| 409 | 状态冲突 | 是否重复提交 | (待构造) |
| 400 / 422 | 请求不合法 | Content-Type、JSON、字段范围 | (待做 4.3-B) |
python: command not found |
环境 | 是否激活 venv、是否在正确目录 | 新终端未激活 venv |
排查口诀:先分层(网络 / 应用),再分责(4xx / 5xx),最后看日志;云端场景先确认防火墙,再怀疑代码。 新增两条:快速失败 ≠ 超时 ;用第二个客户端交叉验证。