跳至内容

Flask 与 FastAPI 的差异

Flask 与 FastAPI 的用法差异和选型。

  • Python
  • Flask
  • FastAPI
  • WSGI
  • ASGI
  • SSE
  • WebSocket

1. 两个框架的定位

Flask 是轻量 Web 框架,自带路由、模板和 session。

模板:带变量的页面文件。Flask 用 Jinja 模板引擎把用户名等数据填入模板,生成 HTML 页面。

session(会话):在多次请求之间保留用户状态。例如登录后保存用户 ID,访问其他页面时仍能识别这个用户。

FastAPI 主要用于写 API,用 Starlette 处理 Web 请求,用 Pydantic 校验数据。

Starlette:一个支持异步的 Web 框架。FastAPI 使用它提供的路由、请求与响应处理、WebSocket 等功能。

Pydantic:Python 数据校验库,按声明检查字段类型和取值限制。例如要求 text 是字符串,且不能为空。

FastAPI 把参数声明、校验和文档放在一套写法里;Flask 的请求体校验和接口文档则需要自己实现或接入扩展。

2. 参数、校验与响应

2.1 GET 与 POST 接口示例

  • GET /get?page=1&size=2:读取 URL 中的 page 和 size,在响应的 args 字段中返回这两个参数。
  • POST /post:读取 JSON 请求体,在响应的 json 字段中返回数据。

运行环境:Python 3.13.11、Flask 3.1.3、FastAPI 0.156.4、Pydantic 2.14.1、Uvicorn 0.54.0。

2.1.1 Flask

Flask 从 request 中读取数据。这里的 POST 接口只检查请求体是不是 JSON 对象,不检查其中的字段。

flask_app.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from flask import Flask, jsonify, request

app = Flask(__name__)


@app.get("/get")
def get_args():
    return jsonify({"args": request.args.to_dict()})


@app.post("/post")
def post_json():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return jsonify({"error": "请求体必须是 JSON 对象"}), 400
    return jsonify({"json": data})

启动:

代码块 Shell
1
2
flask --app flask_app run --port 5000
# 输出:Running on http://127.0.0.1:5000

另开终端请求:

代码块 Shell
1
2
3
4
curl "http://127.0.0.1:5000/get?page=1&size=2"
curl -X POST http://127.0.0.1:5000/post \
  -H "Content-Type: application/json" \
  -d '{"text": "hello"}'
运行结果 Text
1
2
# {"args":{"page":"1","size":"2"}}
# {"json":{"text":"hello"}}

2.1.2 FastAPI

FastAPI 的 POST 接口按 TextBody 校验 text 字段,只把模型中的字段放进响应。模型未声明的额外字段默认会被忽略。

fastapi_app.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from fastapi import FastAPI, Request
from pydantic import BaseModel, Field

app = FastAPI()


class TextBody(BaseModel):
    text: str = Field(min_length=1)  # 声明字段类型和最小长度


@app.get("/get")
def get_args(request: Request):
    return {"args": dict(request.query_params)}


@app.post("/post")
def post_json(body: TextBody):
    return {"json": body.model_dump()}  # 把校验后的模型转成字典

启动:

代码块 Shell
1
2
uvicorn fastapi_app:app --port 8000
# 输出:Uvicorn running on http://127.0.0.1:8000

另开终端请求:

代码块 Shell
1
2
3
4
curl "http://127.0.0.1:8000/get?page=1&size=2"
curl -X POST http://127.0.0.1:8000/post \
  -H "Content-Type: application/json" \
  -d '{"text": "hello"}'
运行结果 Text
1
2
# {"args":{"page":"1","size":"2"}}
# {"json":{"text":"hello"}}

传入空字符串时,FastAPI 在进入 post_json 前返回 422:

代码块 Shell
1
2
3
curl -X POST http://127.0.0.1:8000/post \
  -H "Content-Type: application/json" \
  -d '{"text": ""}'
