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 -v https://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 不要觉得奇怪,没区别。
五、手搓一个 API(Python 15行代码)
Python 自带一个 http.server 模块,不需要装任何第三方库。下面是完整代码:
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class Handler(BaseHTTPRequestHandler):
def do_GET(self): # 1. 处理 GET 请求
if self.path == "/api/hello": # 2. 匹配路径
data = {"msg": "你好,世界!"}
body = json.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. 不认识的路径,404
self.end_headers()
self.wfile.write(b"Not Found")
server = HTTPServer(("0.0.0.0", 8000), Handler)
print("服务跑在 http://localhost:8000")
server.serve_forever()
逐行解释:
启动后,浏览器访问 http://localhost:8000/api/hello 就能看到 JSON 了。也可以用 curl 测:
curl http://localhost:8000/api/hello
# → {"msg": "你好,世界!"}
curl http://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")
def hello():
return {"msg": "你好,世界!"}
三行搞定。你会瞬间明白框架替我们干了什么——路由匹配、状态码、响应头、序列化、空行分隔、异常兜底。 不是你变得更聪明了,是你已经知道了全貌,所以剩下的都是节省。
孤勇之后,世界近在眼前。




