全书目录

第四十三篇:Actix Web 完全指南 —— 高性能分流要塞宇宙

第四十三篇:Actix Web 完全指南 —— 高性能分流要塞宇宙

22 分钟 9,640 字 第 808 / 962 个阅读单元

Actix Web,可以把它想成一座专门应对高并发车流的“分流要塞”。

这座要塞不是那种什么都包办的超级帝都,也不是一个只会裸接 TCP 的钢筋地基。它更像一套已经修好的高速城防系统:

  • 城门很多,但进城规则极严
  • 每条车道都有人值守,但值守方式极轻
  • 每个请求一进门,就会被迅速分流、核验、取件、送往对应处理官
  • 处理官在等外部资源时,不会傻站在门口堵路
  • 真正需要共享的仓库会被严格上锁,不需要共享的就分给各个守备队自己保管

这就是 Actix Web 的气质:高性能、强类型、偏工程实战、不是玩具,也不是“全家桶神框架”。

#一、为什么会有这座要塞

Rust 世界需要 Actix Web,本质上是因为传统 Web 服务有一个长期矛盾:

  • 你想要高吞吐、低延迟
  • 你又不想在并发、内存、安全、类型上靠“自觉”
  • 你还希望写出来不是纯底层管道,而是一个可维护的服务框架

如果只追求底层控制,你可以直接去用 hyper 这一类更底层的 HTTP 引擎;但那更像你自己在荒地上砌城墙。

Actix Web 的存在意义,就是把 Rust 的 async/await、Tokio 运行时、类型系统、服务抽象,组织成一座能直接投入生产的分流要塞。

它解决的不是“怎么发一个 Hello World 响应”,而是这些更现实的问题:

  • 大量请求怎么稳稳接住
  • 路由怎么组织
  • 参数怎么安全提取
  • 中间件怎么插
  • 错误怎么统一出城
  • 状态怎么共享而不把吞吐打烂
  • 异步等待怎么不堵住工作线程

#二、Rust Web 里的 Actix Web,站在什么位置

如果把 Rust Web 世界看成一片交通基建带,大致可以这样理解:

  • hyper:发动机和底盘
  • tower:标准化的管线接口
  • axum:更偏 Tower 原教旨的高速公路系统
  • actix-web:更像一座已经修好城门、检查口、分流道、登记厅的高性能要塞

Actix Web 到 2026-03-28 的稳定认知有两个重点:

  1. 它是 Rust Web 主流路线之一。
  2. 今天别再把它理解成“actor 框架外壳”。

早期很多人一提 Actix 就想到 actor,但 Actix Web 官方文档已经明确:Actix Web 现在与 actor 框架已经大体分离。你今天学 Actix Web,主线心智不是 actor,而是:

  • HttpServer
  • App
  • worker
  • route
  • extractor
  • middleware
  • ResponseError
  • Tokio 上的异步执行

所以,现代 Actix Web 的正确打开方式不是“我先学 actor 模型”,而是“我先学这座要塞怎么分流”。

#三、最重要的心智模型:HttpServer、App、worker 到底谁是谁

这一段如果你吃透了,后面 70% 的 Actix Web 都会顺。

把它们想成下面这套结构:

text
外部请求车流
      |
      v
[ HttpServer 总分流塔 ]
      |
      | 监听端口 / 接收连接 / 分配到值守队
      |
      +-------------------+-------------------+-------------------+
      |                   |                   |
      v                   v                   v
[ worker 1 ]         [ worker 2 ]         [ worker N ]
单线程守备队          单线程守备队          单线程守备队
      |                   |                   |
      v                   v                   v
 [ App 实例 ]          [ App 实例 ]          [ App 实例 ]
 城内规则册            城内规则册            城内规则册
      |
      v
中间件走廊
      |
      v
路由分发台
      |
      v
extractor 登记官
      |
      v
handler 处理官
      |
      +------> 本地/共享状态仓库
      +------> 数据库 / Redis / MQ / 外部 API
      |
      v
响应出城

现在分别看:

#1. HttpServer:总分流塔

HttpServer 负责:

  • 监听端口
  • 接受连接
  • 启动多个 worker
  • 把流量分配给 worker
  • 管理优雅停机、超时、TLS 等外围能力

