第8章:REST 服务:pistache 实战

把 C++ 程序变成一台能收发请求的服务器——用 pistache 摆好这盘 REST 的棋

🪶

本章导师:诸葛亮

核心方法论:运筹帷幄

「用兵之道,在于算无遗策。写一个网络服务也一样:请求从哪里进来、参数怎么取、错误怎么回、服务怎么收场——把这些都提前想清楚,代码自然四平八稳。这一章,我们借 pistache 这盘棋,把 REST 服务的每一步都摆给你看。」

8.1 零基础铺垫:REST 是什么,为什么 C++ 需要框架

REST(Representational State Transfer,表述性状态转移)是一种设计网络接口的风格——说人话:它不是一种协议,而是一套约定,规定"程序之间该怎么礼貌地互相请求数据"。核心思想是把服务器上的东西都看作资源(resource)——一个用户、一篇文章、一台设备都是资源,每个资源有一个地址(URL);而操作资源的方式,就是 HTTP 自带的几个方法(method)。REST 这个词是计算机科学家 Roy Fielding 在 2000 年的博士论文里提出的,如今绝大多数 Web API 都遵循这套风格。

HTTP 方法有十来个,REST 常用的就四个:GET(读取资源)、POST(新建资源)、PUT(整体更新资源)、DELETE(删除资源)——正好对应程序里的"查、增、改、删"。这样设计的好处是:URL 负责"说清楚操作哪个资源",方法负责"说清楚干什么",两者一组合,接口的语义就明确了。比如 GET /users/42 读用户 42,DELETE /users/42 删掉用户 42,一看便知,不需要额外解释。下面这张表是 REST 服务的"四字兵法":

HTTP 方法操作说人话例子
GET读取资源看用户列表、看某条记录
POST新建资源注册新用户、发一条新消息
PUT整体更新资源把用户资料整个换掉
DELETE删除资源注销账号、删掉一条记录

那问题来了:C++ 为什么需要框架来做这件事?因为"接收 HTTP 请求"这件事远比看起来复杂。一个请求到了服务器,程序要解析请求行(方法、URL、协议版本)、解析各种请求头(Content-Type、Cookie……)、读取请求体、处理 URL 编码;响应时又要拼状态行、拼响应头、算好内容长度;这还只是"单个请求"。真实场景里同时有几百个连接进来,你得处理并发;连接超时了要清理;服务要能优雅关闭……这些底层细节自己写一遍,工作量不亚于写半个操作系统。下面的"错误示范"只演示了第一步——解析请求行——你就知道有多琐碎了。

# 客户端视角:用 curl 发 HTTP 请求(REST 的调用方式)
# GET:读取资源 —— 查用户 42
curl http://127.0.0.1:9080/users/42

# POST:新建资源 —— 注册一个用户(数据放在请求体里)
curl -X POST -H "Content-Type: application/json" \
     -d '{"name":"孔明","alive":false}' http://127.0.0.1:9080/users

# PUT:整体更新 —— 把用户 42 的资料整个换掉
curl -X PUT -d '{"name":"卧龙"}' http://127.0.0.1:9080/users/42

# DELETE:删除资源 —— 注销用户 42
curl -X DELETE http://127.0.0.1:9080/users/42
// 错误示范:不借助框架,手写 HTTP 服务的第一步就得自己解析请求行
std::string line = "GET /users/42 HTTP/1.1";
auto p1 = line.find(' ');
std::string method = line.substr(0, p1);          // "GET"
auto p2 = line.find(' ', p1 + 1);
std::string path = line.substr(p1 + 1, p2 - p1 - 1); // "/users/42"
// 这才拆完第一行。后面还有请求头、请求体、URL 编码、粘包、超时……
// 更别说并发:来一个连接开一个线程?线程池?服务怎么优雅关闭?
诸葛亮提示

如果你已经忘了第 6 章的 jsoncpp——没关系,本章 8.6 会用到它来拼 JSON 响应,用到时我会点明对应的小节。这两章是天然的搭档:jsoncpp 负责"数据的模样",pistache 负责"数据的运输"。

8.2 pistache 是什么:一个为 C++ 而生的 REST 框架

