小叶-duck头像
《C++ 工具库实战》httplib 轻量 HTTP 服务:动态路由、静态托管实战封面图

《C++ 工具库实战》httplib 轻量 HTTP 服务:动态路由、静态托管实战

 

🔥小叶-duck:个人主页

 ❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、httplib 介绍

  1.1 httplib 核心特性

二、httplib 安装

三、httplib 使用

  2.1 头文件包含

  4.2 核心接口与核心结构体解析

    4.2.1 HTTP 协议基础回顾

    4.2.2 Request 请求结构体

    4.2.3 Response 响应结构体

    4.2.4 Server 服务端核心类

    4.2.5 Client 客户端核心类

    4.2.6 Result 返回结果类

四、完整示例讲解

  4.1 示例 1:Get 注册动态接口

  4.2 示例 1 优化:正则路由

  4.3 示例三:静态资源目录挂载 set_mount_point

  4.4 动态路由接口与静态资源挂载的区别

结束语


前言

      在 C++ 服务端开发中,如果想要快速搭建简易 HTTP 服务,完整从零手写一套 HTTP 协议栈成本很高,需要自行处理协议解析、连接管理、请求分发等大量底层工作。而 httplib 是一款单头文件、轻量级的 C++ HTTP 库,无需复杂编译链接,就可以快速实现 HTTP 服务端与客户端能力,非常适合用来快速开发调试接口、搭建静态网页服务。

      本文将从 httplib 基础介绍、环境安装开始,依次拆解库内核心结构体与核心类,结合多个可直接运行的代码示例进行实战演示。我们会学习基础 GET 动态接口、正则路由的使用,同时掌握静态资源目录挂载,区分动态业务接口和静态文件托管的适用场景。

一、httplib 介绍

      在 C++ 后端开发中,如果想快速搭建简易 HTTP 服务、开发轻量 http 接口,或者实现 HTTP 客户端发起网络请求,不需要从零手写 socket、解析 HTTP 报文。
      cpp-httplib(简称 httplib)就是一款非常适合的单头文件轻量级 HTTP 库。它仅需要引入一个头文件就能使用,无需复杂链接,是 C++ 项目快速开发 HTTP 服务的常用工具。

  1.1 httplib 核心特性

  • 单头文件,轻量化:整个库仅单个头文件,无任何基础外部依赖,引入头文件即可使用,编译部署简单。
  • 跨平台:支持 Linux、Windows、macOS,一套代码可以在多系统编译运行。
  • 同时支持服务端 + 客户端:既可以快速搭建 HTTP 服务器,注册接口路由;也可以作为客户端,向外发送 HTTP 请求。
  • 完整 HTTP/1.1 协议支持:支持长连接、管道化,GET、POST 等常用请求方法。
  • 丰富请求类型:原生支持multipart/form-data,可以直接实现文件上传功能。
  • 支持 HTTPS/WSS:搭配 OpenSSL/mbedTLS 库,即可开启加密通信。
  • API 简洁易用:路由注册、请求参数获取、响应构造接口设计直观,上手门槛低。
  • 支持同步、异步两种编程模型,适配不同业务场景。

二、httplib 安装

方式 1:源码克隆安装(Ubuntu / CentOS 通用,推荐)

# 拉取源码仓库
git clone https://github.com/yhirose/cpp-httplib.git
cd cpp-httplib
# 将头文件 httplib.h 复制到系统头文件目录,全局可用
sudo cp httplib.h /usr/include/

方式 2:包管理器安装

Ubuntu 直接命令安装:

sudo apt update
sudo apt install libcpp-httplib-dev

CentOS 直接命令安装:

# CentOS8+/RockyLinux
sudo dnf install cpp-httplib-devel

说明:CentOS 的包名后缀是-devel,和 Ubuntu 的-dev命名习惯不一样。

验证是否安装成功:

  • 查看头文件
ls /usr/include/httplib.h

安装完成结果:

三、httplib 使用

  2.1 头文件包含

#include <httplib.h>

      cpp-httplib 是单头文件库,只需要引入这一个头文件,就可以使用全部的服务端、客户端能力。

  4.2 核心接口与核心结构体解析

    4.2.1 HTTP 协议基础回顾

      HTTP 是基于明文字符串的应用层协议,分为请求报文和响应报文两部分,这也是 Request 和Response 两个结构体设计的底层依据。

