9.20. 小结

你已经走过了一条网络消息从摄像头到达世界其他地方所经过的各个层次:

  • 动因——网络之所以存在,是因为一旦有多于几台设备需要通信、或者对端不在同一根线缆上、或者许多程序同时共享同一条链路,点对点的布线就无法再扩展了。答案是共享介质、逻辑地址和路由。

  • 分层模型——五个层次,每一层解决一个问题,并向下一层提供一个清晰的接口。物理层和链路层在直接相邻的节点之间处理比特和帧,网络层处理跨互联网的寻址和路由,传输层处理程序到程序的投递,而应用协议则建立在所有这些之上。

  • 底层——以太网(Ethernet)和 Wi-Fi 作为实用的链路技术;MAC 地址在本地网段上标识硬件。摄像头的 network 模块暴露了一个值得了解的旋钮:加入哪个 Wi-Fi 网络。在那之后,下面的一切都是自动的。

  • 网络层(IP)——IPv4 和 IPv6 地址独立于主机插在哪根线缆上来标识主机。路由器在各个本地网段之间逐跳转发数据包,直到它们到达。私有地址范围和 NAT 是家庭和办公网络拥有自己内部地址空间、并在边缘共享一个公网地址的原因;出站流量畅通无阻,入站则需要帮助。

  • 传输层——端口标识主机内部的 程序;完整的 (IP, port) 对标识一个特定的套接字。UDP 是一个薄层,一次发送一个自包含的数据报,不提供任何保证——快速、廉价,是丢失可接受时的正确工具。TCP 则是一种面向连接、可靠、有序的字节流——大部分互联网流量的主力,代价是一个往返的握手延迟。

  • Python API——socket.socket 是两种协议共用的同一个类。UDP 使用 sendto() / recvfrom();TCP 使用连接或监听模式外加 send() / recv()。套接字与 asyncio 配合得很好:asyncio.open_connection()asyncio.start_server() 为每条 TCP 连接提供一对读取器/写入器,从而让许多并发会话共享一个事件循环,无需使用线程。

  • 名称与时间——socket.getaddrinfo() 把像 example.com 这样的名称转换成一个可以直接交给套接字的 IP 地址。network.hostname() 设置摄像头自己的名称,路由器会用它在本地 DNS 中注册,摄像头内置的 mDNS 响应器也会以 <name>.local 来应答它。ntptime.settime() 是把同样的“在网络上查找某样东西”的思路应用于挂钟时间,从公共 NTP 服务器以 UTC 设置板载时钟。

  • 加密——ssl 用 TLS 包裹一个套接字。摄像头出厂时不带任何证书颁发机构存储,所以开箱即用只能得到加密——会话不再是明文的,但摄像头并不验证是谁应答的。要实现真正的身份验证——验证公共 HTTPS 服务器、将摄像头作为 TLS 服务器运行、双向 TLS——基于证书的工作流程在 使用 TLS 证书 中介绍。DTLS(基于 UDP 的 TLS)以相同的方式使用同一个模块。

  • 一个真实的应用协议——MQTT 作为下面每一层连接在一起的实战示例。一个 1 字节的类型与标志字节、一个变长的剩余长度字段、一个带长度前缀的 UTF-8 主题,以及有效载荷,全都通过 TCP(并可选地在 TLS 内部)传送到代理(broker),由代理把消息分发给该主题上的每个订阅者。捆绑的 mqtt 客户端把线路格式包装成一个 connect / publish / subscribe API,小到可以一口气读完。

这足以编写能与其他机器通信、向远程服务发布数据、接受本地网络上客户端的连接,并在与摄像头其余工作并发的情况下完成所有这些的摄像头应用程序。

9.20.1. 日后使用本参考

把网络章节当作参考资料来对待;回头查阅 UDP 监听器的确切写法,或者 TLS 与 asyncio 配合的模式,正是它的预期用途。当问题只是“这个调用的确切名称是什么”时,network --- 网络配置socket --- socket 模块ssl --- SSL/TLS 模块ntptime --- 简单的 NTP 客户端 参考页在一处列出了每一个方法、标志和常量。

9.20.2. 接下来何去何从

Web 服务器 是下一个重要主题。有了可用的套接字和可用的 TLS,自然向上的下一层就是建立在它们之上的协议:用于提供内容和 API 的 HTTP、用于保持双向连接打开的 WebSocket,以及那些隐藏了样板代码的小型框架。本节中的一切都会延续下去——毕竟,一个 Web 服务器无非就是一个在其已接受的套接字上讲 HTTP 的 TCP 服务器,通常还用 TLS 把整个东西包裹起来。