15分钟0基础理解HTTP手搓一个API语言会变框架会变HTTP 协议不会过时。一、先搞清楚上网这件事到底发生了什么你在浏览器地址栏敲一个网址、点一个按钮、或者 APP 加载数据——本质上都是同一件事你的电脑客户端向另一台电脑服务器发了一段文字服务器回了一段文字。这段对话的格式就是 HTTP 协议。调用 API 这个动作里有两个角色调用方客户端发请求的人比如浏览器、curl、你的前端代码。被调用方服务端收请求、处理、返回结果的人也就是你要写的 API。我们平时用别人的 API站在调用方角度。今天换个视角自己写一个 API站在被调用方的角度。要做这件事必须先搞清楚 HTTP 请求和响应的格式——因为你的程序收到什么、该回什么全靠这个格式规定。二、一次 HTTP 对话长什么样一次完整的 HTTP 交互 一段请求request 一段响应response。两段都是纯文本而且结构完全对称┌─ 请求 ──────────────────┐ ┌─ 响应 ──────────────────┐ │ 请求行 第1行 │ │ 状态行 第1行 │ │ 请求头 第2~N行 │ │ 响应头 第2~N行 │ │ 空行 分隔线 │ │ 空行 分隔线 │ │ 请求体 可选正文 │ │ 响应体 正文 │ └─────────────────────────┘ └─────────────────────────┘拆开来看请求行包含三件事空格隔开GET /api/user/123 HTTP/1.1 ↑ ↑ ↑ 方法 路径 协议版本请求头是对这次请求的附加说明一行一条格式是名字: 值Host: example.com Accept: application/json User-Agent: curl/8.0每行一条想加几条就几条。服务端通过它们知道你是谁、想要什么格式、能接受什么语言等等。空行就是头跟体之间的一行空白。它告诉接收方“头说完了后面是正文”。没有这一行对方分不清头和体的边界。请求体是你真正要提交给服务端的内容。比如发一条微博文字本身就在请求体里。GET 请求一般没有请求体。响应的结构几乎一模一样唯一区别是第一行不叫请求行而叫状态行HTTP/1.1 200 OK ↑ ↑ ↑ 协议版本 状态码 原因短语状态码是服务端告诉你这次请求怎么样了。不用背记规律2xx→ 成功200 OK是最常见的3xx→ 重定向“你要的东西换地方了”4xx→ 你的请求有问题404 Not Found找不到、400请求格式不对5xx→ 服务器内部出问题了不是你的事响应头里最关键的一条是Content-Type它告诉调用方我返回的内容是什么格式Content-Type: application/json ← JSON 数据 Content-Type: text/html ← 网页 Content-Type: text/plain ← 纯文本浏览器和 curl 平时会帮我们把这些协议层面的东西藏起来只展示响应体。很多人用了好多年互联网从来没见过上面这些东西——不是不存在是被工具隐藏了。三、HTTP 方法不是方向是意图常见方法也叫动词一共就那么几个方法含义类比GET取数据看书POST提交数据让对方处理填表交上去PUT整体替换换整本书PATCH改一部分改一页DELETE删除撕掉HEAD只要头不要体只看看目录OPTIONS“你能对这个资源做什么”问路⚠️ 一个最常见的误解很多人觉得“GET 是往回拿数据所以不往外送数据POST 是往外送数据。” 然后遇到用 POST 也能获取数据就不理解了。事实是HTTP 方法描述的是你对资源的意图不是数据流向。GET 有响应体服务端照样可以返回大段数据给你。POST 有请求体你当然可以带数据上去POST 也可以有响应体服务端处理完给你返回结果。只是 curl 等工具有个默认规则不加-d参数默认发 GET一旦加了-d带请求体自动切成 POST。所以看起来好像带数据就是 POST实际上是工具习惯不是协议规定。四、用 curl -v 亲眼看看 HTTP 原文curl是命令行里的网络请求工具。-v这个参数verbose至关重要——它会把你发的请求原文和收到的响应原文一字不漏打印出来。curl-vhttps://httpbin.org/json你会看到三大类输出 开头的行 ← 你发出的请求原文你发给服务器的 开头的行 ← 你收到的响应原文服务器回给你的 * 开头的行 ← curl 自己的过程旁白连接过程、TLS握手等一个完整的段大概是这样的简化版 GET /json HTTP/1.1 Host: httpbin.org User-Agent: curl/8.0 Accept: */* 最后那行空白后面什么都没有——那就是分隔头和体的空行。然后服务端回过来的段 HTTP/1.1 200 OK Content-Type: application/json Content-Length: 456 {slideshow: {title: Sample}}又出现了一行空白后面什么都没有——还是那个分隔空行它下面是响应体。小知识点HTTP/1.1 的响应头首字母大写状态码后面带OK这样的原因短语。HTTP/2 则全是小写且不带原因短语。看到HTTP/2 200而不是HTTP/1.1 200 OK不要觉得奇怪没区别。五、手搓一个 APIPython 15行代码Python 自带一个http.server模块不需要装任何第三方库。下面是完整代码fromhttp.serverimportHTTPServer,BaseHTTPRequestHandlerimportjsonclassHandler(BaseHTTPRequestHandler):defdo_GET(self):# 1. 处理 GET 请求ifself.path/api/hello:# 2. 匹配路径data{msg:你好世界}bodyjson.dumps(data).encode(utf-8)self.send_response(200)# 3. 写状态码self.send_header(Content-Type,application/json)# 4. 写响应头self.end_headers()# 5. 写空行分隔self.wfile.write(body)# 6. 写响应体else:self.send_response(404)# 7. 不认识的路径404self.end_headers()self.wfile.write(bNot Found)serverHTTPServer((0.0.0.0,8000),Handler)print(服务跑在 http://localhost:8000)server.serve_forever()逐行解释do_GET这个类里do_GET方法专门处理 GET 请求。来了 GET 请求就自动调它。self.path请求的路径比如/api/hello。send_response(200)往响应里写HTTP/1.0 200。状态码在参数里。send_header(...)写一行响应头。想写几行就调几次。end_headers()写那个分隔空行。没这一行对方分不清头和体在哪。wfile.write(body)把响应体的字节写进去。else 分支路径不匹配返回 404。启动后浏览器访问http://localhost:8000/api/hello就能看到 JSON 了。也可以用 curl 测curlhttp://localhost:8000/api/hello# → {msg: 你好世界}curlhttp://localhost:8000/不存在的路径# → Not Found六、两个关键实验实验一Content-Type到底有什么用在代码里把Content-Type从application/json改成text/html同样的响应体——JSON 字符串——浏览器打开就会尝试把它当网页渲染结果要么一片白要么乱码。结论对浏览器来说网页和 API 返回的数据本质上都是同一段文本。区别就靠Content-Type这个头来标注。虽然 HTTP 规范没强制要求一定要写它但实践中不写就是给自己埋坑——调用方不知道该用什么方式解析你的返回内容。实验二服务端能看到来访者的什么信息在do_GET里加两行print(请求头:,self.headers)# 打印所有请求头print(来访 IP:,self.client_address)# 打印客户端 IP对比一下curl 来访很老实就报几个基本头User-Agent: curl/x.x、Accept: */*。浏览器来访一大堆User-Agent、Accept、Accept-Language、Accept-Encoding、Cookie……。服务端通过User-Agent一眼就能区分来的终端还是浏览器。当然这个头完全可以伪造——HTTP 协议本身就是这么天真你说你是谁服务器就信。七、为什么费这么大劲手搓直接用框架不行吗回答这个问题之前先看看这一段代码有多少跟业务无关的杂活判断请求方法匹配路径不认识的路径要 404 兜底写状态码写响应头永远不能忘记写end_headers()那行空行把对象序列化成 JSON 字节POST 请求还得读请求体、解析、校验我们只写了一个接口。一个真实项目里有几十上百个接口按这个写法不是累死的问题——是代码里 80% 的内容在干同一套杂活只有 20% 是真正的业务逻辑。但好消息是这些杂活是 HTTP 规范本身决定的不管用什么语言写后端都会遇到。所以聪明人才把这些通用逻辑打包成框架Flask、FastAPI、Express、Spring Boot……。你手搓过一遍下次打开 FastAPI 看到app.get(/api/hello)defhello():return{msg:你好世界}三行搞定。你会瞬间明白框架替我们干了什么——路由匹配、状态码、响应头、序列化、空行分隔、异常兜底。不是你变得更聪明了是你已经知道了全貌所以剩下的都是节省。孤勇之后世界近在眼前。