请求报文格式:

  1. 请求行:请求方法,URI,协议版本 GET /index.html?s=key HTTP/1.1
  2. 请求头部:一组 key:value 键值对,描述请求附加信息
  3. 空行:用于分隔头部与请求正文
  4. 正文:请求携带的载荷数据(POST 提交的数据、表单、JSON 等)

响应报文格式:

  1. 响应行:协议版本 状态码 状态描述 HTTP/1.1 200 OK
  2. 响应头部:一组 key:value 键值对,描述返回资源信息
  3. 空行:分隔头部与响应正文
  4. 正文:服务器返回的有效数据,如 html、json、文件二进制内容

      httplib 库内部已经完成 HTTP 报文的解析与组装,我们不用手动处理字符串拆分,直接使用封装好的Request、Response对象读写数据。

    4.2.2 Request 请求结构体

      Request 用来存放客户端发来的完整请求解析结果,在路由回调函数中作为入参传入。

struct Request {
    std::string method;          // HTTP请求方法: GET / POST / PUT / DELETE
    std::string path;            // 请求路径,如 /hi
    Headers headers;             // 请求头,键值对
    std::string body;            // 请求正文,POST提交的json、文本数据存在这里
    Params params;               // URL查询参数,即 ?key=value 部分
    MultipartFormDataMap files;  // 文件上传的表单数据,multipart/form-data
    Ranges ranges;
    Match matches;               // 正则路由捕获的路径参数

    // for client
    ResponseHandler response_handler;
    ContentReceiverWithProgress content_receiver;
};

using ResponseHandler = std::function<bool(const Response &response)>;

using ContentReceiverWithProgress =
    std::function<bool(const char *data, 
                       size_t data_length,
                       uint64_t offset,
                       uint64_t total_length)>;
  • method:获取请求方法,判断是 GET 还是 POST 请求。
  • path:客户端访问的请求路径。
  • headers:存放全部请求头,例如Content-Type、User-Agent。
  • params:url 问号后的查询参数,例如/hi?name=zhangsan,可以直接拿到 name 的值。
  • body:POST 请求的请求体,常用于接收 JSON 报文。
  • files:专门处理文件上传场景,读取 multipart 表单上传的文件。
  • matches:正则路由捕获的分组内容,例如/numbers/(\d+),捕获到的数字就存放在 matches。
  • response_handler:客户端发起请求后,用于异步接收并处理服务端响应的回调函数。当服务端返回状态码、响应头和响应体时触发,常用于非阻塞的请求场景。
    类型为包装器类型:std::function<bool(const Response &response)>

  • content_receiver:客户端接收服务端返回数据块(如大文件下载)时的流式接收与进度回调。不仅能分块处理数据避免内存溢出,还能实时获取下载进度。
    类型为包装器类型:std::function<bool(const char *data, size_t data_length, uint64_t offset, uint64_t total_length)>

    4.2.3 Response 响应结构体

      Response 用来构造服务器返回给客户端的报文内容,在回调函数中填充内容。

struct Response {
    std::string version;
    int status = -1;                    // HTTP状态码,200成功、404找不到资源、500服务器错误
    std::string reason;
    Headers headers;                    // 响应头键值对
    std::string body;                   // 响应正文,返回给浏览器/客户端的数据
    void set_content(const std::string &s, const std::string &content_type);
    void set_header(const std::string &key, const std::string &val);
};
  • status:设置 HTTP 状态码,rsp.status = 200代表正常返回。
  • set_content():封装好的接口,一次性设置响应正文和Content-Type。
  • set_header():自定义添加响应头信息。

    4.2.4 Server 服务端核心类

      Server 是用来搭建 HTTP 服务的核心类,负责监听端口、维护路由表、分发请求到对应的回调函数。

class Server {
    using Handler = std::function<void(const Request &, Response &)>;
    bool set_mount_point(const std::string &mount_point, const std::string &dir, Headers headers = Headers());
    Server &Get(const std::string &pattern, Handler handler);
    Server &Post(const std::string &pattern, Handler handler);
    Server &Put(const std::string &pattern, Handler handler);
    Server &Delete(const std::string &pattern, Handler handler);
    bool listen(const std::string &host, int port);
};
  • Get/Post/Put/Delete:注册路由回调。
        第一个参数是匹配路径,第二个是请求处理函数。 底层会把路径 + 请求方法作为 key,存入内部哈希路由表。当客户端请求到达,Server 查找路由表,找到匹配的 handler 回调执行,传入 Request、Response 对象。
  • set_mount_point():静态资源挂载接口。
        映射本地磁盘目录到 http 访问路径,不需要手写回调。
  • listen(host, port):启动服务器,绑定 IP 与端口,开始阻塞等待客户端连接。

    4.2.5 Client 客户端核心类

      Client 用于作为 HTTP 客户端,在代码里主动向外发起 HTTP 请求,调用远端 HTTP 接口。

