🧪 第12章 网络 API 的概要
📄 一句话概括

HTTP 就是几行文本:请求行、几个头、一个空行。抓到原始字节就能看见。 网页和 API 的分界线只有一个 Content-Type:给人看还是给程序看。 Flask 3 的 jsonify 默认还转义中文,要设 app.json.ensure_ascii = False。

📝 本章和前面几章的关系

这一篇对应教材 12.1 到 12.1.2:World Wide Web 与 API 的意义、客户端与服务器、API 与 JSON。 第 2 篇做的是一个网页应用。第 3 篇要把它做成 API,先得把概念说清。

一、HTTP 就是几行文本

先看实测:在 socket 层拦下一个 GET 请求,收到的原始字节是这样:

行 内容
1 GET /hello?x=1&y=2 HTTP/1.1
2 Accept-Encoding: identity
3 Host: 127.0.0.1:59657
4 User-Agent: Python-urllib/3.13
5 (空行)

一共 113 字节。 没有二进制、没有握手、没有加密——HTTPS 是在外面又包了一层,里面这些内容一模一样。 请求行三段用空格隔开:方法、路径、版本。路径里 ? 后面那段就是查询串,服务器从这儿拿参数。 那条空行是结尾标记。少了它,服务器会一直等——它不知道请求发完了没有。 这是自己写 HTTP 客户端时第一个会踩的坑。

二、服务器回的东西形状一样

部分 内容
状态行 HTTP/1.1 200 OK
头 Content-Type: application/json
头 Content-Length: 12
空行 (分界线)
体 {"ok": true}

四段,跟请求完全对称。 实测这 12 字节被 urllib 解析成「状态 200、类型 application/json、体那串 JSON」。 Content-Length 是字节数不是字符数。中文一个字三字节,算错的话客户端会截断或者一直等。 所以「写一个 Web 服务器」这件事本身不难,难的是把这一堆约定处理对。Flask 干的就是这个。

三、网页和 API 的分界线

端点 Content-Type 浏览器怎么处理
/api/hello application/json 当数据,不渲染
/page text/html; charset=utf-8 渲染成页面

两个端点跑在同一个应用、同一个进程、同一个端口上。唯一的区别就是那个头。 所以「网页」和「API」不是技术栈的区别,是「返回的东西给谁看」的区别。 这也解释了一件事:为什么同一份数据要开两个端点。 浏览器要的是带样式的页面,手机 App 要的是能解析的 JSON。两个端点、一份数据源,这是最常见的做法。

四、JSON 与 Python 的六种类型

Python JSON
dict object
list array
str string
int / float number
True / False true / false
None null

就这六种。转一圈回来类型还对得上——实测 json.loads(json.dumps(d)) 与原来相等。 这是 JSON 能当通用格式的原因:它足够简单,任何语言都能实现, 而且不会有「这个类型在那边叫什么」的歧义。 但它也只认这六种:元组变列表、日期不能直接转、集合不行、自定义对象更不行。 所以「对象转 JSON」从来不是免费的,得写一层转换。

五、中文会被转义成 \uXXXX

实测同一个字典,两种输出:"name": "\u732b" 与 "name": "猫"。 \u732b 就是「猫」。JSON 规范允许这种转义,转义后的东西还是合法的 JSON,解析出来一样。 但人看着难受。 所以要给 json.dumps 加 ensure_ascii=False。

Flask 那边有个相关的坑:Flask 3 的 jsonify 默认仍然转义中文。 实测 /api/hello 返回 {"msg":"\u4f60\u597d","n":2}, 设了 app.json.ensure_ascii = False 之后才是 {"msg":"你好","n":2}。 程序之间传输无所谓,解析出来都是对的。但调试的时候,一个中文都看不见的响应体会让人怀疑自己写错了。

六、客户端与服务器:谁先动

角色 动作 例子
客户端 发起请求,等响应 浏览器、urllib、手机 App
服务器 守着端口,收到才动 Flask、nginx 后面那堆

服务器不会主动找你。这是 HTTP 最基本的一条:一次一问一答。 所以「实时通知」在 HTTP 上只有两条路:轮询(客户端一遍遍问「有新的吗」), 或者 WebSocket(把这条规矩改掉,变成双向长连接)。 这也解释了第 5 章那个「并发」为什么重要:一个服务器进程同时只能处理一件事, 一百个客户端一起来就得排队。

七、API 这个词

API = Application Programming Interface,翻译过来是「应用程序编程接口」。 说人话:给别人用的、有约定的入口。 这里的「约定」有三样:地址(去哪)、方法(怎么动)、数据格式(说什么话)。 三样都定死了,别人才能不看你的代码就调你的东西。这就是为什么第 14 章要写接口文档。

Web API 特指「用 HTTP 当通道的 API」。 它的好处是不用装任何客户端库——能发 HTTP 请求的东西都能调,curl、浏览器、Python、手机、 另一个服务器都行。

八、动手 Demo:HTTP 就是几行文本:从 socket 看见它

这个 Demo 不用任何框架,直接在 socket 层拦一个请求,把原始字节打出来。 第一节那 113 字节是这一篇的地基:看完它,后面所有概念都有地方挂。

srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(1)
port = srv.getsockname()[1]

conn, _ = srv.accept()
raw = conn.recv(8192)        # 这就是客户端发来的全部内容
conn.sendall(b"HTTP/1.1 200 OK\r\n"
             b"Content-Type: application/json\r\n\r\n"
             b'{"ok": true}')

实跑输出:

--------------------------------------------------------------------
HTTP 就是几行文本:从 socket 看见它
--------------------------------------------------------------------

  ① 客户端到底发出去什么
  --------------------------------------------------------------------
  客户端收到 -> 200,Content-Type: application/json
  响应体 -> {"ok": true}
  服务端收到的原始字节(113 字节):
    | GET /hello?x=1&y=2 HTTP/1.1
    | Accept-Encoding: identity
    | Host: 127.0.0.1:59718
    | User-Agent: Python-urllib/3.13
    |
    |
  ! 这就是 HTTP 的全部。三行请求:请求行、两个头,末尾一个空行。
  没有二进制、没有握手协议、没有加密(那是 HTTPS 在外层加的)。
  ! 请求行三个部分用空格隔开:方法、路径、版本。
  GET /hello?x=1&y=2 HTTP/1.1——路径里那个 ? 后面就是查询串。

  ② 服务器回什么
  --------------------------------------------------------------------

  [原样]
  | HTTP/1.1 200 OK
  | Content-Type: application/json
  | Content-Length: 12
  |
  | {"ok": true}
  状态行 + 若干头 + 一个空行 + 响应体。空行是分界线,少了它客户端一直等。

  ! 实测那段响应被 urllib 解析成:状态 200、Content-Type 是 application/json、
  body 是 {"ok": true}。
  ! Content-Length 是字节数不是字符数。中文一个字三字节,算错了客户端会截断或者一直等。

  ③ Flask 走的是同一条线
  --------------------------------------------------------------------
  GET /api/hello -> 200
  Content-Type -> application/json
  响应体(默认设置)-> {"msg":"\u4f60\u597d","n":2}
  响应体(ensure_ascii=False)-> {"msg":"你好","n":2}
  GET /page -> 200
  Content-Type -> text/html; charset=utf-8
  响应体 -> <h2>这是一个网页</h2>
  ! 两个端点的区别只有一个 Content-Type。
  浏览器看到 text/html 就渲染,看到 application/json 就把它当数据。
  ! 这就是「网页」和「API」的分界线:不是地址不同、不是框架不同,
  是返回的东西是给人看的还是给程序看的。
  ! 上面那两行响应体值得单看:Flask 3 的 jsonify 默认还是转义中文的,你好 出来是 \u4f60\u597d。
  要真中文得设 app.json.ensure_ascii = False。程序之间传输无所谓,但人调试的时候看着难受。

  ④ JSON 与 Python 的对应
  --------------------------------------------------------------------

| Python | JSON |
|---|---|
| dict | object |
| list | array |
| str | string |
| int / float | number |
| True / False | true / false |
| None | null |

  json.dumps 默认 -> {"id": 1, "name": "\u732b", "tags": ["dog", "pet"], "ok": true, "note": null}
  ensure_ascii=False -> {"id": 1, "name": "猫", "tags": ["dog", "pet"], "ok": true, "note": null}
  sort_keys=True -> {"id": 1, "name": "猫", "note": null, "ok": true, "tags": ["dog", "pet"]}
  ! 默认那个 \u732b 就是「猫」。JSON 规范允许转义,但人看不懂。
  要给别人调试的接口,加上 ensure_ascii=False。
  ! Flask 的 jsonify 默认不转义中文(它设了 JSON_AS_ASCII = False 的效果),
  所以刚才那个响应体是真中文。
  转一圈回来还等于原来吗 -> True
  ! True 变成 true、None 变成 null,转一圈回来还是 True 和 None。
  类型对得上,这是 JSON 能当通用格式的原因。
  ! 但它只认这六种:元组会变成列表、日期不能直接转、集合不行。
  所以「对象转 JSON」这件事从来不是免费的,得写一层转换。

  ⑤ 客户端与服务器,谁先动
  --------------------------------------------------------------------

| 角色 | 动作 | 例子 |
|---|---|---|
| 客户端 | 发起请求,等响应 | 浏览器、requests、手机 App |
| 服务器 | 等着,收到才动 | Flask、nginx 后面的那堆 |

  ! 服务器不会主动找你。这是 HTTP 最基本的一条:请求-响应,一次一问一答。
  ! 所以「实时通知」在 HTTP 上要么靠轮询(客户端一遍遍问),要么靠 WebSocket(把这条规矩改了)。
  这跟第 5 章那个「并发」是同一类问题。
  ! 刚才那个 make_server 起在后台线程里,本质就是一个「一直在等」的东西。
  serve_forever() 那个名字很准确——它就在那儿等。

第一节末尾那两个空 | 行,就是请求结尾那个空行(\r\n\r\n)。 第三节那两行响应体,一行转义一行真中文,差别只在 ensure_ascii。 第五节两张表说明同一件事:请求和响应的形状是对称的。

30 秒实验

  1. 把 socket 那段改成收两次 recv,看第二个包里还有没有内容。
  2. 把响应的 Content-Length: 12 改成 3,再用 urllib 请求一次,看会怎样。
  3. 想一想:为什么「服务器不会主动找你」这件事会决定实时通知怎么做?

常见误区

  1. 以为 HTTP 是某种二进制协议:它就是文本,抓包一看就懂。
  2. 把 Content-Length 当成字符数:中文一个字三字节,算错就截断。
  3. 以为网页和 API 是两套技术:区别只在 Content-Type。
  4. jsonify 出来全是 \uXXXX 就以为坏了:那是默认转义,加一行配置就好。
自测三问(点开对答案) 1. 一个 HTTP 请求由哪几部分组成? —— 请求行、若干头、一个空行(GET 没有体,POST 的空行后面是体)。 2. 网页和 API 的区别到底是什么? —— Content-Type:text/html 是给人渲染的,application/json 是给程序解析的。 3. JSON 只认哪六种类型? —— object、array、string、number、true/false、null,对应 Python 的 dict、list、str、数字、 布尔、None。

概念搭好了,下一篇把网址拆开、把四个方法排开,看它们各自管什么。

相关笔记