pistache 是一个用 C++ 写的 HTTP 与 REST 框架——说人话:它把你上节看到的那些底层活(解析请求、管理连接、并发调度)都包好了,你只需要声明"哪个 URL 对应哪段业务代码"。pistache 最初由开发者 Mathieu Stefani 创建(GitHub 昵称 octal),代码以 Apache License 2.0 开源,官方定位是"modern and elegant"(现代、优雅):纯 C++17 编写,API 清晰,支持 Linux、macOS、Windows 和 BSD。素材笔记里作者对比了多个 C++ REST 库后选择了 pistache,理由是它在对比中性能表现突出。

有一个地址细节值得注意,也是本章素材与官网的唯一分歧点:素材笔记(2020 年)clone 的是 https://github.com/oktal/pistache.git——这是作者个人账号下的旧地址;如今项目已迁到 pistacheio 组织名下,官方仓库地址是 https://github.com/pistacheio/pistache。历史原因很简单:开源项目成长后,通常会从个人账号搬到组织账号,方便多人共同维护。老地址在 GitHub 上会重定向到新地址,但教学和日常使用请认准新地址。

pistache 的依赖也值得一提:它本身不重复造轮子,SSL 加密依赖 OpenSSL,性能关键路径用到了 libevent,构建时还能选择是否开启这些功能(对应构建选项 PISTACHE_USE_SSL 等,8.3 会看到)。仓库里的 examples/ 目录放着官方示例(比如 rest_server.cc 就是带路由的 REST 服务),是学习 API 的第一手资料。项目至今还没到 1.0 版本,官方 README 的说法是"unstable but not unusable"——不稳定但不难用,大部分代码已经达到生产可用级别,用来学习完全没问题。

# 官方仓库地址(pistacheio 组织,现在维护的地址)
git clone https://github.com/pistacheio/pistache.git
cd pistache

# 历史说明:2020 年素材笔记里 clone 的是旧地址
# git clone https://github.com/oktal/pistache.git  (作者个人账号)
# 项目迁到 pistacheio 组织后,官方新地址如上,认准新的
# clone 之后,仓库里的关键目录长这样:
include/pistache/   # 头文件:http.h  endpoint.h  router.h  http_defs.h ...
src/                # 库的实现源码
examples/           # 官方示例,rest_server.cc 是带路由的 REST 服务
tests/              # 单元测试
subprojects/        # 构建时可能用到的第三方依赖(由构建系统处理)

# 主要第三方依赖:OpenSSL(SSL 支持)、libevent(网络事件循环)等
# 构建选项 -DPISTACHE_USE_SSL=true 就是开启 OpenSSL 支持(见 8.3)

8.3 编译安装:clone + submodule + CMake 构建

pistache 需要自己编译安装,素材笔记记录了一次完整的踩坑过程,命令可以直接照抄。第一个坑是编译器版本:当时的系统 GCC 还是 4.8(只支持到 C++11),而 pistache 需要支持 C++14 的编译器,所以笔记里先通过源码编译把 GCC 升级到了 5.1.0。由于系统里同时存在多个 GCC,直接用 g++ 可能还是老版本,需要用环境变量把编译器显式指到新装的版本——这正是第 4 章讲过的 CC / CXX 环境变量。如今 pistache 主线已经是纯 C++17,编译器要求随之上调(GCC 7 及以上稳妥),但"用环境变量固定编译器"的思路完全不变。

第二个坑是依赖不全:pistache 构建需要 curl 的开发头文件,笔记里用 yum install curl-devel -y 补齐;CMake 版本也不能太老(笔记要求 3.18 以上)。第三个坑是第三方子模块:素材时代 clone 之后还要执行 git submodule update --init 拉取仓库里引用的其他 git 仓库——子模块(submodule)说人话就是"一个 git 仓库里挂着另一个 git 仓库的引用",拉下来才能编。现在的 pistacheio 仓库已改为由构建系统处理第三方依赖(subprojects/ 目录),这步通常可以跳过;但了解它,遇到老版本或老教程时不至于发懵。

# 素材笔记:CentOS 环境,先装 curl 开发包(pistache 依赖它)
sudo yum install curl-devel -y

# 当年系统 GCC 4.8 只支持 C++11,素材里源码编译升级到 GCC 5.1.0(支持 C++14)
# 多版本并存时,用环境变量把默认编译器指到新装的版本(第 4 章的技能):
export CC=/usr/local/bin/gcc
export CXX=/usr/local/bin/g++
g++ --version   # 确认显示 gcc version 5.1.0 或更高