class Client {
    explicit Client(const std::string &host, int port);

    Result Get(const std::string &path, const Headers &headers);
    Result Get(const std::string &path, const Params &params, const Headers &headers, Progress progress = nullptr);

    Result Post(const std::string &path, 
                const Headers &headers,
                const std::string &body,
                const std::string &content_type) //常用
    Result Post(const std::string &path, const std::string &body, const std::string &content_type);
    Result Post(const std::string &path, const Params &params);
    Result Post(const std::string &path, const Headers &headers, const Params &params);
    Result Post(const std::string &path, const MultipartFormDataItems &items);

    Result Client::send(const Request &req)
    bool Client::send(Request &req, Response &res, Error &error)

    Result Put(const std::string &path, const std::string &body, const std::string &content_type);
    Result Delete(const std::string &path, const std::string &body, const std::string &content_type);
};
  • 构造函数传入目标服务的主机和端口。
  • Get、Post等成员函数用于发起对应类型 HTTP 请求。
  • 返回值为Result类型,用来判断请求是否成功,获取远端返回的 Response。

    4.2.6 Result 返回结果类

      Result 是 Client 发起请求后的返回包装对象,重载了布尔运算符,可以直接判断请求是否成功,也可以用 -> 直接访问 Response 成员。

class Result {
    operator bool() const { return res_ != nullptr; }
    const Response &value() const { return *res_; }
    Response &value() { return *res_; }
    const Response &operator*() const { return *res_; }
    Response &operator*() { return *res_; }
    const Response *operator->() const { return res_.get(); }
    Response *operator->() { return res_.get(); }
};
  • 支持if(ret)判断请求是否成功。
  • ret->body 直接拿到响应正文,简化客户端的代码书写。

四、完整示例讲解

  4.1 示例 1:Get 注册动态接口

      本示例演示使用 Server::Get() 注册 GET 路由回调,读取请求全部信息,构造 HTML 响应返回给浏览器。

#include <iostream>
#include <httplib.h>

// GET请求对应的回调处理函数
// req: 入参,封装客户端发来的全部HTTP请求数据
// resp: 出参,我们填充内容,httplib会把它组装成HTTP响应报文返回客户端
void Hello_world(const httplib::Request &req, httplib::Response &resp)
{
    // 打印请求方法、请求路径、请求正文
    std::cout << req.method << std::endl;
    std::cout << req.path << std::endl;
    std::cout << req.body << std::endl;
    std::cout << std::endl;

    // 遍历打印所有请求头(headers是键值对容器)
    for (auto &it : req.headers)
    {
        std::cout << it.first << " = " << it.second << std::endl;
    }
    std::cout << std::endl;

    // 遍历打印URL查询参数,即url中 ?key1=val1&key2=val2 部分
    for (auto &it : req.params)
    {
        std::cout << it.first << " = " << it.second << std::endl;
    }

    // 构造返回的html页面正文
    std::string html_body = "<html><body><h1>Hello World</h1></body></html>";
    // 设置响应正文与Content-Type,告诉浏览器这是html文本
    resp.set_content(html_body, "text/html");
    // 设置HTTP响应状态码,200代表请求成功
    resp.status = 200;
}

int main()
{
    // 实例化http服务对象
    httplib::Server server;
    // 注册GET路由:访问路径为 /hello 时,执行Hello_world回调函数
    server.Get("/hello", Hello_world);
    // 启动服务,监听本机所有网卡的8080端口,阻塞等待连接
    server.listen("0.0.0.0", 8080);

    return 0;
}

