人人都能学编程
English 开始学习
专项 · 后端第 1 课 / 共 6 课免费阅读

路由 · 后端开发

Web 服务器拿到一个请求后,到底做了什么?

课程目录

为什么需要它

你用的每个应用都在和一个后端通信:一个接收请求、查找数据、返回答案的程序。Flask、FastAPI、Express、Spring 看起来各不相同,但底层都在做同样的三件事。看懂了这三件事,任何后端框架都好读了。

直觉理解

请求带着一个方法和一个路径到达:GET /users/42。服务器会:

  1. 路由:找到方法和路径模式都匹配的处理函数。/users/<id> 匹配 /users/42,得到 id = "42"。
  2. 执行处理函数,干活。
  3. 响应:返回一个状态码和响应体(通常是 JSON)。
状态码意思
200 OK这是你要的东西
201 Created新东西我建好了
400 Bad Request你的输入有问题
401 / 403你是谁?/ 你没有权限
404 Not Found没有这个东西
405 Method Not Allowed这个路径存在,但不支持这个方法
500 Internal Server Error服务器出错了

在 Flask 里同样的路由是这样写的,匹配由框架替你完成:

@app.get("/users/<id>")
def get_user(id):
    return {"id": int(id)}

本课程里我们自己写匹配,用返回 (状态码, 响应体) 的普通函数。

定义

路由

A method plus a path pattern, connected to the handler that serves it: GET /users/<id> → get_user.
一个方法加一个路径模式,连着负责处理它的函数:GET /users/<id> → get_user。

处理函数

The function that does the work for one route and returns the response.
为某个路由干活并返回响应的函数。

状态码

A three-digit number at the start of every response. 2xx success, 4xx the client's mistake, 5xx the server's.
每个响应开头的三位数字。2xx 成功,4xx 是客户端的错,5xx 是服务器的错。

示例

一个处理函数

动手试试

处理函数查找数据,然后选一个状态码。

USERS = {1: "Ada", 2: "Linus"}

def get_user(user_id):
    if user_id not in USERS:
        return 404, {"error": "no such user"}
    return 200, {"id": user_id, "name": USERS[user_id]}

print(get_user(1))
print(get_user(7))
输出
(200, {'id': 1, 'name': 'Ada'})
(404, {'error': 'no such user'})
逐步执行
  1. L4先处理找不到的情况。
  2. L8找到了:200,以及用户字典(它会变成 JSON)。(200, {'id': 1, 'name': 'Ada'})
  3. L9没找到:404,带一条错误信息。(404, {'error': 'no such user'})

匹配路径模式

动手试试

把两边都切成几段,逐段比较;<名字> 这样的段负责取值。

def match(pattern, path):
    want = pattern.strip("/").split("/")
    got = path.strip("/").split("/")
    if len(want) != len(got):
        return None
    params = {}
    for w, g in zip(want, got):
        if w.startswith("<") and w.endswith(">"):
            params[w[1:-1]] = g
        elif w != g:
            return None
    return params

print(match("/users/<id>", "/users/42"))
print(match("/users/<id>", "/posts/42"))
print(match("/users/<id>/posts", "/users/7/posts"))
print(match("/users/<id>", "/users/7/posts"))
输出
{'id': '42'}
None
{'id': '7'}
None
逐步执行
  1. L2"/users/<id>" 变成 ["users", "<id>"]。
  2. L4段数不同:不可能匹配。
  3. L8<名字> 段匹配任何内容并记下来。
  4. L10固定的段必须相等。
  5. L14id 被取出来,是文字 "42"。{'id': '42'}

一个迷你路由器

动手试试

一张路由表,和一个找到第一个匹配项的循环。

def match(pattern, path):
    want = pattern.strip("/").split("/")
    got = path.strip("/").split("/")
    if len(want) != len(got):
        return None
    params = {}
    for w, g in zip(want, got):
        if w.startswith("<") and w.endswith(">"):
            params[w[1:-1]] = g
        elif w != g:
            return None
    return params

def health(params):
    return 200, {"ok": True}

def get_user(params):
    return 200, {"id": int(params["id"])}

ROUTES = [
    ("GET", "/health", health),
    ("GET", "/users/<id>", get_user),
]

def handle(method, path):
    for m, pattern, handler in ROUTES:
        params = match(pattern, path)
        if params is not None and m == method:
            return handler(params)
    return 404, {"error": "not found"}

print(handle("GET", "/health"))
print(handle("GET", "/users/5"))
print(handle("GET", "/nope"))
输出
(200, {'ok': True})
(200, {'id': 5})
(404, {'error': 'not found'})
逐步执行
  1. L20每条路由:方法、模式、处理函数。
  2. L26按顺序尝试每条路由。
  3. L30没有任何匹配。

常见错误

什么都返回 200

如果错误也返回 200 加 {"error": …},客户端、缓存和监控都会以为成功了。要用和实际情况对应的状态码。

忘了路径参数是文字

在 /users/42 里,id 收到的是字符串 "42"。用 int() 转换,失败时返回 400 或 404。

路由的顺序

第一个匹配的路由胜出。/users/me 要放在 /users/<id> 前面,否则 "me" 会被当成 id。

小测验

1. 客户端发送 POST /orders,订单保存成功。哪个状态码最合适?

  1. A201 Created
  2. B200 OK
  3. C204 No Content
查看答案

答案:A

201 表示创建了新资源;响应体里通常带着它和它的新 id。

2. 这段代码会输出什么?

def match(pattern, path):
    want = pattern.strip("/").split("/")
    got = path.strip("/").split("/")
    if len(want) != len(got):
        return None
    params = {}
    for w, g in zip(want, got):
        if w.startswith("<") and w.endswith(">"):
            params[w[1:-1]] = g
        elif w != g:
            return None
    return params

print(match("/shop/<item>/reviews", "/shop/lamp/reviews"))
print(match("/shop/<item>", "/shop"))
  1. A
    {'item': 'lamp'}
    None
  2. B
    {'item': 'lamp'}
    {}
  3. C
    {'item': 'reviews'}
    None
查看答案

答案:A

lamp 落进 <item>。"/shop" 只有一段,而模式有两段,所以不匹配。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出给请求找路由

    准确写出它打印的内容。

  2. 改需求该是 405,不是 404

    路径存在但方法不对时,服务器应该返回 405 {"error": "method not allowed"},而不是 404。修改 handle,做到:

    • 路径和方法都匹配 → 返回处理函数的响应
    • 路径能匹配某条路由,但没有这个方法的路由 → 405
    • 没有任何路由匹配这个路径 → 404
  3. 从零写带类型的路径参数

    写一个 match(pattern, path),和例子里的一样,再加一个功能:写成 <int:名字> 的段只匹配数字,而且值要转换成 int。

    match("/users/<int:id>", "/users/42")   # {'id': 42}
    match("/users/<int:id>", "/users/me")   # None
    match("/tags/<name>", "/tags/python")   # {'name': 'python'}

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

本课术语

术语中文英文定义
Backend后端The server side of an app: it handles requests and stores data.
Route路由A method and path pattern connected to a handler.
Status code状态码The number that says how a request went: 200, 404, 500…