# CMake 也要够新(素材要求 >= 3.18),cmake.org 下载安装三部曲即可
cmake --version
# 克隆 + 第三方子模块(素材原样;新仓库通常可省略第二步)
git clone https://github.com/pistacheio/pistache.git
cd pistache
git submodule update --init
# 素材笔记的完整构建流程(2020 年实测通过,命令原样保留)
mkdir build && cd build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release \
      -DPISTACHE_BUILD_EXAMPLES=true \
      -DPISTACHE_BUILD_TESTS=true \
      -DPISTACHE_BUILD_DOCS=false \
      -DPISTACHE_USE_SSL=true \
      -DCMAKE_INSTALL_PREFIX=/usr/local/pistache ..
make -j 4
sudo make install

# 验证安装结果:头文件与库文件都在 /usr/local/pistache 下
ls /usr/local/pistache/include/pistache/
# 应看到 http.h  endpoint.h  router.h  http_defs.h ...
ls /usr/local/pistache/lib/
# 应看到 libpistache.so(动态库)和 libpistache.a(静态库)

注意:cmake 配置里的几个 PISTACHE_* 开关各有用途——PISTACHE_BUILD_EXAMPLES=true 会连官方示例一起编译(强烈建议开着,示例是最好的学习材料),PISTACHE_BUILD_TESTS=true 编译单元测试,PISTACHE_BUILD_DOCS=false 跳过文档生成(省时间),PISTACHE_USE_SSL=true 开启 HTTPS 支持(需要系统里有 OpenSSL)。如果你的系统没有 OpenSSL 开发包,把最后一个关掉也能编过。

8.4 核心 API:Endpoint / Router / Code,跑起第一个服务

装好之后,我们来认识 pistache 的四个核心角色。Http::Endpoint(端点)是服务器的"门面":它负责绑定地址和端口、启动监听、管理线程池。流程固定三步:init() 初始化(可传选项,比如线程数)→ setHandler() 挂上处理器 → serve() 开始服务。这里的端口(port)说人话就是"服务器大门上的编号",客户端靠 IP 找到机器、靠端口找到服务,9080 是 pistache 示例里的常用端口。绑定用的 Address(Ipv4::any(), Port(9080)) 表示"监听本机所有 IPv4 地址的 9080 端口"。

第二个角色是Rest::Router(路由器)——说人话就是一张"URL 分发表":注册时告诉它"哪个路径用哪个函数处理",请求进来它自动分发。router.get("/hello", handler) 注册 GET 方法,router.post(...)router.put(...)router.del(...) 同理;官方也提供等价的 Routes::Get(router, ...) 系列自由函数。第三个角色是处理器(handler):一段接收请求、返回响应的函数,签名固定为 void(const Rest::Request&, Http::ResponseWriter)——请求信息在第一个参数里读,用第二个参数 response.send(...) 把结果发回去。第四个角色是 Http::Code:HTTP 状态码的枚举,Http::Code::Ok 就是 200(成功),常见还有 Created(201 创建成功)、Bad_Request(400 请求不合法)、Not_Found(404 找不到)、Internal_Server_Error(500 服务器内部错误)——这些是 http_defs.h 里的真实定义,全部与标准状态码一一对应。

下面是最小的 REST 服务:只注册一个 GET /hello 路由,用 lambda 当处理器。注意 lambda 的形参 request 这里用不到,可以省略名字(类型保留)。

// hello_router.cpp:用 Router 注册路由的最小 REST 服务
#include <pistache/endpoint.h>
#include <pistache/http.h>
#include <pistache/router.h>

using namespace Pistache;

int main() {
    // 1. 建 Router:路由表,负责把 URL 分发给对应的处理函数
    Rest::Router router;

    // 2. 注册路由:GET /hello → 返回一段文本
    router.get("/hello",
              [](const Rest::Request&, Http::ResponseWriter response) {
                  response.send(Http::Code::Ok, "Hello, REST!");
              });

    // 3. 创建服务器端点(Endpoint),绑定本机所有 IPv4 地址的 9080 端口
    Http::Endpoint endpoint(Address(Ipv4::any(), Port(9080)));

    // 4. 初始化(2 个线程)→ 挂上路由处理器 → 开始服务
    auto opts = Http::Endpoint::options().threads(2);
    endpoint.init(opts);
    endpoint.setHandler(router.handler());
    endpoint.serve();
}
# 编译链接:-I 指向安装的头文件目录,-L/-lpistache 指向库
g++ -std=c++17 -I/usr/local/pistache/include hello_router.cpp \
    -L/usr/local/pistache/lib -lpistache -o hello_router