校验错误响应(节选) Text
1
2
3
4
5
6
7
8
# {
#   "detail": [{
#     "type": "string_too_short",
#     "loc": ["body", "text"],
#     "msg": "String should have at least 1 character",
#     "input": ""
#   }]
# }

loc 指出错误在请求体的 text 字段,input 是触发错误的值。

对比项Flask 示例FastAPI 示例
校验规则只检查请求体是不是 JSON 对象按 TextBody 检查 text 的类型和长度
{"text": ""}返回 200,json 中保留空字符串返回 422,不执行路由函数
增加字段校验在项目中编写检查逻辑,或接入 Pydantic 等工具在模型中声明规则,由框架执行
接口文档需要另接文档工具根据模型自动生成

2.2 路径、查询和请求体

  • 路径参数:/records/1 中的 1,对应 record_id。
  • 查询参数:?q=hello 中的 hello,对应 q。
  • 请求体:{"text": "hello"},对应 Record 模型。

FastAPI 接口样例:

records_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from typing import Annotated

from fastapi import FastAPI, HTTPException, Path, Query
from pydantic import BaseModel, Field

app = FastAPI()
records = {1: {"record_id": 1, "text": "hello"}}  # 示例数据放在内存中


class Record(BaseModel):
    text: str = Field(min_length=1)


# Annotated[类型, 附加声明]:保留 int、str 等类型,同时标明参数来源和约束。
# Path(ge=1) 要求路径编号至少为 1;Query 限制 q 最长 20 个字符,缺省值为 None。
@app.get("/records/{record_id}")
def read_record(record_id: Annotated[int, Path(ge=1)], q: Annotated[str | None, Query(max_length=20)] = None):
    if record_id not in records:
        # HTTPException 立即结束请求,客户端收到 404 和 detail 中的错误说明。
        raise HTTPException(status_code=404, detail="Record not found")
    result = dict(records[record_id])
    if q:
        result["q"] = q
    return result


@app.put("/records/{record_id}")
def put_record(record_id: Annotated[int, Path(ge=1)], record: Record):
    # Record 告诉 FastAPI 从 JSON 请求体读取数据,校验后再传入函数。
    records[record_id] = {"record_id": record_id, **record.model_dump()}
    return records[record_id]

Path、Query 和模型里的 Field 都在路由函数执行前校验,失败时返回 422。

Flask 对应的取值方式:

参数写法
路径参数<int:record_id>
查询参数request.args
JSON 请求体request.get_json()

路径中的 {record_id}、<int:record_id> 都是参数占位符。 请求 /records/3 时,函数收到 record_id=3。

  • Flask 的 <int:record_id> 把路径内容转成整数。写成 <int(min=1):record_id> 后,0 不匹配路由,返回 404。
  • FastAPI 的 Path(ge=1) 要求编号至少为 1。0 能匹配路径,但参数校验不通过,返回 422。

2.3 响应模型与状态码

FastAPI 用 response_model 校验响应数据并筛选字段,用 status_code 设置成功响应的状态码。

response_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import FastAPI, status
from pydantic import BaseModel, Field

app = FastAPI()
records = {1: {"record_id": 1, "text": "hello"}}


class Record(BaseModel):
    text: str = Field(min_length=1)


class RecordOut(BaseModel):
    record_id: int
    text: str


@app.post("/records", response_model=RecordOut, status_code=status.HTTP_201_CREATED)
def create_record(record: Record):
    record_id = max(records) + 1
    records[record_id] = {"record_id": record_id, "text": record.text}
    return {**records[record_id], "internal": "hidden"}  # internal 不在 RecordOut 中,会被过滤

首次提交 {"text": "hello"},返回 201 和以下内容:

运行结果 Text
1
# {"record_id":2,"text":"hello"}

Flask 则在视图中构造响应字段,并返回状态码。

下面样例用 Pydantic 校验同一份输入:

response_flask.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, jsonify, request
from pydantic import BaseModel, Field, ValidationError

app = Flask(__name__)
records = {1: {"record_id": 1, "text": "hello"}}