它不是“业务本体”,它是要塞的总调度塔。

#2. worker:值守守备队

Actix Web 默认会启动多个 HTTP worker。官方文档给出的稳定心智是:

  • 默认 worker 数接近机器可用并行度
  • 每个 worker 是独立线程
  • 每个 worker 都会拿到自己的一份 App 实例
  • 每个 worker 内部跑的是单线程异步运行时

这句话非常关键。

很多人一看到“多 worker”就以为每个请求都在一个巨大的共享线程池里乱窜。不是。Actix Web 更像是:

  • 城门外有总调度
  • 城内有多支守备队
  • 每支守备队各守自己那条线
  • 同一支队内部处理大量异步任务,但这条线本身是单线程值守

这带来两个非常重要的结果:

  • worker 内部不共享的状态,心智更简单
  • 想跨 worker 共享状态时,你才需要显式引入同步代价
#3. App:每个 worker 手里那本城内规则册

App 不是整个服务器唯一的全局单例。

这是 Actix Web 初学者最容易踩的坑。

App 里通常放:

  • 路由
  • scope
  • middleware
  • app data
  • 各种 service 配置

但要记住:HttpServer::new(|| App::new()...) 这个闭包,是“造规则册的工厂”。worker 起几个,App 就会被实例化几份;如果你绑定多个地址,实例化次数还会更多。

所以正确理解是:

  • HttpServer = 总分流塔
  • App = 每个 worker 自己持有的一本规则册
  • worker = 真正值守处理请求的守备队

#四、路由:这座要塞怎么决定“你该去哪个门”

Actix Web 的路由就是城内道路系统。

最常见的几种组织方式:

  • App::route():快速挂单条路线
  • App::service():挂资源
  • web::scope():给一大片区域加统一前缀
  • 宏路由:#[get]#[post] 这类声明式门牌

一个很实用的心智是:

  • App:整座要塞
  • scope("/api"):东城 API 区
  • resource("/users/{id}"):某个具体城门
  • route(web::get().to(handler)):这个城门上哪种请求由谁接待

例如:

rust
use actix_web::{web, App, HttpServer, HttpResponse};

async fn health() -> HttpResponse {
    HttpResponse::Ok().body("ok")
}