# 后台启动,然后用 curl 验证(& 让程序在后台跑)
./hello_router &
curl http://127.0.0.1:9080/hello
# 输出:Hello, REST!
// 更简单的官方写法:listenAndServe —— 不需要 Router,适合超小服务
#include <pistache/endpoint.h>
#include <pistache/http.h>

using namespace Pistache;

struct HelloHandler : public Http::Handler {
    HTTP_PROTOTYPE(HelloHandler)   // 必需的宏:让框架能构造这个处理器

    void onRequest(const Http::Request&, Http::ResponseWriter response) override {
        response.send(Http::Code::Ok, "Hello, World!");
    }
};

int main() {
    Http::listenAndServe<HelloHandler>("*:9080");
}

两种写法对比:Router 版适合"一个服务很多接口"的场景,URL 分发清晰;listenAndServe 版适合只有一个响应逻辑的极简场景。日常开发绝大多数用 Router 版——因为它天然支持多路由、路径参数(下一节的主角)和 404 兜底。

8.5 路由进阶:路径参数 :id 与 query 查询参数

真实接口很少是写死的路径,/users/42 里的 42 是运行时才决定的。pistache 用路径参数(path parameter)解决:注册时在路径里写 :id 这样的占位符,匹配时把 URL 对应位置的值取出来。取用方法是 request.param(":id").as<std::string>()——占位符名字要和注册时一致,as<T>() 把参数转成想要的类型(字符串、整数都行)。另一种传参方式是查询参数(query parameter),就是 URL 里 ? 后面的 key=value 对(如 /search?q=pistache),用 request.query().get("q") 读取——它返回 std::optional<std::string>,先判断有没有值再解引用,避免拿到空值。

还有一个常用的兜底:router.addNotFoundHandler(...) 注册一个处理器,当所有路由都没匹配上时返回 404——否则客户端会收到一个笼统的默认错误。把这三样凑一起,就是一个像模像样的用户查询服务:/users/:id 查单个用户,/search?q=... 搜索,其余路径一律 404。

// users.cpp:路径参数 :id + 查询参数 ?q= 的完整示例
#include <pistache/endpoint.h>
#include <pistache/http.h>
#include <pistache/router.h>
#include <string>

using namespace Pistache;

int main() {
    Rest::Router router;

    // 路径参数::id 会匹配 /users/42 里的 42
    router.get("/users/:id",
              [](const Rest::Request& request,
                 Http::ResponseWriter response) {
                  std::string id =
                      request.param(":id").as<std::string>();
                  response.send(Http::Code::Ok, "user id = " + id);
              });

    // 查询参数:/search?q=pistache 里的 q(& 连接多个参数)
    router.get("/search",
              [](const Rest::Request& request,
                 Http::ResponseWriter response) {
                  auto q = request.query().get("q"); // std::optional<std::string>
                  if (q) {
                      response.send(Http::Code::Ok, "search: " + *q);
                  } else {
                      response.send(Http::Code::Bad_Request, "missing q");
                  }
              });

    // 兜底:所有路由都没匹配上 → 404
    router.addNotFoundHandler(
        [](const Rest::Request&, Http::ResponseWriter response) {
            response.send(Http::Code::Not_Found, "no such route");
        });

    Http::Endpoint endpoint(Address(Ipv4::any(), Port(9080)));
    endpoint.init(Http::Endpoint::options().threads(2));
    endpoint.setHandler(router.handler());
    endpoint.serve();
}
# 编译并后台启动
g++ -std=c++17 -I/usr/local/pistache/include users.cpp \
    -L/usr/local/pistache/lib -lpistache -o users
./users &

# 路径参数生效
curl http://127.0.0.1:9080/users/42
# 输出:user id = 42

# 查询参数生效(URL 里有 ? 和 &,用双引号包住防止 shell 误解)
curl "http://127.0.0.1:9080/search?q=pistache"
# 输出:search: pistache

# 没匹配到的路径 → 404
curl -i http://127.0.0.1:9080/whatever
# 响应行:HTTP/1.1 404 Not Found
诸葛亮提示

