10.1. 你的第一个端点

在摄像头能做任何有意思的事情之前,网络中的其他部分必须能够访问到它。要证明服务器处于运行状态,最简单的办法就是提供一个返回少量 JSON 的单路由 HTTP 端点:

from microdot import Microdot

app = Microdot()

frame_count = 0
trigger_count = 0

@app.get('/status')
async def status(request):
    return {'frames': frame_count, 'triggers': trigger_count}

app.run(host='0.0.0.0', port=80)

在 IDE 中运行它。然后在局域网中的任意其他机器上打开 http://<cam-ip>/status。浏览器会显示:

{"frames": 0, "triggers": 0}

这些计数器只是占位符——目前还没有任何东西去改动它们——但请求已经穿越了网络,摄像头对它进行了路由、运行了一个处理函数,并把 JSON 返回了回来。

10.1.1. 每一行的作用

每个脚本只有一个 microdot.Microdot 实例。该实例持有路由表、错误处理函数以及生命周期(启动、服务、停止)。大型应用会拆分成多个 Python 模块,但仍然共享同一个 app 对象。

@app.get('/status') 是路由装饰器。这里我们只用到了 microdot.Microdot.get()post()put()delete() 会在后面的章节里出现,那时摄像头开始接受写入操作。

每个路由处理函数都是一个 asyncio 协程,并以请求作为它的第一个参数。处理函数不一定要使用 request——本例就忽略了它——但这个参数始终存在,从而保证函数签名的一致性。

返回一个 dict 是发送 JSON 最简短的方式。Microdot 会自动将该 dict 序列化为 JSON,并在响应中设置 Content-Type: application/json。返回一个字符串会发送 text/plain。显式返回一个 microdot.Response 则是冗长的写法——当响应体是二进制数据或需要自定义响应头时才需要它。

app.run(host='0.0.0.0', port=80) 启动服务器。0.0.0.0 表示 在摄像头拥有的每一个网络接口上监听——如果有线以太网和 WiFi STA 都已启用,那么两者都会被监听。端口 80 是 HTTP 的默认端口,因此浏览器无需输入端口号。

10.1.2. 一次请求,从头到尾

手机向摄像头打开一个 TCP 连接,发送一个 HTTP 请求,摄像头进行解析、路由、运行处理函数,然后 将响应写回。

手机打开一个 TCP 连接,写入请求行和请求头,然后等待。摄像头从套接字上读取这些字节,将它们解析为一个 microdot.Request 对象,根据路由表匹配路径和方法,await 处理函数协程,序列化它返回的内容,再将状态行、响应头和响应体写回套接字,然后关闭连接(HTTP/1.0 默认行为)或复用它(带 Connection: keep-alive 的 HTTP/1.1)。整个交换过程所需的时间,大约等于一次网络往返加上处理函数所花费的时间。

10.1.3. 关于阻塞的说明

run()阻塞的——在服务器停止之前它永远不会返回。对于一个单一用途的服务器来说,这没有问题。而对于一个同时还要捕获帧或运行其他协程的应用,则应改用置于 asyncio.run() 中的 start_server(),这样 HTTP 服务器就能与其他所有任务共享事件循环。

该应用回应一个 URL。