欢迎光临
我们一直在努力

15分钟0基础理解HTTP,手搓一个API

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()

逐行解释:

  • 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 测:

    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": "你好,世界!"}

    三行搞定。你会瞬间明白框架替我们干了什么——路由匹配、状态码、响应头、序列化、空行分隔、异常兜底。 不是你变得更聪明了,是你已经知道了全貌,所以剩下的都是节省。


    孤勇之后,世界近在眼前。

    赞(0)
    未经允许不得转载:171主机测评 » 15分钟0基础理解HTTP,手搓一个API
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址