class Record(BaseModel):
    text: str = Field(min_length=1)


@app.post("/records")
def create_record():
    try:
        record = Record.model_validate(request.get_json())  # 显式调用模型校验
    except ValidationError:
        return jsonify({"error": "参数不合法"}), 400
    record_id = max(records) + 1
    records[record_id] = {"record_id": record_id, "text": record.text}
    saved = {**records[record_id], "internal": "hidden"}
    return jsonify({"record_id": saved["record_id"], "text": saved["text"]}), 201

同样首次提交 {"text": "hello"},返回 201 和以下内容:

运行结果 Text
1
# {"record_id":2,"text":"hello"}

FastAPI 应用启动后的文档地址:

  • /docs、/redoc:可浏览的接口文档。
  • /openapi.json:描述接口的 OpenAPI JSON。

2.4 错误处理

情况FastAPI 默认结果由谁判断
/records/0,编号不满足 ge=1422,detail 是校验错误列表框架按参数声明校验
/records/99,内存中没有这条记录404,{"detail": "Record not found"}视图抛出 HTTPException

参数格式合法,不代表记录存在或用户有权限。 后两项需要业务代码判断。

注册 RequestValidationError 处理器,可以把参数校验失败的响应改成 400:

errors_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import Annotated

from fastapi import FastAPI, HTTPException, Path, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

app = FastAPI()
records = {1: "hello"}


@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    # 只接管参数校验失败,不改变视图主动抛出的 HTTPException。
    return JSONResponse(status_code=400, content={"error": "参数不合法"})


@app.get("/records/{record_id}")
def read_record(record_id: Annotated[int, Path(ge=1)]):
    if record_id not in records:
        raise HTTPException(status_code=404, detail="Record not found")
    return {"record_id": record_id, "text": records[record_id]}

接口收到 /records/0 时返回 400 和 {"error": "参数不合法"};收到 /records/99 时仍返回 404。

要改这类 404 的响应格式,还需单独处理 HTTPException。

Flask 的 abort(404) 默认返回 HTML。API 要返回 JSON,可以通过 @app.errorhandler 注册处理器。

3. WSGI、ASGI 与并发

3.1 两套调用约定

WSGI(Web Server Gateway Interface)约定服务器与同步应用的调用方式:服务器传入请求信息,应用返回响应体。

ASGI(Asynchronous Server Gateway Interface)使用异步调用,应用通过 await 接收请求消息、发送响应消息。

方面WSGIASGI
调用方式app(environ, start_response)await app(scope, receive, send)
响应方式start_response 设置状态码和响应头,返回字节串可迭代对象作为响应体通过 send 发送状态码、响应头和响应体消息
协议范围HTTP 请求与响应,不定义 WebSocket 交互HTTP/1.1、HTTP/2、HTTP/3 和 WebSocket;实际支持情况还要看服务器
常见服务器Gunicorn、uWSGIUvicorn、Hypercorn

ASGI 应用在事件循环中执行。等待异步 I/O 时,它能交回控制权,让其他请求继续运行。直接调用阻塞库则会卡住事件循环,async def 本身不会消除阻塞。

单线程同步 worker 要等当前请求结束,才能处理下一个;异步 worker 能在等待 I/O 时推进其他请求。

WSGI 不限制一个进程只能处理一个请求,并发方式由服务器安排。 Flask 常见的部署方式有多进程、多线程;使用 gevent 时,则通过协程和与其配合的 I/O 调用处理多个连接。

3.2 Flask 的 async

Flask 2.0 起支持 async def 视图,需要安装 flask[async]。

普通 WSGI 部署下,Flask 在线程里启动事件循环,执行异步视图。负责这个请求的工作线程,要等视图完成才能返回响应。

这允许一个请求并发等待几个异步接口,但只把视图改成 async def,不会增加同时处理请求的数量。

3.3 同步函数和异步函数

Depends 用来声明接口依赖的函数。FastAPI 调用这个函数,把结果传给接口参数,例如数据库对象或当前用户信息。