路径参数和查询参数怎么选?经验法则:路径参数用来"指出是哪个资源"(名词),查询参数用来"描述怎么处理"(形容词)/users/42 点名用户 42;/users?page=2 说"用户列表,看第 2 页"。分不清时想想 REST 的本意——URL 描述资源,方法描述动作。

8.6 返回 JSON:pistache 与 jsoncpp 联手

文本响应只是热身,真实 REST 服务几乎都返回 JSON——第 6 章学过的那种轻量级数据交换格式。组合拳是:jsoncpp 负责把 C++ 数据变成 JSON 字符串,pistache 负责把字符串作为响应体发出去。要让客户端正确解析,还要告诉它响应体是什么类型——这靠响应头 Content-Type(内容类型)字段,值为 application/jsonMIME 类型说人话就是"互联网上给文件/内容贴的标签",application/json 表示"这是一段 JSON"。pistache 里用 MIME(Application, Json) 这个宏直接生成这个值,配合 response.send(code, body, mime) 的第三个参数一起发。

第 6 章学过的 jsoncpp 序列化在这里原样复用:Json::Value 搭树 → Json::StreamWriterBuilderJson::writeString(builder, root) 一行出字符串。有一个第 6 章踩过的坑必须再提醒:jsoncpp 序列化对象时,键名按字母顺序输出,与插入顺序无关——所以你在代码里先写 root["id"] 再写 root["name"],客户端看到的可能是 {"id":...,"name":...} 或别的字母序。这不是 bug,解析 JSON 的一方本就不该依赖键序。下面的例子把 8.5 的查询服务升级成 JSON 版:

// json_api.cpp:pistache + jsoncpp,返回 application/json
#include <pistache/endpoint.h>
#include <pistache/http.h>
#include <pistache/router.h>
#include <json/json.h>   // 第 6 章的 jsoncpp

using namespace Pistache;

int main() {
    Rest::Router router;

    // GET /api/user → 返回一个 JSON 对象
    router.get("/api/user",
              [](const Rest::Request&, Http::ResponseWriter response) {
                  Json::Value root;
                  root["id"]    = 42;
                  root["name"]  = "孔明";
                  root["alive"] = false;

                  Json::StreamWriterBuilder builder;
                  std::string body = Json::writeString(builder, root);

                  // 第三个参数声明内容类型:application/json
                  response.send(Http::Code::Ok, body, MIME(Application, Json));
              });

    Http::Endpoint endpoint(Address(Ipv4::any(), Port(9080)));
    endpoint.init(Http::Endpoint::options().threads(2));
    endpoint.setHandler(router.handler());
    endpoint.serve();
}
// 片段:POST /api/user —— 从请求体里读 JSON(接着上面的文件写)
#include <sstream>   // std::istringstream

router.post("/api/user",
          [](const Rest::Request& request,
             Http::ResponseWriter response) {
              // request.body() 拿到请求体原始文本
              Json::CharReaderBuilder readerBuilder;
              Json::Value root;
              std::string errs;
              std::istringstream iss(request.body());

              if (!Json::parseFromStream(readerBuilder, iss, &root, &errs)) {
                  response.send(Http::Code::Bad_Request, "bad json: " + errs);
                  return;
              }
              std::string name = root.get("name", "").asString();
              response.send(Http::Code::Created, "created: " + name);
          });
# 编译、启动、验证(-i 显示响应头,能看到 Content-Type)
g++ -std=c++17 -I/usr/local/pistache/include json_api.cpp \
    -L/usr/local/pistache/lib -lpistache -o json_api
./json_api &

curl -i http://127.0.0.1:9080/api/user
# HTTP/1.1 200 OK
# Content-Type: application/json
# ...
# {"alive":false,"id":42,"name":"孔明"}   ← 键按字母序输出,不是 bug

# POST:客户端把 JSON 放在请求体里发过来
curl -i -X POST -H "Content-Type: application/json" \
     -d '{"name":"司马懿"}' http://127.0.0.1:9080/api/user
# HTTP/1.1 201 Created
# created: 司马懿