代码分析:

  1. 回调函数参数含义
          const httplib::Request &req:只读的请求对象,httplib 框架在收到 HTTP 报文后自动解析、填充。我们可以从中读取请求方法、路径、请求头、url 查询参数 params、请求体 body。
          httplib::Response &resp:响应对象,由我们填充内容。函数执行结束后,httplib 内部读取 resp,组装成标准 HTTP 响应报文发回浏览器。
  2. URL 查询参数 req.params 浏览器访问地址:47.98.198.24:8080/hello?ply=123456&login=abcd
          路径部分:/hello,匹配我们注册的路由 /hello
          ? 后面的 ply=123456&login=abcd,会被库自动解析,存入 req.params(multimap 键值对),循环遍历就能取出这两组参数。

    这就是 GET 请求传递参数最常用的方式。

  3. 响应构造
          resp.set_content(内容, MIME类型):一次性设置响应 body 和 Content-Type 响应头。这里 text/html 告诉浏览器返回的是 HTML,浏览器会自动渲染页面,而不是直接打印原始文本。
          resp.status = 200:设置 HTTP 状态码,200 OK 表示请求处理成功。

运行结果展示:

  4.2 示例 1 优化:正则路由

问题:httplib 的 Get 接口为什么原生支持正则表达式?

      httplib 在设计路由模块时,内部会区分两种路由字符串:普通固定字符串路由 和 正则路由。

  • 当你调用 server.Get(路由串, 回调),库内部会判断传入的路由字符串是不是正则表达式。
  • 底层使用 C++11 标准库 <regex> 做正则匹配。当路由字符串是合法正则,httplib 不会做简单字符串相等比对,而是执行正则匹配;如果正则带有捕获括号(),匹配成功之后,会自动把捕获分组结果存入 req.matches 容器,在回调里直接读取。

设计动机:

  1. 资源路径经常有一类相同业务逻辑的请求,例如文件下载、用户信息查询(/user/1001、/user/1002)。如果没有正则路由,每一个资源都要单独注册路由,代码极度冗余,不好维护。
  2. 区分路径参数和查询参数。查询参数放在?后面,而资源标识直接放在 URL 路径里,RESTful 风格接口常用这种写法。
  3. 统一在框架层做路径解析,业务代码不用自己手写字符串切割,减少手写解析带来的 bug。
/*
    使用HTTPLIB搭建HTTP服务器
    1、包含头文件
    2、实例化Server对象
    3、注册路由信息 -- 告诉httplib收到哪个请求应该由哪个接口处理
    4、启动服务器
*/

#include <iostream>
#include <httplib.h>

// GET请求对应的回调处理函数
// req: 入参,封装客户端发来的全部HTTP请求数据
// resp: 出参,我们填充内容,httplib会把它组装成HTTP响应报文返回客户端
void Hello_world(const httplib::Request &req, httplib::Response &resp)
{
    // 打印请求方法、请求路径、请求正文
    std::cout << req.method << std::endl;
    std::cout << req.path << std::endl;
    std::cout << req.body << std::endl;
    std::cout << std::endl;

    // 遍历打印所有请求头(headers是键值对容器)
    for (auto &it : req.headers)
    {
        std::cout << it.first << " = " << it.second << std::endl;
    }
    std::cout << std::endl;

    // 遍历打印URL查询参数,即url中 ?key1=val1&key2=val2 部分
    for (auto &it : req.params)
    {
        std::cout << it.first << " = " << it.second << std::endl;
    }

    // 构造返回的html页面正文
    std::string html_body = "<html><body><h1>Hello World</h1></body></html>";
    // 设置响应正文与Content-Type,告诉浏览器这是html文本
    resp.set_content(html_body, "text/html");
    // 设置HTTP响应状态码,200代表请求成功
    resp.status = 200;
}

// 下载接口回调函数
void DownloadHandler(const httplib::Request &req, httplib::Response &resp)
{
    // 打印请求方法、请求完整路径
    std::cout << req.method << std::endl;
    std::cout << req.path << std::endl;

    // 遍历输出正则捕获到的全部分组内容
    // matches[0] 完整匹配的整条路径,matches[1] 捕获组提取的文件名
    for (auto &it : req.matches)
    {
        std::cout << it << std::endl;
    }
}

int main()
{
    // 实例化http服务对象
    httplib::Server server;
    // 注册固定路由:精准匹配 /hello,访问路径为 /hello 时,执行Hello_world回调函数
    server.Get("/hello", Hello_world);

    // 注册正则路由:匹配 /download/文件名,限定文件名只能是大小写字母、数字、下划线、点、短横线
    server.Get(R"(/download/([A-Za-z0-9._-]+))", DownloadHandler);

    // 启动服务,监听本机所有网卡的8080端口,阻塞等待连接
    server.listen("0.0.0.0", 8080);
    return 0;
}