FastAPI 调用路由和依赖函数时,按声明选择执行位置:

函数声明执行位置常见用法
async def事件循环调用支持 await 的数据库或 HTTP 库,或只处理少量内存数据
普通 defStarlette 线程池调用同步阻塞库,等待时占用工作线程

通过 Depends 声明的依赖函数遵循同一规则。同步路由在等待数据库时,事件循环能继续处理其他请求,但线程池满了以后,新的同步调用就要排队。

同步依赖与异步路由的组合:

depends_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


def get_message():
    # 普通 def 依赖由 FastAPI 在线程池中执行。
    return "hello"


@app.get("/message")
async def read_message(message: Annotated[str, Depends(get_message)]):
    # async def 路由在事件循环中执行,message 接收依赖的返回值。
    return {"message": message}

Depends(get_message) 把函数交给 FastAPI 调用,返回的 "hello" 会传入 message。 这里传函数本身,不写 get_message()。

启动:

代码块 Shell
1
2
uvicorn depends_fastapi:app --port 8000
# 输出:Uvicorn running on http://127.0.0.1:8000

另开终端请求:

代码块 Shell
1
curl http://127.0.0.1:8000/message
运行结果 Text
1
# {"message":"hello"}

自己直接调用的普通函数,不会被 FastAPI 自动移到线程池。 在 async def 路由里直接调用同步数据库函数,阻塞的仍是事件循环线程。

如果路由内部的同步函数只做少量内存操作,例如 get_message() 只返回一个字符串,路由仍适合写成 async def,直接调用这些函数无需线程池。

示例使用的是 Depends(get_message),同步依赖仍在线程池执行。 路由的 async def 不会改变依赖函数的执行位置。

3.4 单 worker 的时间线

下面两个接口都等待 1 秒。Flask 版本阻塞当前线程:

slow_flask.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import time

from flask import Flask

app = Flask(__name__)


@app.get("/slow")
def slow():
    time.sleep(1)  # 当前线程在这一秒内不能处理其他请求。
    return {"ok": True}

FastAPI 版本在等待时让出事件循环:

slow_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import asyncio

from fastapi import FastAPI

app = FastAPI()


@app.get("/slow")
async def slow():
    await asyncio.sleep(1)  # 等待期间,事件循环可以运行其他请求。
    return {"ok": True}

假设 Flask 使用一个单线程同步 worker,FastAPI 使用一个异步 worker,同时收到 4 个请求,且没有额外的业务计算:

代码块 Text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Flask,单 worker 单线程:
  req1 |---- 1s ----|
  req2              |---- 1s ----|
  req3                           |---- 1s ----|
  req4                                        |---- 1s ----|

FastAPI,单 worker:
  req1 |---- 1s ----|
  req2 |---- 1s ----|
  req3 |---- 1s ----|
  req4 |---- 1s ----|

时间线只计算等待时间,没有计入网络和调度开销。Flask 使用多 worker 或多线程时,请求能同时等待。

函数声明和等待方式都会影响 FastAPI 的并发:

路由写法执行方式
def + time.sleep(1)路由进入线程池。空闲线程足够时,4 个请求能同时等待,每个请求占一个线程
async def + time.sleep(1)阻塞事件循环线程,同一进程里的其他异步任务也无法继续

async def 不会让 CPU 计算变快。 大量 Python 计算可以交给多个进程或独立计算服务;任务队列负责安排任务何时、由谁执行,不能直接缩短计算本身的耗时。

4. 依赖注入与生命周期

4.1 Depends 与 yield

多个接口共用数据库对象时,FastAPI 用 Depends 声明对象由哪个函数提供。框架调用这个函数,把它提供的对象传入视图。

示例中的 DB 仅模拟数据库:

db_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from typing import Annotated

from fastapi import Depends, FastAPI

app = FastAPI()


class DB:
    def get(self, record_id: int):
        return {"record_id": record_id, "text": "example"}

    def close(self):
        print("close")