至此,一条完整的 REST 链路已经打通:客户端用 curl(或浏览器、别的程序)发 HTTP 请求 → pistache 解析请求、按路由分发给你的处理器 → 处理器里用 jsoncpp 拼装或解析 JSON → pistache 带着正确的状态码和 Content-Type 把响应发回去。这四件事串起来,就是一个能对外提供服务的 C++ 后端了。

诸葛亮提示

想换一种写法设置 Content-Type?也可以不用 send 的第三个参数,而是先操作响应头:response.headers().add<Http::Header::ContentType>(MIME(Application, Json));response.send(Http::Code::Ok, body);。两种方式等价,官方示例里两种都见得到,选顺手的那种即可。

章末练习

练习 1:概念三连 入门

用一句话说人话分别解释:REST、资源(resource)、HTTP 方法。再回答:在 REST 约定里,PUT 和 POST 通常各管什么事?

提示

回想 8.1 的"四字兵法"表:查、增、改、删分别对应谁?资源在 URL 里,方法在请求里,合起来构成完整语义。

参考答案

REST:一种设计网络接口的风格,把服务器上的数据看作资源,用 HTTP 方法操作它们;资源:服务器上可以被寻址和操作的数据对象(用户、文章、设备……),每个资源有 URL;HTTP 方法:对资源执行的动作(GET 读取、POST 新建、PUT 整体更新、DELETE 删除)。PUT 管"改"(把资源整个换成新状态),POST 管"增"(新建一个资源)。

练习 2:加两个路由 进阶

在 8.4 的 hello_router.cpp 基础上,给 Router 加上:① GET /greet/:name,返回 hello, <name>;② GET /greet,从查询参数 ?name=xxx 读名字,没传则返回 400。

提示

路径参数用 request.param(":name").as<std::string>();查询参数用 request.query().get("name"),先判断 if (q) 再解引用 *q

参考答案

router.get("/greet/:name", [](const Rest::Request& req, Http::ResponseWriter res){ res.send(Http::Code::Ok, "hello, " + req.param(":name").as<std::string>()); });router.get("/greet", [](const Rest::Request& req, Http::ResponseWriter res){ auto q = req.query().get("name"); if (q) res.send(Http::Code::Ok, "hello, " + *q); else res.send(Http::Code::Bad_Request, "missing name"); }); 两个路由路径不同(一个带 :name 一个不带),可以共存。

练习 3:POST JSON 收尾 挑战

把 8.6 的 POST 片段补完整:POST /api/user 接收 JSON,若解析失败返回 400 和错误信息;若成功但 JSON 里没有 name 字段,返回 400;成功则返回 201。再用 curl 分别测三种情况。

提示

字段是否存在用 root.isMember("name")(第 6 章学的);解析失败检查 parseFromStream 的返回值;201 用 Http::Code::Created。curl 测 400 时故意发 -d '{"name":' 这种残缺 JSON。

参考答案

核心逻辑:解析失败 response.send(Http::Code::Bad_Request, "bad json: " + errs)if (!root.isMember("name")) response.send(Http::Code::Bad_Request, "name required");都过了再 response.send(Http::Code::Created, "created: " + root["name"].asString())。测试:残缺 JSON → 400;-d '{"age":1}' → 400(缺 name);-d '{"name":"郭嘉"}' → 201。注意 root["name"].asString() 前先用 isMemberget("name", "") 兜底,避免读到空值。

练习 4:内存用户表 挑战

综合小项目:用 std::map<std::string, std::string> 当内存数据库,实现三个接口:GET /users 返回全部用户的 JSON 数组;POST /users 从 body 读 {"name":...} 新增;DELETE /users/:id 按 id 删除(不存在返回 404)。跑通后用 curl 完整走一遍增、查、删。

提示

JSON 数组用 Json::Value arr(Json::arrayValue); arr.append(item); 构造(第 6 章:数组顺序有保证);删除用 router.del(...)map.erase(id) 返回删除个数,为 0 就是 404。

参考答案

核心骨架:std::map<std::string, std::string> users;(id → name)。GET 遍历 map 把每对塞进 Json::Value 对象再 append 到数组,send(Http::Code::Ok, body, MIME(Application, Json))。POST 解析 body 后 users[id] = name,id 可用计数器生成,返回 201。DELETE 用 router.del("/users/:id", ...)auto n = users.erase(id); if (n == 0) send(Not_Found, "no such user"); else send(Ok, "deleted");。这个"map 当数据库"的模式就是真实项目中内存缓存的雏形。