HTTP、REST 与 JSON:看懂一次请求到底发生了什么
正在读取学习记录…
HTTP 是客户端向服务端发送请求、接收响应时常用的通信约定;JSON 是正文里常见的数据格式。上一篇的函数在本机直接接收字典,这一篇让程序向本机启动的小服务请求一份资料。
先看正常往返:客户端请求 /documents/1,服务端返回状态和 JSON 正文,客户端读出资料。它像寄快递:写地址、交包裹、收到回执,再打开包裹看内容。走通这次请求后,我们才比较状态码、JSON 格式和业务结果各自能说明什么。
先记住三个角色:客户端发请求,服务端处理请求,响应把结果带回来。下面的实验会让你在自己电脑上看到这次往返。
1. 把一次请求拆开
先看见结果:把 http_lab.py 保存到新的学习文件夹,在地址栏输入 powershell,执行 python -X utf8 http_lab.py。输出里第一条应是 /documents/1、200 和 ready。这是一次真实的本机往返:脚本启动服务、请求资料、收到响应,再关闭服务。
macOS:先在 Python 篇的学习文件夹中打开终端,执行 source .venv/bin/activate。再输入 cd ,把当前例子的文件夹拖入终端并回车,运行下面的命令。
保存为 http_lab.py:
"""启动本机 HTTP 服务,观察正常响应、找不到资源和错误 JSON。"""
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Thread
from urllib.request import build_opener, ProxyHandler
from urllib.error import HTTPError
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_):
pass
def do_GET(self):
# 每个路径对应状态码、内容类型和响应正文;客户端随后读取它们。
fixtures = {
'/documents/1': (200, 'application/json', '{"ok":true,"title":"退货说明"}'),
'/missing': (404, 'application/json', '{"ok":false,"error":"not_found"}'),
'/broken': (200, 'application/json', '{broken'),
'/denied': (200, 'application/json', '{"ok":false,"error":"not_allowed"}'),
}
status, mime, text = fixtures.get(self.path, fixtures['/missing'])
body = text.encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', mime)
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def run_cases():
server = HTTPServer(('127.0.0.1', 0), Handler)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
opener = build_opener(ProxyHandler({})) # 本机回环实验直接连接,避免经过代理。
rows = []
try:
for path in ['/documents/1', '/missing', '/broken', '/denied']:
try:
response = opener.open(f'http://127.0.0.1:{server.server_port}{path}', timeout=2)
except HTTPError as error:
response = error
with response:
status = response.code
raw = response.read(4096).decode('utf-8')
if not 200 <= status < 300:
outcome = 'http_error'
else:
try:
value = json.loads(raw)
except json.JSONDecodeError:
outcome = 'json_error'
else:
outcome = 'ready' if isinstance(value, dict) and value.get('ok') is True else 'business_error'
rows.append({'path': path, 'status': status, 'content_type': response.headers.get('Content-Type'), 'body': raw, 'outcome': outcome})
finally:
server.shutdown()
server.server_close()
thread.join()
return rows
if __name__ == '__main__':
print(json.dumps(run_cases(), ensure_ascii=False, indent=2))第一条响应里的 body 是 {"ok":true,"title":"退货说明"}:title 给出资料标题,ok 表示服务允许这次读取。先把这一条读懂,再往下拆解地址和数据。其余三条用来比较失败位置,第4节会解释。
假设资料接口是http://127.0.0.1:8765/documents/1?lang=zh。这不是一长串随便拼起来的文字:http是协议方案,127.0.0.1指当前机器,8765是服务监听的端口,/documents/1定位资源,lang=zh是查询参数。
换成远程服务时,主机通常是域名。HTTPS提供传输加密与服务端身份验证机制,但不意味着返回内容必然真实、业务权限一定配置正确。这里使用HTTP是因为实验只在本机;不要把它原样作为公网凭据传输方式。
下面是便于阅读的HTTP/1.1请求示意,不是实验抓包:
GET /documents/1 HTTP/1.1
Host: 127.0.0.1:8765
Accept: application/jsonGET 表达读取资源的意图,Host 说明目标,Accept 说明客户端希望接收的格式。图中三行表达的是应用语义;HTTP/2 与 HTTP/3 在网络上的编码方式不同。
响应也有不同部分。状态码描述HTTP处理结果;Content-Type声明正文格式;正文才是具体数据。Accept是请求方偏好,Content-Type是消息所携带正文的类型声明,不要把两者方向弄反。MDN HTTP概述提供了请求与响应的基础解释。
2. HTTP、REST 和 JSON 各自的作用
HTTP是通信协议;JSON是数据交换格式;REST是一组架构约束,不是安装一个包就自动满足的标准接口形状。你可以用HTTP传HTML、图片或JSON,也可以在文件中保存JSON而不使用HTTP。
REST 的重点是用统一接口操作资源,而不是给每个动作另造一个 URL。它还有客户端与服务端分离、无状态、缓存和分层等约束;完整定义见 Fielding 的原始论述。
对于资料助手,可以把一份资料看作资源,通过/documents/1访问其表示。路径叫名词不自动证明REST设计正确;更重要的是客户端和服务端如何使用一致的接口语义,以及是否不必要地耦合内部实现。
| 概念 | 回答的问题 | 常见误解 |
|---|---|---|
| HTTP | 双方怎样表达请求和响应 | HTTP接口一定返回JSON |
| JSON | 数据怎样编码成可交换的文本 | JSON合法就代表字段和值都正确 |
| REST | 系统如何围绕资源和统一接口组织 | URL有名词就是完整REST |
| 业务契约 | 哪些字段、状态和操作被允许 | 200表示业务目标全部完成 |
模型服务也不必把所有接口设计成同一种风格。学习时先准确理解实际接口契约,再判断设计,不要为了让名称符合某个标签而忽略行为。
3. 从Python对象走到JSON字节
把下面的完整例子保存为同目录的 json_trip.py,运行 python -X utf8 json_trip.py:
# 按固定输入观察HTTP、REST 与 JSON的处理过程和返回结果。
import json
value = {"title": "退货说明", "ok": True}
text = json.dumps(value, ensure_ascii=False)
body = text.encode("utf-8")
restored = json.loads(body.decode("utf-8"))
print("字典:", value)
print("JSON文字:", text)
print("传输字节:", body)
print("还原字典:", restored)
assert restored == value这里有四个表示:Python字典、JSON文本、用于传输的字节、接收后重新解析出的字典。字节是计算机实际传送的数据单位;Python函数则更方便处理字典。UTF-8 是把文字变成字节的一套常见规则。代码里的 ensure_ascii=False 让“北京”等字符直接出现在 JSON 文本中,而不是写成反斜杠加数字的转义形式;它不会省掉 UTF-8 编码。ASCII 是早期主要为英文字母、数字和标点制定的一套字符编码,中文不在那套基本字符表里。Python json 官方文档定义了 dumps 和 loads 各自的转换方向。
JSON用双引号包住字符串,布尔值写成true/false,空值写成null;Python里对应的是True/False和None。直接对字典调用str(),看上去也有大括号,却不等于合法JSON。还有两个容易漏掉的特殊值:NaN表示“不是一个有效数字”,Infinity表示无穷大;严格JSON不允许把它们写成数字。Python默认编码器的宽松行为不能当作协议标准。RFC 8259定义了格式。
运行后,第一行与第四行是同样的字典,第二行的布尔值写成 JSON 的 true,第三行是以 b 开头的字节表示。assert restored == value 检查往返后数据是否一致。解析恢复了对象以后,再检查字段和值;例如标题应该是文字,业务字段 ok 应为 True。L1 会继续展开这种分层检查。
4. 四个本机响应,为什么只接受一个
沿用第1节保存的 http_lab.py,将下面的测试保存为同目录中的 test_http_lab.py。前者收发请求,后者检查四种响应;两个文件就足够运行。
完整代码:test_http_lab.py
# 核对HTTP、REST 与 JSON的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest
from http_lab import run_cases
class HttpTests(unittest.TestCase):
def test_status_json_and_business_are_distinct(self):
rows = run_cases()
self.assertEqual([row['status'] for row in rows], [200, 404, 200, 200])
self.assertEqual([row['outcome'] for row in rows],
['ready', 'http_error', 'json_error', 'business_error'])
if __name__ == '__main__':
unittest.main()在资源管理器打开保存文件的目录,复制路径,用 PowerShell 的 Set-Location "你的目录" 切换过去,再用 Get-ChildItem 确认文件名。这些是 PowerShell 命令,不要输入到 Python 的 >>> 窗口。
先按 Python 环境篇核对当前窗口的 python 指向学习环境,再运行:
python -X utf8 http_lab.py
python -X utf8 -m unittest test_http_lab -v实验启动127.0.0.1上的临时端口,客户端通过HTTP访问四个路径,执行后关闭服务。端口由系统分配,不占用固定8765;前面的URL只是讲解示意。实验禁用代理,避免本机请求经过你的代理配置。
脚本一起启动服务端和客户端,运行时只用一个窗口。执行完会关闭临时服务。若报找不到文件,先检查当前目录;若测试导入失败,确认两个文件都已保存且没有 .txt 后缀。
第二条命令只运行 HttpTests 这一组测试,正常时显示 Ran 1 test 与 OK。这是一个测试检查四种响应,不是只有一个响应被验证。出现 FAILED 或 ERROR 时先读失败方法及异常,不要继续当作实验成功。
| 路径 | HTTP状态 | 正文 | 客户端判断 |
|---|---|---|---|
/documents/1 | 200 | 合法JSON且ok为true | ready |
/missing | 404 | 合法JSON错误对象 | http_error |
/broken | 200 | 故意损坏的JSON | json_error |
/denied | 200 | 合法JSON但ok为false | business_error |
/denied 的状态码表示请求已处理,正文的 ok:false 则表示业务拒绝。客户端分别检查这两层,就能准确解释结果。
程序输出:
[
{
"path": "/documents/1",
"status": 200,
"content_type": "application/json",
"body": "{\"ok\":true,\"title\":\"退货说明\"}",
"outcome": "ready"
},
{
"path": "/missing",
"status": 404,
"content_type": "application/json",
"body": "{\"ok\":false,\"error\":\"not_found\"}",
"outcome": "http_error"
},
{
"path": "/broken",
"status": 200,
"content_type": "application/json",
"body": "{broken",
"outcome": "json_error"
},
{
"path": "/denied",
"status": 200,
"content_type": "application/json",
"body": "{\"ok\":false,\"error\":\"not_allowed\"}",
"outcome": "business_error"
}
]5. 按执行顺序读代码
5.1 文件中谁在当服务端,谁在当客户端
打开保存的 http_lab.py,先不要从第一行开始背模块名。按下面的顺序找三个位置:
| 位置 | 负责什么 | 对应你已经看到的现象 |
|---|---|---|
class Handler(...) 中的 do_GET | 收到 GET 后决定返回哪条测试数据 | 四个路径返回不同状态和正文 |
run_cases() | 启动服务、逐个请求、分类并收集结果 | 输出四行结构化记录 |
最后的 if __name__ == '__main__' | 直接运行此文件时启动实验并打印 | 输入运行命令后出现 JSON 列表 |
class 定义一类对象。这里用它创建标准库要求的“请求处理器”:HTTPServer 收到 GET 请求时会调用 do_GET。self.path 是当前请求中的路径;self 让方法访问这次处理需要的对象状态。
5.2 给服务端准备四个固定答案
fixtures 是一个字典,键是路径,值是包含三个元素的元组,例如 (200, 'application/json', '{"ok":true}')。元组可以先理解为按位置放好的一组值。status, mime, text = ... 将这三个值分别取出来。
fixtures.get(self.path, fixtures['/missing']) 的意思是:能找到路径就用对应值,找不到则使用 404 案例。fixtures 是事先写好的响应表,让每个请求都得到确定结果,便于观察 HTTP 请求与响应。
然后服务端把正文变成 UTF-8 字节。为什么不能直接发送字典?因为 HTTP 正文传输的是字节,不认识 Python 的内存对象。字典要先序列化成 JSON 文本,文本再编码成字节;本例的固定正文已经是 JSON 文本,坏 JSON 案例则故意省略了合法格式。
服务端的Handler根据路径选择固定响应,send_response写状态,send_header写头,end_headers结束头部,再写正文。Content-Length按编码后字节长度计算,不按汉字数量计算。若把字符数当字节数,多字节文本可能造成读取问题。
5.3 为什么一个脚本能同时收发请求
HTTPServer(('127.0.0.1', 0), Handler) 创建服务器。0 表示请操作系统分配一个可用端口,真实端口通过 server.server_port 读取。客户端必须用这个值组装地址,而不是照抄讲解示例的8765。
服务器的 serve_forever 会持续等待请求。如果直接在当前执行流程调用,程序就会停在服务循环里,走不到后面的客户端请求。示例因此用 Thread(...) 创建一个线程来运行服务:可以先把线程理解为同一程序内另一条执行流程。主流程继续发请求,服务线程接收并响应。下一篇会讲另一种等待组织方式——异步;不要把这里的线程和协程当成同一个概念。
build_opener(ProxyHandler({})) 创建直连本机的客户端,地址是 127.0.0.1。
5.4 收到响应后逐层检查
客户端使用标准库urllib。它遇到某些HTTP错误状态会抛HTTPError,但这个异常仍能提供响应状态、头和正文。实验把它作为响应继续读取,目的是观察404,而不是把404改成成功。
对照完整文件中的这一小段,看看客户端怎样保留错误响应:
try:
response = opener.open(url, timeout=2)
except HTTPError as error:
response = error随后先判断状态码;只有成功状态才继续解析JSON,再判断业务字段。连接不上服务、DNS失败等不是HTTPError响应,它们可能根本没有状态码。本例没有捕获所有网络错误,因此意外环境故障会使程序明确失败,而不是被伪装成业务拒绝。
实验的read(4096)只适合这些已知的小响应,不能当通用的流式大小保护实现。生产客户端还需验证完整读取、内容上限、编码、连接与读取超时,并按可信错误类别处理。Python的http.server文档也明确不建议将该模块用于生产服务。
注意 value.get('ok') is True:只有真正的布尔值 True 通过;数字1、字符串 "true" 或缺少字段均不算通过。这是本例的小契约,不是一个完整的接口校验系统。
5.5 最后为什么一定要关闭服务
循环外面的 finally 负责结束服务器、关闭监听资源,并用 thread.join() 等待服务线程退出。正常完成或中途失败都需要收尾;否则你可能看到“程序逻辑结束了,但终端还没退出”或者资源一直被占用。
每个响应外还有 with response:,表示读取完当前响应后关闭它。一次响应的关闭和整个服务器的关闭不是同一件事,不能用其中一个代替另一个。
你现在可以把整个脚本读成一句话:准备固定响应→后台运行本机服务→主流程逐个请求→检查状态、JSON、业务字段→保存记录→关闭响应和服务器。先能复述这条路径,再逐行研究标准库实现。
6. 方法与状态码怎样使用
| 方法或状态 | 基本含义 | 学习时需要保留的边界 |
|---|---|---|
| GET | 读取资源表示 | 不应把删除、转账等动作藏在读取里 |
| POST | 让资源处理提交的数据 | 并非天然可安全重复提交 |
| PUT | 创建或替换目标资源状态 | 幂等语义不等于服务内部没有日志等附带行为 |
| DELETE | 请求删除目标资源关联 | 重复请求的响应可能不同,不等于幂等失效 |
| 400 / 401 / 403 | 请求有误 / 需要认证 / 拒绝访问 | 先按服务契约定位,不盲目重试 |
| 429 / 5xx | 请求过多 / 服务端错误类别 | 是否重试取决于操作、预算与已知结果 |
“幂等”指相同请求重复执行的预期效果与执行一次相同,不是所有响应字节一致。超时也不等于服务端没有做事:创建请求发出后连接断了,客户端可能拿不到结果,服务端却已经执行了。准备重试写操作时,先查询原动作是否发生,或使用业务请求编号防止重复执行。详细方法语义见HTTP语义标准。
7. 从症状定位故障
例如 /broken 返回 200,但正文少了结束括号,JSON 解析失败。问题在第三层:先看正文格式,而不是去调网络或模型参数。
先问有没有建立通信:连不上时查地址、端口和服务进程;拿到HTTP响应后看状态及服务错误说明;状态成功但解析失败时查Content-Type和实际正文;数据合法却任务不对时查业务契约和上游资料。
不要把所有失败都记成“模型调用失败”。这会让你无法判断是网络、限流、JSON格式还是业务逻辑导致。日志记录路径、状态、耗时和错误类别即可起步,认证头和敏感正文不应默认记录。浏览器CORS问题也不能靠给Python脚本加一个头就概括处理:浏览器与服务端客户端的安全机制不同,后续接网页时再单独研究。
8. 自测与作品
课后练习展开习题收起习题
题1:服务器声明application/json,但正文是HTML,客户端应相信哪一个?
头部是格式声明,不是格式正确性的证明。应记录不匹配并按契约拒绝或报告解析错误,不把HTML文本硬塞成正确对象。
题2:404包含合法JSON,是否可以忽略HTTP状态?
不可以。正文可用于解释错误,但不能改变404的HTTP含义。需要先判断协议结果,再按错误契约读取详细信息。
题3:为什么实验里的ready不意味着真实资料查询成功?
服务是固定测试数据,ready只证明这条小契约被接受;没有真实检索或事实验证。实验能验证通信与分类,不验证模型或检索效果。
实践:在自己的副本增加/empty,返回200和{"ok":true,"items":[]}。先决定空结果对你的任务是否允许,再写测试。若像本例“必须有资料才能回答”那样拒绝空集合,先预测:原始分类器只检查 ok is True,所以会误报 ready;固定四行数量的原测试也会因为新增第五行而失败。这是两项不同的失败,不应只修改断言让它变绿。
在保存的学习副本里,按顺序改三个地方:
- 在
http_lab.py的Handler.do_GET中,为fixtures增加'/empty': (200, 'application/json', '{"ok":true,"items":[]}')。 - 在同一文件
run_cases的路径列表末尾加入'/empty',让客户端真的请求它。 - 在
test_http_lab.py的两条assertEqual中,将状态列表末尾追加200,结果列表末尾追加'business_error'。
这时运行 python -X utf8 -m unittest test_http_lab -v。第五条响应还是 ready,与预期不同,测试会失败。回到 http_lab.py,找到判断 ok 的那一行,将它替换为:
outcome = 'ready' if isinstance(value, dict) and value.get('ok') is True and value.get('items') != [] else 'business_error'原 /documents/1 没有 items 字段,仍得到 ready;/empty 的空列表则得到 business_error。保存后重新运行脚本与测试,检查第五条结果。
同一个测试方法现在检查五条响应。拒绝空列表是资料查询的业务规则:格式正确、请求成功以后,仍要确认结果是否够用。
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 200 就一定成功吗?
接口返回 HTTP 200,但正文是合法 JSON {"ok":false}。程序应如何处理?
对应知识点:L0-http
简答题 · 为什么 POST 不能随便重试?
网络请求超时,但服务器可能已经创建订单。请说明自动重试前至少要确认什么。
对应知识点:L0-http
代码题 · 写出三层判定伪代码
为响应写出 HTTP 状态、JSON 解析、业务字段 ok 的检查顺序,并为 404、坏 JSON、ok=false 各给出结果。
对应知识点:L0-http
场景题 · 接口返回了 HTML
调用方期待 JSON,却收到登录页 HTML。请设计排查顺序,不要只说 JSON 解析失败。
对应知识点:L0-http
9. 回顾请求与响应
回看四行结果,依次找出状态错误、格式错误和业务拒绝;它们需要检查不同的位置。
读完可以用三句话检查自己是否真的分清了层次:
- HTTP 管请求与响应;JSON 是正文的一种格式;REST 是设计接口时的一组架构约束。
- 收到状态码 200 后,仍要解析正文、检查业务字段。
{"ok":false}就是“通信成功,任务被拒绝”。 - 四个路径分别得到
ready、http_error、json_error、business_error。加上空列表反例,原业务检查会露出缺口。
项目按学习用途排列:CPython的urllib与http.server用于理解这个实验;HTTPX用于后续同步/异步客户端研究;FastAPI用于理解更完整的服务端输入契约。下一篇:异步、并发与超时。
参考资料
参考资料:RFC 9110 HTTP Semantics、RFC 8259 JSON、Fielding 的 REST 原始论述、Python json 与 http.server。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。