def get_db():
    db = DB()
    try:
        yield db  # 把对象交给视图,函数暂时停在这里。
    finally:
        db.close()  # 默认在响应发完后执行清理。


# Annotated 保留 DB 类型,同时用 Depends 指定对象的提供函数。
DBDep = Annotated[DB, Depends(get_db)]


@app.get("/db/{record_id}")
def read_record(record_id: int, db: DBDep):
    return db.get(record_id)

yield 之前创建对象,yield 之后清理对象。这里采用默认的请求作用域,清理发生在响应发送完成后。

同一请求多次引用同一依赖时,默认复用它的结果。 依赖函数需要的其他依赖,同样写在参数声明中。

如果视图返回后就不再需要这个对象,Depends(get_db, scope="function") 会在视图结束后、发送响应前清理。流式响应若还需要访问对象,就应保留默认的请求作用域。

Flask 用 g 存放当前上下文里共用的对象,视图显式调用 get_db():

db_flask.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from flask import Flask, g, jsonify

app = Flask(__name__)


class DB:
    def get(self, record_id: int):
        return {"record_id": record_id, "text": "example"}

    def close(self):
        print("close")


def get_db():
    if "db" not in g:
        g.db = DB()  # 当前请求第一次使用时创建,之后复用。
    return g.db


@app.teardown_appcontext
def close_db(exc):
    # 没有用到数据库的请求,也会经过这个清理函数。
    db = g.pop("db", None)
    if db is not None:
        db.close()


@app.get("/db/<int:record_id>")
def read_record(record_id):
    return jsonify(get_db().get(record_id))

g 属于当前应用上下文,Flask 处理请求时会自动建立这个上下文。第一次调用 get_db() 创建对象,后续调用取回同一个对象;上下文结束时,teardown_appcontext 执行清理。

对比项Flask 示例FastAPI 示例
获取对象视图调用 get_db()参数中声明 Depends,框架传入对象
同一请求内复用保存在 g 中默认缓存依赖结果
清理对象teardown_appcontextyield 后的清理代码

4.2 应用启动与关闭

数据库连接池、深度学习模型通常在应用启动时准备,关闭时释放。FastAPI 的 lifespan 参数用于注册资源管理函数:

lifespan_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from contextlib import asynccontextmanager

from fastapi import FastAPI


class Pool:
    def close(self):
        print("close")


@asynccontextmanager
async def manage_resources(app: FastAPI):
    # 启动阶段:把共享对象放在应用实例上;这里的 Pool 只是演示类。
    app.state.pool = Pool()
    yield  # 启动准备完成,应用开始处理请求。
    app.state.pool.close()  # 应用正常关闭时释放资源。


app = FastAPI(lifespan=manage_resources)

@asynccontextmanager 把带有 yield 的异步生成器函数包装成异步上下文管理器。manage_resources 是自定义函数名,lifespan 则是 FastAPI 的参数名。

  • yield 之前:启动时准备资源。
  • yield 之后:正常关闭时释放资源。

每个 worker 进程都会运行自己的生命周期。开 4 个 worker,就可能创建 4 份连接池或加载 4 份模型,数据库连接数和内存占用也会增加。

Flask 的资源管理要区分三个时机:

  • 创建应用:在 app factory(创建并配置应用的函数)或扩展初始化中准备资源。
  • 应用上下文结束:teardown_appcontext 清理当前上下文的资源,不是进程退出回调。
  • 进程退出:由部署服务器钩子等机制安排清理。

旧教程中的 before_first_request 已在 Flask 2.3 移除。

5. 路由组织

APIRouter 用于整理一组接口、共享配置。 不使用它,直接在 app 上注册路由,接口照样能处理请求:

routing_direct_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from fastapi import FastAPI

app = FastAPI()


@app.get("/records", tags=["records"])
def list_records():
    return [{"record_id": 1, "text": "hello"}]


@app.get("/records/{record_id}", tags=["records"])
def read_record(record_id: int):
    return {"record_id": record_id, "text": "hello"}