代码分析:

  1. 正则表达式 [A-Za-z0-9._-]+
          字符集 [A-Za-z0-9._-]:
    仅识别大小写英文字母、数字、点.、下划线_、短横线-,不会匹配斜杠/。
          量词+:要求匹配内容至少包含 1 个字符,不会匹配空字符串。
          整体效果:只能匹配单层文件名,无法匹配带有子目录分隔符/的路径。

  2. req.matches遍历打印
    访问 /download/test.txt:
          matches[1]:test.txt(括号捕获的文件名)
          matches[0]:/download/test.txt(完整匹配的路径)
          直接循环遍历req.matches,方便调试查看正则捕获结果,开发阶段用来验证正则是否符合预期。
  3. 多条路由共存
    • 固定路由 /hello 和正则路由 /download/xxx 可以同时注册在同一个 server 实例中。
    • 匹配优先级:固定精确路由 > 正则路由,当请求路径同时命中多条规则时,优先走固定路由。

运行结果展示:

  4.3 示例三:静态资源目录挂载 set_mount_point

#include <iostream>
#include <httplib.h>

int main()
{
    httplib::Server server;
    // 第一个参数:url访问前缀;第二个:本地文件夹目录
    // 含义:所有以 / 开头的请求,都会去本地 ./wwwroot/ 目录寻找对应的资源
    server.set_mount_point("/", "./wwwroot");

    server.listen("0.0.0.0", 8080);
    return 0;
}

代码分析:

  • set_mount_point("/", "./wwwroot") 为整目录挂载。当访问路径以 / 开头时,httplib 会自动把请求路径拼接至本地 ./wwwroot 目录下查找资源。
  • 若 ./wwwroot 路径下存在多个 html 文件,也无需为每一个 html、图片文件单独注册接口,一行代码完成整个网站静态资源的发布,子目录资源也会自动识别。
  • 如果请求的文件不存在,httplib 会直接返回 404 状态码。

运行结果展示:

  4.4 动态路由接口与静态资源挂载的区别

httplib 提供了两种使用方式,分别适配动态业务处理和静态资源分发场景。

  1. 动态路由(svr.Get()注册回调)
          浏览器或者客户端访问我们注册好的接口路径时,可以携带查询参数、请求体等报文数据。服务器收到请求后,会执行我们自定义的回调函数,在回调内读取客户端传来的数据,执行业务逻辑(查询信息、处理提交数据等),动态组装响应内容返回给客户端。 典型场景:用户提交表单、查询个人信息、传递 JSON 报文。返回的内容是代码实时计算生成,不同请求可能返回不一样结果。

      典型场景:提交表单、查询个人信息、提交 JSON 报文,每次返回的内容是根据用户请求动态计算出来的。

  1. 静态资源挂载(set_mount_point)
          该接口将服务器本地磁盘目录映射到 HTTP 访问路径。当客户端发起请求,httplib 库内部自动去指定文件夹寻找对应的文件,直接读取文件内容返回,不需要我们手写回调函数。文件内容预先写在磁盘上,不会随请求动态变化。 典型场景:首页 index.html、前端页面、图片、js、css 这类固定不变的静态资源。

      典型场景:首页 index.html、前端页面、图片资源,文件内容是固定不变的静态资源。

      补充:二者可以共存于同一个 HTTP 服务。请求到来时,优先匹配手动注册的动态路由;动态路由匹配失败,才会尝试走静态文件挂载逻辑。

结束语

      本篇我们完整学习了 httplib 这款轻量级 C++ HTTP 库,从库的特性、环境安装,到 Request/Response、Server 等核心类的原理剖析,再到动态 GET 路由、正则路由、静态资源挂载三个实战示例,逐步体会了 httplib 单头文件、开箱即用的优势。

      动态路由适合用来处理业务逻辑、接口计算,而静态资源挂载专门用于托管 html、图片这类文件,二者各司其职,在实际项目中经常搭配使用。需要注意 httplib 更适合做小型服务、接口调试、本地网页服务;在高并发、大规模线上业务场景,还需要结合性能需求评估选型。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_91275995/article/details/168142459

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--