async fn get_user(path: web::Path<(u64,)>) -> HttpResponse {
    let user_id = path.into_inner().0;
    HttpResponse::Ok().body(format!("user {}", user_id))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/api")
                    .route("/health", web::get().to(health))
                    .route("/users/{id}", web::get().to(get_user))
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

你要把路由匹配想成“要塞门牌匹配”:

  • 先看前缀区块
  • 再看资源路径
  • 再看方法和 guard
  • 第一个匹配到的就停下,不会继续往后找

所以路由顺序、资源顺序、guard 条件,都会影响最终命中。

#五、extractor:登记官为什么是 Actix Web 的灵魂

Actix Web 最有代表性的机制之一,就是 extractor。

请求到了城门口,处理官不应该自己在一大坨原始请求里手扒:

  • 路径参数
  • 查询参数
  • JSON body
  • 表单
  • header
  • 状态对象

而应该由登记官提前把材料分类装订好,再交给处理官。

这就是 extractor。

常见 extractor:

  • web::Path<T>:路径参数
  • web::Query<T>:查询参数
  • web::Json<T>:JSON 请求体
  • web::Form<T>:表单
  • web::Data<T>:应用状态
  • HttpRequest:原始请求对象
  • Bytes / String / Payload:更底层的 body 读取方式

看一个典型例子:

rust
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct UserPath {
    id: u64,
}

#[derive(Deserialize)]
struct UserQuery {
    verbose: Option<bool>,
}

#[derive(Serialize)]
struct UserView {
    id: u64,
    name: String,
    verbose: bool,
}

async fn get_user(
    path: web::Path<UserPath>,
    query: web::Query<UserQuery>,
) -> impl Responder {
    let path = path.into_inner();
    let query = query.into_inner();

    web::Json(UserView {
        id: path.id,
        name: "fortress-user".into(),
        verbose: query.verbose.unwrap_or(false),
    })
}

这段代码的意思不是“框架在帮你偷懒”,而是:

  • 路径参数先由登记官提取
  • 查询参数先由登记官提取
  • 处理官拿到的已经是类型安全的材料

这会带来三个巨大的工程收益:

  • 参数解析逻辑不散落
  • 类型错误尽早暴露
  • handler 只写业务,不写拆包流水账

不过这里有一个非常重要的规则

会消费 body 的 extractor,通常只有第一个能成功读取。

也就是说你不能理直气壮地同时写:

  • 一个 web::Json<T>
  • 再来一个 Bytes

因为 body 流不是无限复制的。城门口的材料包被第一个登记官拆走了,后面的人就拿不到原件了。

#六、state:仓库该放哪里,决定这座要塞是快还是堵

Actix Web 的状态管理,核心不是“能不能共享”,而是“你到底该不该共享”。

先记最重要的一条:

如果你想在多个 worker 之间共享状态,web::Data 通常要在 HttpServer::new 外面构造。

因为 Data 内部是基于 Arc 的,克隆成本很低,适合把同一个共享仓库分发给各个守备队。

看一个完整点的例子:

rust
use std::sync::atomic::{AtomicU64, Ordering};

use actix_web::{
    http::StatusCode,
    middleware::{Logger, NormalizePath},
    web, App, HttpResponse, HttpServer, ResponseError,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;

struct AppState {
    app_name: String,
    request_count: AtomicU64,
}

#[derive(Debug, Deserialize)]
struct UserPath {
    id: u64,
}

#[derive(Debug, Deserialize)]
struct UserQuery {
    page: Option<u32>,
}

#[derive(Debug, Deserialize)]
struct CreateUser {
    name: String,
}

#[derive(Debug, Serialize)]
struct UserView {
    id: u64,
    name: String,
    page: u32,
    served_by: String,
}

#[derive(Debug, Error)]
enum ApiError {
    #[error("user not found")]
    NotFound,
    #[error("invalid input: {0}")]
    BadRequest(String),
}

impl ResponseError for ApiError {
    fn status_code(&self) -> StatusCode {
        match self {
            Self::NotFound => StatusCode::NOT_FOUND,
            Self::BadRequest(_) => StatusCode::BAD_REQUEST,
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code()).body(self.to_string())
    }
}

async fn health(state: web::Data<AppState>) -> HttpResponse {
    let total = state.request_count.load(Ordering::Relaxed);
    HttpResponse::Ok().body(format!("{} alive, requests={}", state.app_name, total))
}

async fn get_user(
    path: web::Path<UserPath>,
    query: web::Query<UserQuery>,
    state: web::Data<AppState>,
) -> Result<web::Json<UserView>, ApiError> {
    state.request_count.fetch_add(1, Ordering::Relaxed);

    if path.id == 404 {
        return Err(ApiError::NotFound);
    }

    Ok(web::Json(UserView {
        id: path.id,
        name: "fortress-user".into(),
        page: query.page.unwrap_or(1),
        served_by: state.app_name.clone(),
    }))
}

async fn create_user(
    payload: web::Json<CreateUser>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ApiError> {
    state.request_count.fetch_add(1, Ordering::Relaxed);

    if payload.name.trim().is_empty() {
        return Err(ApiError::BadRequest("name cannot be empty".into()));
    }

    Ok(HttpResponse::Created().body(format!("created {}", payload.name)))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    let state = web::Data::new(AppState {
        app_name: "fortress-api".into(),
        request_count: AtomicU64::new(0),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .wrap(Logger::default())
            .wrap(NormalizePath::trim())
            .service(
                web::scope("/api")
                    .route("/health", web::get().to(health))
                    .route("/users/{id}", web::get().to(get_user))
                    .route("/users", web::post().to(create_user))
            )
    })
    .workers(std::thread::available_parallelism()?.get())
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

这里你应该看到几个关键点:

  • stateHttpServer::new 外面构造,表示它是真共享仓库
  • handler 里通过 web::Data<AppState> 取状态
  • 高频计数用 AtomicU64,避免无脑上大锁
  • 错误通过 ResponseError 统一出城

还要再补一条高级但非常实用的认知:

Actix Web 的路由层会“就近取 state”。

也就是:

  • App 上挂了一份 Data<T>
  • 某个 scope 又挂了一份同类型 Data<T>

那么进入这个 scope 的请求,会优先拿到“更近”的那一份。就像东城区先用东城仓库,不会回中央总仓。

#七、middleware:护城河、安检廊、审计官

如果说 extractor 是“登记官”,那 middleware 就是“沿路检查设施”。

它能做的事包括:

  • 请求前置校验
  • 日志
  • 默认响应头
  • 路径规范化
  • 会话
  • 错误响应加工
  • 指标统计
  • 提前拦截直接返回

Actix Web 的 middleware 一个必须记住的规则是:

注册顺序和执行顺序相反。后 wrap() 的,先执行。

这就像你穿过多道安检廊,最后装上的那道门,最先拦住来人。

官方已经把很多常用设施准备好了:

  • Logger
  • NormalizePath
  • DefaultHeaders
  • Compress
  • ErrorHandlers

而且现在写简单中间件时,不必一上来就硬啃 Transform/Service,可以直接用 from_fn

例如一个最小自定义中间件:

rust
use actix_web::{
    body::MessageBody,
    dev::{Service, ServiceRequest, ServiceResponse},
    middleware::{from_fn, Next},
    App, Error,
};

async fn timing_mw(
    req: ServiceRequest,
    next: Next<impl MessageBody>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let started = std::time::Instant::now();
    let res = next.call(req).await?;
    println!("request took: {:?}", started.elapsed());
    Ok(res)
}

// App::new().wrap(from_fn(timing_mw));

把 middleware 想成要塞里的几层设施最容易理解:

  • Logger:审计官
  • NormalizePath:纠偏路牌
  • 认证中间件:身份安检门
  • 压缩中间件:出城打包站
  • ErrorHandlers:事故公关办

#八、错误处理:事故不是不能发生,而是不能乱响警报

Actix Web 的错误处理主心智,不是“到处 unwrap”,而是:

  • handler 正常返回响应
  • 出问题时返回 Result<T, E>
  • E 尽量实现 ResponseError
  • 框架根据你的错误类型,自动决定 HTTP 状态码和响应内容

这就是为什么 ResponseError 很重要。它像要塞里的事故分级手册:

  • 这是 400,说明来访材料不合规
  • 这是 404,说明你找的门不存在
  • 这是 401/403,说明权限不够
  • 这是 500,说明城内设备故障,但不该把内部细节全喊给外面听

成熟项目里最好把错误分成两层:

  • 对内错误:日志里保留完整上下文
  • 对外错误:只暴露安全、可理解的信息

Actix Web 也会按官方规则记录错误日志。默认错误会进入日志;调高日志级别并开启回溯时,排障信息会更完整。

#九、异步与性能来源:这座要塞为什么快

很多人说 Actix Web 快,但“快”不能只背结论,要知道它快在哪。

把请求处理想成“值守官处理来车”:

  • 如果值守官查库存要 20ms,他不该站在门口发呆 20ms
  • 正确做法是把这个等待挂起,让这条线先处理别的车
  • 等库存回来了,再接着办这单

这就是异步 I/O 的本质。

Actix Web 的性能来源,主要不是某个神秘魔法,而是几层结构一起生效:

#1. Rust 原生可执行产物
  • 没有解释器层
  • 没有运行时反射式 DI 的大额开销
  • 没有 GC 停顿这一类常见尾延迟来源
#2. 跑在 Tokio 上

Actix Web 官方文档明确:Actix Web 运行在 Tokio 上。

这意味着:

  • 你能接入大量 Tokio 生态
  • 你可以使用成熟的 async 数据库/网络库
  • 异步等待不会白白堵住 worker 线程
#3. worker 模型很清楚

每个 worker 单线程值守,减少了“同一路请求在多个线程之间乱跳”的协调复杂度。不是没有并发,而是把并发组织得很有秩序。

#4. 尽量少共享,必须共享时显式付费

每个 worker 拿自己那份 App,天然避免了一部分无意义共享。你真的要共享时,再通过 Data、原子类型、锁来付代价。

这比“默认万物全局共享”更容易保住吞吐。

#5. 类型驱动的参数提取与响应

请求怎么拆、响应怎么出,很多都在编译期被约束住了。运行时少猜、少反射、少动态兜底,路径就更短。

不过一定要记住:

Actix Web 快,不代表你写什么都快。

下面这些东西会把要塞自己堵死:

  • 在 async handler 里直接跑阻塞 I/O
  • 长时间持有 Mutex
  • 持锁跨 await
  • 用同步数据库驱动硬塞进 async 路线
  • 把所有请求都去抢一个全局大锁
  • 大量 CPU 密集任务直接塞在请求线程里

Actix Web 快的前提是:你配得上它的分流模型。

#十、一个完整请求,在要塞里到底怎么走

把一次请求流程串起来,你就会更稳:

text
客户端发起 GET /api/users/7?page=2
        |
        v
HttpServer 接到连接
        |
        v
分配给某个 worker
        |
        v
进入这个 worker 的 App 实例
        |
        v
经过 middleware
后 wrap 的先执行
        |
        v
路由匹配 /api/users/{id}
        |
        v
extractor 提取:
- Path<UserPath>  => id = 7
- Query<UserQuery> => page = 2
- Data<AppState>  => 共享仓库句柄
        |
        v
handler 执行业务
        |
        +--> 需要查 DB / Redis / 外部 API 时 await
        |
        v
返回 Json / HttpResponse / Result<T, E>
        |
        v
若 E 实现了 ResponseError,则自动转 HTTP 响应
        |
        v
响应再穿过 middleware 出城

你会发现,Actix Web 其实没有那么玄学。它只是把每一层职责都切得很干净。

#十一、常见误区:很多人不是不会用,是把要塞想错了

#误区 1:把 App 当成全局唯一单例

错。App 是由 HttpServer::new 的工厂闭包按 worker 实例化出来的。

后果是:

  • 你把计数器写在闭包里面,以为全局共享,结果其实每个 worker 一份
  • 你把初始化写错位置,结果行为和预期完全不同
#误区 2:所有 state 都塞进 Mutex<HashMap<...>>

这是最经典的“把中央总仓修成唯一瓶颈”。

能不用共享可变状态,就别共享。 能用原子类型,就别大锁。 真要上锁,也尽量缩短锁持有时间,别拿着锁过 await

#误区 3:在 async handler 里跑阻塞代码

例如:

  • 同步文件 I/O
  • 同步数据库客户端
  • std::thread::sleep
  • 大量 CPU 运算直接硬算

这会直接堵住 worker。值守官被一辆重卡卡在门口,后面的车全排长队。

#误区 4:以为 extractor 可以随便重复读取 body

不行。谁先拆包,谁先拿走材料。 如果你既想校验 body,又想保留原始字节,要专门设计这条链路。

#误区 5:middleware 顺序想反了

wrap(A).wrap(B).wrap(C),通常是 C 先入,A 后入。 别把安检门顺序装反。

#误区 6:用了 NormalizePath 却还乱定义尾斜杠路由

路径规范化和你的路由定义风格要统一。 否则会出现“看着像有门,实际上永远进不去”的怪现象。

#误区 7:还把 WebSocket 主线理解成旧 actor 路子

到 2026 的稳定主心智里,WebSocket 的现代高层路径是 actix-ws,不是继续把旧 actor 方式当默认主路。

#十二、Actix Web 适合什么,不适合什么

#适合的场景
  • 高并发 JSON API
  • 网关、BFF、聚合层
  • webhook 接入站
  • 内部基础服务
  • I/O 密集型服务
  • 对尾延迟和资源占用比较敏感的系统
  • 团队本身就接受 Rust 的工程门槛

这类场景里,Actix Web 像一座特别合适的分流要塞:入口规矩多,但进来后秩序非常好。

#不太适合的场景
  • 团队主要是 Python/TS,目标是最快业务试错
  • 你想要像 NestJS 一样很重的模块化 DI 组织感
  • 你想要像 FastAPI 那样极快起手并自动长出 OpenAPI 体验
  • 你的业务是大量 CPU 密集计算,却又不准备做任务拆分或离线化
  • 你主要跑的是边缘函数/非长期驻留服务器环境

Actix Web 不是不能做这些,而是它不是这些场景的默认最优武器

#十三、和 Axum、NestJS、FastAPI、Express 怎么看

这一段不要用“谁吊打谁”的思路,要用“哪座城市适合哪种交通系统”的思路。

框架 最像什么 核心心智 默认优势 默认代价
Actix Web 高性能分流要塞 HttpServer + worker + App + extractor 吞吐强、控制力强、成熟实战感强 Rust 学习曲线、工程门槛高
Axum 标准件高速公路系统 Router + Tower Service + State Tokio/Tower 生态贴合度高、抽象统一 更偏原教旨,类型组合感更重
NestJS 政务大楼总包 Module + Controller + Provider + DI 企业组织性强、规范感强 运行时层更重
FastAPI 智能报关大厅 类型注解 + 依赖注入 + OpenAPI 开发快、文档体验极好 Python 在极高并发和 CPU 密集上先天不占优
Express 路边轻岗亭 req / res / next 轻、自由、上手快 大项目秩序靠团队自觉

再把重点掰开说:

#Actix Web vs Axum

这是 Rust 圈最常见的一组比较。

  • Actix Web 更像“成型要塞”
    • 有自己比较鲜明的 worker 心智
    • extractor、middleware、路由组织都很完整
    • 实战手感更像成熟框架
  • Axum 更像“标准化高速公路”
    • 跟 Tower、Hyper、Tokio 的抽象咬合更紧
    • 很适合喜欢标准 service/layer 体系的人
    • 对很多 Rust 基础抽象更友好,也更“硬核”

如果你喜欢“框架已经把城门修好”,Actix Web 很顺手。 如果你喜欢“整个系统都建立在统一标准件之上”,Axum 很顺手。

#Actix Web vs NestJS
  • NestJS 赢在组织治理、模块化、依赖注入、企业团队协作体验
  • Actix Web 赢在原生性能、资源效率、并发安全边界

NestJS 像一栋制度严明的大楼。 Actix Web 像一座吞吐极强的城防系统。

#Actix Web vs FastAPI
  • FastAPI 的最大优势是开发速度、文档体验、依赖注入和 Python 生态
  • Actix Web 的最大优势是吞吐、内存效率、并发控制和编译期约束

FastAPI 很适合快速建报关大厅。 Actix Web 更适合把报关大厅修成抗高峰冲击的钢筋堡垒。

#Actix Web vs Express

这俩其实不是一个重量级。

  • Express 的魅力在于轻和自由
  • Actix Web 的魅力在于稳和快

Express 像一个路边岗亭,搭起来快,扩起来全靠经验。 Actix Web 像一座专业交通枢纽,修起来讲究,但成了以后不容易塌。

#十四、收尾:真正学会 Actix Web,不是会写路由,而是脑子里有要塞图

如果你学到最后,只记住一句话,那就记住这句:

Actix Web 是一座把请求分流、材料提取、状态访问、异步等待、错误出城都组织得很严格的高性能要塞。

它为什么存在?

  • 因为 Rust 需要一个既快又能直接干活的 Web 框架。

它在 Rust Web 里是什么位置?

  • 是主流生产级路线之一,偏高性能、偏工程实战,不再以 actor 作为默认主线理解。

它最核心的心智模型是什么?

  • HttpServer 是总分流塔
  • worker 是守备队
  • App 是每支守备队手里的规则册
  • route 是道路系统
  • extractor 是登记官
  • state 是仓库
  • middleware 是安检廊和审计官
  • ResponseError 是事故分级手册

你只要把这张要塞图装进脑子里,后面不管是接数据库、做鉴权、加日志、做网关、加 WebSocket、接 Redis,都会变成“在既有城防系统上继续修设施”,而不是“每次都重新理解一遍框架”。

这就是 Actix Web 最强的地方。

它不是让你觉得“好像很高级”。

它是让你在高流量真正来袭时,知道这座城为什么扛得住。

#官方来源链接清单