APIRouter 中的 /records 前缀和文档分组只写一次,include_router() 负责把整组路由注册到 app:

routing_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from fastapi import APIRouter, FastAPI

app = FastAPI()
router = APIRouter(prefix="/records", tags=["records"])


@router.get("")  # 与前缀拼接后是 /records。
def list_records():
    return [{"record_id": 1, "text": "hello"}]


@router.get("/{record_id}")
def read_record(record_id: int):
    return {"record_id": record_id, "text": "hello"}


app.include_router(router)  # 集中注册;只有这一步之后,app 才会接收这组路径的请求。

两版的 URL 和返回内容相同。APIRouter 减少的是公共配置的重复,通常也便于把一组路由单独放进模块;拆文件本身不要求使用它。

Flask 的 Blueprint 采用类似的分组和注册方式,下面仍提供 /records 和 /records/1 这样的路径:

routing_flask.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from flask import Blueprint, Flask

app = Flask(__name__)
records = Blueprint("records", __name__, url_prefix="/records")


@records.get("")
def list_records():
    return [{"record_id": 1, "text": "hello"}]


@records.get("/<int:record_id>")
def read_record(record_id):
    return {"record_id": record_id, "text": "hello"}


app.register_blueprint(records)

6. 后台任务

FastAPI 的 BackgroundTasks 在响应发完后调用任务函数,适合不必让客户端一直等待的短任务。 下面的 /notify 返回确认消息后,在服务端打印一条通知:

background_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from fastapi import BackgroundTasks, FastAPI

app = FastAPI()


def write_notification(text: str):
    print("notify", text)


@app.post("/notify")
async def notify(background_tasks: BackgroundTasks):
    # 注册函数及参数;此处不会立即调用 write_notification。
    background_tasks.add_task(write_notification, "ready")
    return {"ok": True}
代码块 Shell
1
2
3
# 终端 1:停止此前的 8000 端口应用后启动。
uvicorn background_fastapi:app --port 8000
# 输出:Uvicorn running on http://127.0.0.1:8000
代码块 Shell
1
2
# 终端 2:发出通知请求。
curl -X POST http://127.0.0.1:8000/notify
客户端响应 Text
1
# {"ok":true}
服务端输出 Text
1
# notify ready

任务仍在应用进程里执行;进程退出时尚未完成的任务,不会自动保存并在下次启动后续跑。

通知如果不能丢、失败后需要重试,就需要保存任务状态的队列和执行进程,例如 Celery 或 RQ。

Flask 没有对应的内置后台任务接口,通常接任务队列。若自行启动线程执行短任务,线程管理和应用关闭时的处理也要由应用安排。

7. 认证

认证用于确认这次请求代表哪个用户。 客户端登录成功时获得一段 token,后续请求带上它。服务器验证通过,请求才能进入需要登录的接口。

请求头中的 Bearer 凭据:

代码块 Text
1
Authorization: Bearer secret

Authorization 是传递认证凭据的请求头;Bearer 表示客户端凭持有的 token 发起请求。这里用固定的 secret 演示读取和验证,省略登录及签发过程。

FastAPI 用两层 Depends 连接这些步骤:

认证流程 Text
1
2
3
4
5
HTTPBearer 读取请求头
    ↓
get_current_user 验证 token
    ↓
/me 接收用户信息

客户端只发送 token;user 来自服务端的验证函数,并不是客户端直接提交的参数。

auth_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer

app = FastAPI()
security = HTTPBearer()


# Depends(security) 先调用 HTTPBearer,再把解析结果传入 credentials。
def get_current_user(credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]):
    token = credentials.credentials  # 请求头中 Bearer 后面的内容。
    if token != "secret":
        raise HTTPException(status_code=401, detail="invalid token")
    return {"id": 7, "name": "Lin"}


@app.get("/me")
def me(user: Annotated[dict, Depends(get_current_user)]):
    # user 是 get_current_user 的返回值;验证失败时不会执行这个视图。
    return user
