【C++ 基于微服务的即时通讯系统】网关基建:cpp-httplib 与 WebSocketpp 从入门到实战


🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:


文章目录

  • 前言:
  • [一、httplib 库的安装与使用](#一、httplib 库的安装与使用)
    • [1.1 库介绍](#1.1 库介绍)
    • [1.2 安装方式](#1.2 安装方式)
    • [1.3 核心数据结构与接口](#1.3 核心数据结构与接口)
      • [Request 请求结构体](#Request 请求结构体)
      • [Response 响应结构体](#Response 响应结构体)
      • [Server 服务端类](#Server 服务端类)
      • [Client 客户端类](#Client 客户端类)
    • [1.4 服务端实战样例](#1.4 服务端实战样例)
  • [二、websocketpp 的安装与介绍](#二、websocketpp 的安装与介绍)
    • [2.1 为什么需要 WebSocket](#2.1 为什么需要 WebSocket)
    • [2.2 WebSocket 协议原理解析](#2.2 WebSocket 协议原理解析)
      • [连接建立:HTTP 握手升级](#连接建立:HTTP 握手升级)
      • 数据帧格式
    • [2.3 WebSocketpp 库介绍与安装](#2.3 WebSocketpp 库介绍与安装)
    • [2.4 核心类与接口梳理](#2.4 核心类与接口梳理)
      • 基础类型
      • [server 服务端类](#server 服务端类)
      • [connection 连接类](#connection 连接类)
      • [frame::opcode 枚举](#frame::opcode 枚举)
  • [三、websocketpp 样例编写与测试](#三、websocketpp 样例编写与测试)
    • [3.1 服务端搭建流程](#3.1 服务端搭建流程)
    • [3.2 完整服务端代码](#3.2 完整服务端代码)
    • [3.3 编译运行](#3.3 编译运行)
    • [3.4 功能测试](#3.4 功能测试)
  • 四、核心要点总结
  • 结尾:

前言:

做即时通讯系统的网关层,绕不开两类网络请求:一类是注册、登录、用户信息查询这类HTTP 短连接请求 ,一问一答即可完成;另一类是消息接收、状态推送这类长连接场景 ,需要服务端主动向客户端推送数据。在我们这套 C++ 微服务 IM 架构里,选用了两个非常经典的轻量库来分别承担这两块工作:cpp-httplib 负责 HTTP 服务 ,WebSocketpp 负责 WebSocket 长连接。本文就顺着项目落地的顺序,从安装、核心接口、实战代码到踩坑记录,完整拆解这两个网关基建组件。


一、httplib 库的安装与使用

1.1 库介绍

cpp-httplib 是一个非常轻量的 C++ HTTP 客户端/服务器库,整个库只有一个头文件,集成成本极低。它的特点非常鲜明:

  1. 极致轻量 :仅一个头文件 httplib.h,不依赖任何外部库,包含就能用
  2. 跨平台:支持 Windows、Linux、macOS 全平台
  3. 同步异步都支持:两种调用模式,开发者按需选择
  4. 标准兼容:完整实现 HTTP/1.1 协议,支持持久连接、管道化
  5. 文件上传友好:原生支持 multipart form-data,做文件上传很方便
  6. SSL 支持:对接 OpenSSL 或 mbedTLS,支持 HTTPS、WSS
  7. API 简洁:接口设计非常直白,上手快,学习成本低

补充说明:最开始我们也考虑过直接用 WebSocketpp 自带的 HTTP 能力,但实际测试发现,它在高并发 HTTP 请求场景下存在一些稳定性问题。所以最终 HTTP 服务独立用 cpp-httplib 实现,WebSocket 专用 WebSocketpp,职责拆分更清晰,运行也更稳定。

1.2 安装方式

因为是 header-only(仅头文件)的库,不需要编译安装,直接引入头文件即可。

复制代码
# 克隆仓库
git clone [https://github.com/yhirose/cpp-httplib.git](https://github.com/yhirose/cpp-httplib.git)

# 直接把核心头文件拷贝到项目公共头文件目录即可
mv ./cpp-httplib/httplib.h ../common/

整个头文件代码接近一万行,封装了完整的 HTTP 协议解析、网络 IO、路由匹配等所有能力,业务层完全不用关心底层细节。

1.3 核心数据结构与接口

库的核心就四个类型,理解了就能快速上手:

Request 请求结构体

复制代码
struct Request {
    std::string method;   // 请求方法:GET/POST/PUT/DELETE
    std::string path;     // 请求路径
    Headers headers;      // 请求头,键值对
    std::string body;     // 请求体
    Params params;       // URL参数
};

Response 响应结构体

复制代码
struct Response {
    std::string version;  // HTTP版本
    int status = -1;      // 状态码
    Headers headers;      // 响应头
    std::string reason;   // 状态描述
    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);
};

Server 服务端类

用来搭建 HTTP 服务器,核心是注册路由和启动监听:

复制代码
class Server {
    using Handler = std::function<void(const Request&, Response&)>;

    // 注册不同方法的路由
    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);
};

Client 客户端类

用来发起 HTTP 请求,同步调用方式非常简洁:

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

    Result Get(const std::string& path, const Headers& headers);
    Result Post(const std::string& path, const std::string& body, const std::string& content_type);
    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);
};

1.4 服务端实战样例

搭建一个最简单的 HTTP 服务器,只需要三步:实例化服务器、注册路由回调、启动监听。

复制代码
#include <iostream>
#include "httplib.h"

int main() {
    // 1. 实例化服务器对象
    httplib::Server server;

    // 2. 注册GET路由回调
    server.Get("/hi", [](const httplib::Request& req, httplib::Response& rsp) {
        // 打印请求信息
        std::cout << "请求方法: " << req.method << std::endl;
        std::cout << "请求路径: " << req.path << std::endl;
        for (auto& header : req.headers) {
            std::cout << header.first << ": " << header.second << std::endl;
        }

        // 设置响应内容和状态码
        std::string body = "<html><body><h1>Hello Bite</h1></body></html>";
        rsp.set_content(body, "text/html");
        rsp.status = 200;
    });

    // 3. 启动服务器,监听9090端口
    std::cout << "服务器启动,端口9090" << std::endl;
    server.listen("0.0.0.0", 9090);

    return 0;
}

编译运行:

复制代码
g++ -std=c++17 main.cc -o main -lpthread
./main

启动后浏览器访问 http://服务器IP:9090/hi,就能看到页面返回 Hello Bite,说明 HTTP 服务就跑通了。

这个库本身接口已经足够简洁,业务场景直接用原生接口就行,不需要额外二次封装。


二、websocketpp 的安装与介绍

2.1 为什么需要 WebSocket

传统 HTTP 协议是「一问一答」模式:客户端发请求,服务端回响应,服务端永远是被动的。但在聊天系统里,对方发消息、好友申请这些场景,都需要服务端主动向客户端推送数据。

如果硬用 HTTP 实现,只能用轮询:客户端每隔几秒问一次服务端有没有新消息。这样做不仅延迟高,还会产生大量无效请求,服务器压力大。

WebSocket 就是为了解决这个问题诞生的:它是基于 TCP 的应用层长连接协议,连接建立之后,客户端和服务端都可以主动向对方发送数据,全双工通信,非常适合即时通讯、实时游戏这类需要消息推送的场景。

2.2 WebSocket 协议原理解析

连接建立:HTTP 握手升级

WebSocket 连接不是凭空建立的,而是从 HTTP 协议升级而来:

  1. 客户端先发起一个普通 HTTP 请求,头里带上 Upgrade: websocket、Sec-WebSocket-Key 等特殊字段
  2. 服务端识别到升级请求,返回 101 Switching Protocols 响应
  3. 握手完成,协议升级为 WebSocket,之后双方都用 WebSocket 帧格式传输数据

这个设计的好处是兼容 HTTP 端口,不需要额外开端口,也能兼容现有的 HTTP 代理、防火墙。

数据帧格式

WebSocket 传输以帧为单位,帧头里有几个关键字段:

  • FIN:1 表示这是消息的最后一帧,0 表示还有后续分片

  • opcode

    :标识帧的类型,这是最核心的字段

    • 0x0:延续帧,消息分片传输时的中间帧
    • 0x1:文本帧,传输字符串数据
    • 0x2:二进制帧,传输二进制数据(我们项目用 protobuf 通信,就用这个类型)
    • 0x8:连接断开帧
    • 0x9:ping 心跳帧
    • 0xa:pong 心跳回复
  • mask:表示载荷是否掩码,客户端发服务端的消息必须掩码

  • Payload length:载荷长度,根据长度不同有 7 位、16 位、64 位三种格式

2.3 WebSocketpp 库介绍与安装

库介绍

WebSocketpp 是 C++ 领域非常成熟的开源 WebSocket 库,基于 Boost.Asio 实现,特性很全面:

  • 事件驱动的 API 设计,异步 IO 性能好
  • 同时支持 HTTP/HTTPS、WS/WSS、IPv6
  • 灵活的依赖管理,支持 Boost 库也支持 C++11 标准库
  • 跨平台、线程安全
  • 同时支持 HTTP 和 WebSocket 两种协议

我们项目选它,核心原因就是一个库就能搞定网关的长连接需求,生态成熟,坑也少。

安装方式

Ubuntu 下可以直接 apt 安装开发包,简单快捷:

复制代码
# 安装依赖
sudo apt-get install libboost-dev libboost-system-dev

# 安装websocketpp开发库
sudo apt-get install libwebsocketpp-dev

安装完成后,/usr/include/websocketpp/ 目录下会有所有头文件。它也是偏 header-only 的设计,包含头文件就能用,编译时链接对应的系统库即可。

2.4 核心类与接口梳理

基础类型

  • connection_hdl :连接句柄,本质是 weak_ptr<void>,用来标识一个连接。不能直接操作,需要通过服务器转成真正的连接指针对象。
  • message_ptr:消息指针,用来获取收到的消息数据。

server 服务端类

核心的服务管理类,常用接口:

复制代码
class server {
    // 设置日志级别
    void set_access_channels(log::level channels);

    // 设置回调
    void set_open_handler(open_handler h);   // 连接建立成功回调
    void set_close_handler(close_handler h); // 连接关闭回调
    void set_message_handler(message_handler h); // 收到消息回调

    // 初始化asio框架
    void init_asio();

    // 启用地址重用,避免重启端口占用
    void set_reuse_addr(bool value);

    // 设置监听端口、开始监听、启动事件循环
    void listen(uint16_t port);
    void start_accept();
    size_t run();

    // 通过句柄获取连接对象
    connection_ptr get_con_from_hdl(connection_hdl hdl);

    // 发送消息、关闭连接
    void send(connection_hdl hdl, std::string& payload, frame::opcode::value op);
    void close(connection_hdl hdl, close::status::value code, std::string& reason);
};

connection 连接类

代表一个具体的 WebSocket 连接,常用的就是获取消息载荷和发送消息。

frame::opcode 枚举

对应协议里的帧类型:

复制代码
namespace frame {
    enum value {
        continuation = 0x0,  // 延续帧
        text = 0x1,         // 文本帧
        binary = 0x2,       // 二进制帧
        close = 0x8,        // 关闭帧
        ping = 0x9,         // ping
        pong = 0xA          // pong
    };
}

三、websocketpp 样例编写与测试

3.1 服务端搭建流程

搭建一个 WebSocket 服务器,标准步骤是九步:

  1. 定义 server 类型
  2. 实例化服务器对象
  3. 初始化日志输出(建议关闭默认日志,避免刷屏)
  4. 初始化 asio 框架
  5. 设置连接建立、连接关闭、消息接收三个回调函数
  6. 启用地址重用
  7. 设置监听端口
  8. 开始接受连接
  9. 启动服务器事件循环

3.2 完整服务端代码

复制代码
#include <iostream>
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>

// 0. 定义服务器类型
typedef websocketpp::server<websocketpp::config::asio> server_t;

// 连接建立回调
void onOpen(websocketpp::connection_hdl hdl) {
    std::cout << "WebSocket 长连接建立成功!" << std::endl;
}

// 连接关闭回调
void onClose(websocketpp::connection_hdl hdl) {
    std::cout << "WebSocket 长连接断开!" << std::endl;
}

// 消息接收回调
void onMessage(server_t* server, websocketpp::connection_hdl hdl, server_t::message_ptr msg) {
    // 1. 获取消息载荷
    std::string body = msg->get_payload();
    std::cout << "收到消息: " << body << std::endl;

    // 2. 获取连接对象,回发消息
    auto conn = server->get_con_from_hdl(hdl);
    conn->send(body + " --Hello!", websocketpp::frame::opcode::text);
}

int main() {
    // 1. 实例化服务器对象
    server_t server;

    // 2. 关闭所有日志输出
    server.set_access_channels(websocketpp::log::alevel::none);

    // 3. 初始化asio框架
    server.init_asio();

    // 4. 设置回调函数
    server.set_open_handler(onOpen);
    server.set_close_handler(onClose);

    // 消息处理回调需要访问server对象,用bind绑定
    auto msg_handler = std::bind(onMessage, &server, 
                                  std::placeholders::_1, std::placeholders::_2);
    server.set_message_handler(msg_handler);

    // 5. 启用地址重用
    server.set_reuse_addr(true);

    // 6. 设置监听端口
    server.listen(9090);

    // 7. 开始接受连接
    server.start_accept();

    // 8. 启动服务器事件循环
    std::cout << "WebSocket服务器启动,端口9090" << std::endl;
    server.run();

    return 0;
}

代码解读与踩坑

  1. connection_hdl 是弱指针 它只是个标识,不能直接用来发消息,必须调用 server.get_con_from_hdl(hdl) 拿到真正的 connection_ptr 才能操作。
  2. 回调参数匹配问题 这是新手最高频的编译错误。set_message_handler 要求的回调类型是 void(connection_hdl, message_ptr),如果我们的回调需要额外访问 server 对象,就必须用 std::bind 绑定参数,不能直接传多参数的函数。
  3. 不要给 connection_hdl 加引用 很多人图方便写 connection_hdl& hdl,会直接报类型不匹配的编译错误。connection_hdl 本身就是个轻量句柄,直接传值就行。

3.3 编译运行

编译命令,需要链接 pthread 和 boost_system:

复制代码
g++ -std=c++17 main.cc -o main -lpthread -lboost_system

运行:

复制代码
./main

3.4 功能测试

写个简单的 HTML 页面就能测试,不用专门写客户端:

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>WebSocket测试</title>
</head>
<body>
    <input type="text" id="message" placeholder="输入要发送的消息">
    <button id="submit">提交</button>

    <script>
        // 连接WebSocket服务端,改成自己的服务器地址
        let websocket = new WebSocket("ws://127.0.0.1:9090");

        websocket.onopen = function() {
            console.log("连接建立成功");
        }

        websocket.onmessage = function(e) {
            console.log("收到服务端消息: " + e.data);
        }

        websocket.onerror = function() {
            console.log("连接异常");
        }

        websocket.onclose = function() {
            console.log("连接关闭");
        }

        // 点击按钮发送消息
        let input = document.querySelector('#message');
        let button = document.querySelector('#submit');
        button.onclick = function() {
            console.log("发送消息: " + input.value);
            websocket.send(input.value);
        }
    </script>
</body>
</html>

浏览器打开这个页面,按 F12 打开控制台,输入消息点击发送,就能看到收发日志,说明整个 WebSocket 链路就通了。


四、核心要点总结

  1. 职责拆分清晰:HTTP 短连接业务用 cpp-httplib,单头文件零依赖,集成简单稳定;WebSocket 长连接消息推送用 WebSocketpp,功能完整生态成熟。两者配合就是网关服务的标准网络底座。
  2. WebSocket 本质是 HTTP 升级:先通过 HTTP 握手,再升级为长连接协议,不是凭空的新协议,这也是它能兼容现有网络环境的核心原因。
  3. 句柄和连接要区分 :connection_hdl 只是弱标识,操作连接必须通过 get_con_from_hdl 转换成真正的连接指针对象,这是新手最高频的坑。
  4. 回调参数严格匹配 :库的回调类型是固定的,参数不对就编译失败。需要额外上下文就用 std::bind 绑定,不要硬改函数签名。
  5. 默认日志建议关闭:两个库默认日志都比较多,生产环境一定要关掉,避免日志刷屏影响性能,用自己的日志系统统一管理。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:到这里,网关层的两个核心网络组件就都跑通了。HTTP 负责处理所有的短连接业务请求,WebSocket 负责维护客户端长连接、做消息主动推送,这就是整个即时通讯系统和客户端交互的入口。接下来我们会基于这两个组件,实现完整的网关服务:协议解析、鉴权、服务转发、消息推送,把前面讲的 etcd、brpc 都串起来,形成完整的微服务调用链路。后续也会继续分享更多 C++ IM 微服务的实战内容,欢迎关注交流。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
李纲明1 小时前
WordPress 媒体库上传失败或一直转圈:先核 PHP 上传限制与目录权限,再查安全插件与对象存储
运维·开发语言·安全·php·wordpress·媒体库·devdiy
Ruiery9 小时前
Linux 6.6内核内存管理深度解析(十六):mmap — 用户态“申请内存“的真相
linux·运维·服务器
ClickHouseDB9 小时前
ClickHouse Cloud 与 Snowflake 实时性价比对比:CostBench 测试中的 412 倍差距来
运维·数据库·系统架构
第九序言T9 小时前
Python 数据处理进阶(十八)批量处理同结构文件:一个文件夹一次跑完
开发语言·python·自动化·pandas·数据处理
m0_639906949 小时前
Redis入门|五种数据结构、使用场景、持久化RDB&AOF
数据结构·数据库·redis
欧米欧9 小时前
Linux系统篇(七),环境变量的底层溯源,核心获取机制与安全应用
linux·运维·服务器
Ruiery10 小时前
Linux 6.6内核内存管理深度解析(十八):KASLR 内核地址空间随机化
linux·运维·服务器·安全
Sakura_j_h_10 小时前
EasyEDA PCB 快速设计Skills
c++·python·github
时间售卖铺10 小时前
触发器与函数实战:MySQL高效编程秘诀
运维·数据库·mysql·云原生