代码块 Shell
1
2
3
# 终端 1:停止此前占用 8000 端口的应用后启动。
uvicorn auth_fastapi:app --port 8000
# 输出:Uvicorn running on http://127.0.0.1:8000
代码块 Shell
1
2
# 终端 2:-H 指定请求头,把 token 发送给服务器。
curl -H "Authorization: Bearer secret" http://127.0.0.1:8000/me
运行结果 Text
1
# {"id":7,"name":"Lin"}

没有 Bearer 凭据时,HTTPBearer 返回 401;token 不是 secret 时,get_current_user 返回 401。这两种情况下,me 都不会执行。

取出 token 不等于验证通过,HTTPBearer 不知道它属于哪个用户,也不会替应用检查它是否有效。

在 /docs 点击 Authorize 并填入 secret 后,文档页面会为请求加上同样的认证头。

Flask 的 request.authorization 提供解析后的认证信息,以下示例在视图里直接验证:

auth_flask.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from flask import Flask, jsonify, request

app = Flask(__name__)


@app.get("/me")
def me():
    credentials = request.authorization
    # type 是认证方式,token 是 Bearer 后面的内容。
    if credentials is None or credentials.type != "bearer" or credentials.token != "secret":
        return jsonify({"detail": "invalid token"}), 401
    return jsonify({"id": 7, "name": "Lin"})
代码块 Shell
1
2
3
# 终端 1:停止此前占用 5000 端口的应用后启动。
flask --app auth_flask run --port 5000
# 输出:Running on http://127.0.0.1:5000
代码块 Shell
1
2
# 终端 2:发送相同的凭据。
curl -H "Authorization: Bearer secret" http://127.0.0.1:5000/me
运行结果 Text
1
# {"id":7,"name":"Lin"}
  • Flask:在视图里读取和检查凭据。多个接口共用验证逻辑时,通常提取到装饰器或请求钩子。
  • FastAPI:用 Depends 声明验证依赖,验证通过后,框架把用户信息传入视图。

实际应用需要在登录时签发凭据,并在请求中验证凭据,例如检查 token 是否过期。固定字符串比较只用于演示,不能代替这些验证。

8. 长连接

8.1 WebSocket

WebSocket 让客户端和服务器在同一条连接上持续收发消息。

FastAPI 的回显接口如下:

websocket_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()


@app.websocket("/ws")
async def ws(websocket: WebSocket):
    await websocket.accept()
    try:
        while True:
            text = await websocket.receive_text()  # 等待下一条消息,不占住事件循环。
            await websocket.send_text(f"echo: {text}")
    except WebSocketDisconnect:
        pass  # 客户端断开后,结束这条连接的处理。

客户端每发一条文本,服务器就回复一条带 echo: 前缀的文本,然后继续等待。Uvicorn 需要 WebSocket 协议实现,安装 uvicorn[standard] 会带上相关依赖。

Flask 通常用 Flask-Sock 等扩展,配合支持 WebSocket 的服务器。Flask-SocketIO 使用 Socket.IO 协议,需要匹配的客户端,不能直接当作普通 WebSocket 接口连接。

8.2 SSE

SSE(Server-Sent Events)用于服务器单向推送进度、日志或通知。 它保持一个 HTTP 响应,以 text/event-stream 格式发送事件。

下面的 Flask 和 FastAPI 接口都每隔约一秒发送一条消息,发完三条就结束响应。

Flask 通过生成器逐次返回消息:

sse_flask.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import time

from flask import Flask, Response

app = Flask(__name__)


@app.get("/events")
def events():
    def generate():
        for index in range(3):
            time.sleep(1)
            yield f"data: {index}\n\n"  # 空行标记一条 SSE 事件结束。

    return Response(generate(), mimetype="text/event-stream")
代码块 Shell
1
2
3
# 终端 1:停止此前的 5000 端口应用后启动。
flask --app sse_flask run --port 5000
# 输出:Running on http://127.0.0.1:5000
代码块 Shell
1
2
# 终端 2:-N 关闭 curl 的输出缓冲,逐条显示到达的消息。
curl -N http://127.0.0.1:5000/events
运行结果 Text
1
2
3
4
5
6
# data: 0
#
# data: 1
#
# data: 2
#

FastAPI 的异步生成器在等待时让出事件循环:

sse_fastapi.py Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import asyncio

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()


async def events():
    for index in range(3):
        await asyncio.sleep(1)
        yield f"data: {index}\n\n"


@app.get("/events")
async def sse():
    # StreamingResponse 逐次读取生成器,不等全部内容生成完再返回。
    return StreamingResponse(events(), media_type="text/event-stream")
代码块 Shell
1
2
3
# 终端 1:停止此前的 8000 端口应用后启动。
uvicorn sse_fastapi:app --port 8000
# 输出:Uvicorn running on http://127.0.0.1:8000
代码块 Shell
1
2
# 终端 2:请求流式响应。
curl -N http://127.0.0.1:8000/events
运行结果 Text
1
2
3
4
5
6
# data: 0
#
# data: 1
#
# data: 2
#

data: 后面是消息内容,空行分隔事件。输出块里的 # 仅用于标注运行结果,不属于 SSE 消息。

两版返回相同内容,等待时占用的资源不同:

运行方式等待消息期间
Flask,单线程同步 worker整条响应占住一个 worker
Flask,多线程占住一个工作线程,其他线程继续处理请求
Flask,gevent配合兼容的 I/O 调用,通过协程处理多个长连接
FastAPI 异步生成器await 时让出事件循环,其他请求继续执行

X-Accel-Buffering: no 是应用返回给 nginx 的响应头,表示这次响应不要缓冲,收到数据就转发。

nginx 位于应用和客户端之间;开启响应缓冲时,SSE 消息可能在 nginx 暂存,客户端就看不到逐条到达的效果。

这个响应头由 nginx 识别,浏览器不用处理。若 nginx 配置为忽略它,就在该接口对应的 location 中设置 proxy_buffering off;。

  • 长时间没有消息时,定期发送 : ping\n\n,让连接保持活跃。
  • 生成器持有的订阅或连接在 finally 中释放,并检查客户端断开连接时能否完成清理。

客户端和服务器都需要持续主动发消息时,例如协作编辑,用 WebSocket 更直接。

9. 部署

Flask 使用 Gunicorn:

代码块 Shell
1
2
gunicorn -w 4 flask_app:app
# 输出:Listening at: http://127.0.0.1:8000

FastAPI 使用 Uvicorn:

代码块 Shell
1
2
uvicorn fastapi_app:app --workers 4
# 输出:Uvicorn running on http://127.0.0.1:8000

两条命令都占用 8000 端口,试跑时分别启动。

对比项Gunicorn 命令Uvicorn 命令
工作进程数-w 4,四个进程--workers 4,四个进程
每个进程的执行方式默认同步 worker,同一时间处理一个请求各自运行事件循环;同步路由交给线程池

进程数增加,内存和数据库连接的占用也会增加。 接口慢在数据库查询时,应检查慢查询和连接池,不能只靠增加 worker。

反向代理需要配置转发、健康检查和日志;WebSocket、SSE 还要检查超时和响应缓冲。

10. 框架选型

项目大小不能单独决定选型。Flask 能用于大型服务,FastAPI 也适合小型 API。

简单接口即使主要调用同步库,两者也都能胜任。

主要需求优先考虑
已有 Flask 项目、依赖其扩展,或团队更熟悉 FlaskFlask
需要自动参数校验和 API 文档,或有大量并发 I/O 请求且使用异步库FastAPI

高并发场景下,要区分 I/O 等待和 CPU 计算。 请求主要在等数据库或外部接口时,异步 I/O 能利用这段等待处理其他请求;如果主要耗在 CPU 计算上,换框架不会自动提速。

评论

使用 GitHub 登录后参与讨论,评论将公开保存在 GitHub。

阅读到这里时加载评论。