2025-06-03  2025-06-03    3931 字  8 分钟
- 编程

Websocketpp库用法简介

WebSocket++ 是一个基于 C++ 的开源 WebSocket 客户端和服务器库。 它基于 Asio 提供异步 I/O 支持,支持 TLS 和非加密通信,并以 header-only 模式分发,易于集成。本笔记旨在记录 WebSocket++ 的基本使用方式,包括:

  • 如何启动一个最小的 WebSocket 服务器
  • 如何创建一个简单的客户端连接
  • 如何通过消息处理函数处理客户端/服务器之间的通信

本示例采用的是 “no TLS” 配置,意味着通信内容是明文传输,适用于实验环境或内部测试。

 WebSocket++ 的事件驱动架构基于以下核心回调函数:
 ┌───────────────┐
 │ connect() 发起连接 │
 └──────┬────────┘
        ▼
    [连接成功] ➝ set_open_handler  —— 连接建立时调用,可用来发送初始化数据
        ▼
 set_message_handler                 —— 收到消息时调用(双向)
 set_fail_handler                    —— 连接失败时调用
 set_close_handler                   —— 连接关闭时调用

支持的数据类型包括:
 - 文本消息(text):使用 msg->get_payload() 获取 std::string
 - 二进制消息(binary):通过 get_payload() 仍可获取内容,但处理方式不同

自定义数据结构(如结构体)可通过 JSON 或手动序列化为字符串进行传输。更多高级功能(如广播、TLS、子协议、多线程支持等)可参考官方 examples/ 目录。

WebSocket 回显服务器代码简介

#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>

#include <iostream>

using websocketpp::connection_hdl;

/*
 * 使用无 TLS(不加密)的 Asio 配置:
 * typedef websocketpp::server<websocketpp::config::asio> server;
 *
 * 这表示定义了一个基于 WebSocket++ 的服务器类型,使用 Asio 库作为底层网络实现。
 * 此配置 不启用 TLS 加密,对应的连接协议是 ws://(非加密的 WebSocket)。
 *
 * ---------------------------- TLS 简介 ----------------------------
 * TLS(Transport Layer Security)是传输层安全协议,类似于 HTTPS,
 * 用于在客户端和服务器之间建立安全的加密连接。
 *
 * WebSocket 也支持 TLS。如果你使用 wss:// 开头的安全 WebSocket,就需要启用 TLS。
 * 在 WebSocket++ 中,使用以下配置启用 TLS:
 *
 *     #include <websocketpp/config/asio_tls.hpp>
 *     typedef websocketpp::server<websocketpp::config::asio_tls> server;
 *
 * 同时,你还需要提供服务器证书和私钥,例如:
 *
 *     server.set_tls_init_handler([](websocketpp::connection_hdl) {
 *         auto ctx = std::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::tlsv12);
 *         ctx->use_certificate_chain_file("server.crt");
 *         ctx->use_private_key_file("server.key", boost::asio::ssl::context::pem);
 *         return ctx;
 *     });
 *
 * ----------------------- ws:// vs wss:// 对比 ------------------------
 * - ws://  : 不加密的 WebSocket,默认端口 80,适用于测试或局域网通信
 * - wss:// : 使用 TLS 加密的 WebSocket,默认端口 443,适用于互联网部署
 *
 * 生产环境建议使用 wss:// 来保护数据传输安全。
 */
typedef websocketpp::server<websocketpp::config::asio> server;


/*
 * 简写类型:服务端消息指针
 *
 * message_ptr 是一个 std::shared_ptr 类型的智能指针,
 * 指向接收到的 WebSocket 消息对象(message_type)。
 * 它封装了消息的所有内容和元信息,如消息类型(opcode)、消息体(payload)等。
 *
 * 优点:
 * - 智能指针会自动管理内存,无需手动释放;
 * - 在回调函数中广泛使用,便于获取和处理消息内容。
 *
 * 常用接口:
 * - msg->get_payload():
 *     获取消息体内容,返回 std::string,一般用于 text 类型的消息。
 *
 * - msg->get_opcode():
 *     获取消息的操作码(opcode),用于判断消息类型。
 *     常见的操作码包括:
 *     * websocketpp::frame::opcode::text   :文本消息
 *     * websocketpp::frame::opcode::binary :二进制消息
 *     * websocketpp::frame::opcode::close  :关闭消息
 *     * websocketpp::frame::opcode::ping   :Ping 心跳包
 *     * websocketpp::frame::opcode::pong   :Pong 心跳响应
 *
 *
 * -------------------------- 如何发送自定义结构体数据 --------------------------
 *
 *
 * WebSocket 传输本质上是基于字符串或二进制,因此自定义结构体需序列化。
 *
 * 【方法一】使用 JSON 序列化库(如 nlohmann/json):
 *     struct Person {
 *         std::string name;
 *         int age;
 *     };
 *
 *     void to_json(nlohmann::json& j, const Person& p) {
 *         j = nlohmann::json{{"name", p.name}, {"age", p.age}};
 *     }
 *
 *     void from_json(const nlohmann::json& j, Person& p) {
 *         j.at("name").get_to(p.name);
 *         j.at("age").get_to(p.age);
 *     }
 *
 *     // 发送端
 *     Person p = {"Alice", 25};
 *     nlohmann::json j = p;
 *     s->send(hdl, j.dump(), websocketpp::frame::opcode::text);
 *
 *     // 接收端
 *     std::string str = msg->get_payload();
 *     Person p2 = nlohmann::json::parse(str);
 *
 * 【方法二】自定义字符串转换函数:
 *     struct MyData {
 *         int id;
 *         std::string content;
 *     };
 *
 *     std::string MyData_to_string(const MyData& d) {
 *         return std::to_string(d.id) + "#" + d.content;
 *     }
 *
 *     MyData string_to_MyData(const std::string& s) {
 *         size_t pos = s.find("#");
 *         return {std::stoi(s.substr(0, pos)), s.substr(pos + 1)};
 *     }
 *
 *     // 发送时:
 *     MyData d = {42, "hello"};
 *     s->send(hdl, MyData_to_string(d), websocketpp::frame::opcode::text);
 *
 *     // 接收时:
 *     MyData d2 = string_to_MyData(msg->get_payload());
 *
 * 注意:
 * - 对于二进制消息,可以将结构体通过内存拷贝方式发送(适用于低层或固定格式)。
 * - 建议使用 JSON 或字符串拼接,兼容性更好,也便于调试和跨语言通信。
 */
typedef server::message_ptr message_ptr;



/*
 * 这是 WebSocket++ 中服务端收到消息时的回调函数。
 * 它的函数签名及每个参数含义如下:
 *
 * void on_message(server* s, connection_hdl hdl, message_ptr msg)
 *
 * 参数说明:
 *
 * 1. server* s
 *    - 类型:指向 WebSocket++ 服务端对象的指针。
 *    - 作用:用于通过当前服务器对象发送消息(如 s->send())或
 *          控制服务器行(如:s->stop_listening())。
 *    - 注意:需要用这个对象来调用发送消息的函数,不能直接用其他 server 实例。
 *
 * 2. websocketpp::connection_hdl hdl
 *    - 类型:connection_hdl 是 WebSocket++ 提供的 连接句柄(connection handle)。
 *    - 本质上是一个 std::weak_ptr<void>,可以唯一标识某个客户端连接。
 *    - 作用:
 *      - 用于标识和访问对应的连接。
 *      - 必须传入 send() 函数中作为目标连接。
 *      - 可通过 .lock() 获取实际连接对象的 shared_ptr(如果还未失效)。
 *
 * 3. message_ptr msg
 *    - 类型:message_ptr 是 WebSocket++ 提供的 消息智能指针,
 *          本质是std::shared_ptr<message>。
 *    - 作用:
 *      - 封装了收到的消息内容和元信息(如类型、大小、操作码)。
 *      - 可通过 msg->get_payload() 获取消息的实际内容(若是 text 类型,就是字符串)。
 *      - 可通过 msg->get_opcode() 获取消息的类型(例如 text_frame 表示文本消息)。
 *    - 优点:作为智能指针,它自动管理内存,无需手动释放。
 *
 * 总结:
 * - s:访问服务器的接口(可以发消息、控制服务器行为)。
 * - hdl:标识客户端连接,用于指明消息的发送对象。
 * - msg:保存了客户端发来的消息内容及元数据。
 *
 * ---------------------------------------------------------------------------
 *
 * WebSocket++ 四个核心事件说明(事件处理器):
 *
 * 1. set_open_handler
 *    - 触发时机:连接成功建立后(握手完成)。
 *    - 典型用途:初始化状态、发送欢迎消息、执行首次数据发送等。
 *
 * 2. set_message_handler
 *    - 触发时机:客户端或服务端收到消息时。
 *    - 参数包含消息内容和连接句柄,可用来解析消息、处理请求、做响应。
 *    - 本函数 on_message 就是该事件的回调处理器。
 *
 * 3. set_close_handler
 *    - 触发时机:连接关闭时。
 *    - 可用于释放资源、打印日志、更新连接状态等。
 *
 * 4. set_fail_handler
 *    - 触发时机:连接失败(如连接超时、握手失败)时。
 *    - 典型用途:错误重连、打印错误信息、上报失败原因等。
 *
 * 说明:所有事件处理器都接收一个 connection_hdl(连接句柄)作为参数,
 *       并可通过指针访问连接或发送数据。
 *
 * -------------------------------------------------------------------
 *
 * WebSocket++ 客户端连接事件流程图说明:
 *
 *               connect()
 *                   │
 *        ┌──────────┴───────────┐
 *        ▼                      ▼
 *    [连接失败]            [连接成功]
 *        │                      │
 * set_fail_handler       set_open_handler
 *                               │
 *                          send() / other 初始化操作
 *                               │
 *               ┌──────────────┴───────────────┐
 *               ▼                              ▼
 *    set_message_handler              set_close_handler
 *   (收到服务端消息时)             (连接断开时触发)
 *
 * 简要说明:
 * - connect():发起连接请求。
 * - set_fail_handler:若连接失败,如地址错误或网络异常,会触发该回调。
 * - set_open_handler:连接成功后触发,可以在此处发送初始化数据。
 * - set_message_handler:收到对方发来的消息时触发。
 * - set_close_handler:连接关闭时触发(正常关闭或异常断开都会触发)。
 *
 * 通常推荐的顺序是:
 * 1. init_asio()
 * 2. set_access_channels(...)  // 可选,用于调试输出
 * 3. 设置四个 handler(open, message, close, fail)
 * 4. get_connection() → connect() → run()
 */
void on_message(server* s, connection_hdl hdl, message_ptr msg) {
    std::cout << "[Server] Received message: " << msg->get_payload() << std::endl;

    // 如果收到特定命令,则停止监听(服务器不再接受新连接)
    if (msg->get_payload() == "stop-listening") {
        std::cout << "[Server] Stopping listening on port..." << std::endl;
        s->stop_listening();
        return;
    }

    // 尝试将原消息内容发回客户端(回显 echo)
    try { 
        // 给 hdl 标识的客户端发送类型为 msg->get_opcode() 的消息 msg->get_payload()
        s->send(hdl, msg->get_payload(), msg->get_opcode());
    } catch (const websocketpp::exception& e) {
        std::cout << "[Error] Echo failed: " << e.what() << std::endl;
    }
}

int main() {
    // 创建 WebSocket 服务器对象
    server echo_server;

    try {
        /*
         * 设置访问日志级别:
         * - alevel(access level):控制访问级别的日志,例如连接、断开、消息等。
         * - all:启用所有访问日志。调试阶段建议打开,可以看到连接、接收消息等细节。
         */
        echo_server.set_access_channels(websocketpp::log::alevel::all);

        /*
         * 可选项:关闭帧内容的日志,避免日志输出过多
         * - frame_payload 表示 WebSocket 帧的具体数据内容,通常是非常详细的。
         *   如果你不需要查看每一帧的 payload,建议禁用。
         */
        echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);
        
        /*
         * 初始化 Asio:
         * - 必须在设置完 log 之后调用。
         * - 该操作会初始化底层的 Boost.Asio 网络事件循环。
         */
        echo_server.init_asio();

        /*
         * 设置消息处理函数:
         * - 这是服务端最核心的回调函数之一。
         * - 当有客户端向服务器发送消息时,此回调函数会被调用。
         * - bind 的作用是将成员函数 `on_message` 绑定到 `echo_server` 实例上,
         *   并指定接收两个参数:connection_hdl 和 message_ptr。
         */
        echo_server.set_message_handler(
            websocketpp::lib::bind(
                &on_message,           // 指向处理函数的指针
                &echo_server,          // 传入当前服务器对象
                websocketpp::lib::placeholders::_1, // 第一个占位符参数:连接句柄
                websocketpp::lib::placeholders::_2  // 第二个占位符参数:消息指针
            )
        );

        // 设置监听端口(此处为 9002)
        echo_server.listen(9002);

        /*
         * 启动异步接受连接:
         * - 告诉服务器开始接受来自客户端的连接请求。
         * - 这个调用会在后台准备 socket、创建连接、初始化状态等。
         */
        echo_server.start_accept();

        std::cout << "[Server] Listening on ws://localhost:9002" << std::endl;

        /*
         * 启动事件循环:
         * - 这是 WebSocket++ 的主循环,类似于传统的 asio::io_service::run()
         * - 它会处理所有事件(连接、接收消息、发送消息等)
         * - 注意:这是一个阻塞函数,除非服务器被停止,否则不会返回。
         */
        echo_server.run();

    } catch (const websocketpp::exception& e) {
        std::cout << "[Exception] " << e.what() << std::endl;
    } catch (...) {
        std::cout << "[Exception] Unknown exception occurred." << std::endl;
    }

    return 0;
}

WebSocket 回显客户端代码简介

#include <websocketpp/config/asio_no_tls_client.hpp> 
#include <websocketpp/client.hpp>

#include <iostream>

typedef websocketpp::client<websocketpp::config::asio_client> client; 
// 定义 client 类型为使用 Asio 的无 TLS WebSocket 客户端

using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;
using websocketpp::lib::bind;

// 定义消息指针类型(本质是 shared_ptr,指向 message 类型)
typedef websocketpp::config::asio_client::message_type::ptr message_ptr;

// ==============================
// 客户端接收消息时的处理函数
// ==============================
void on_message(client* c, websocketpp::connection_hdl hdl, message_ptr msg) {
    std::cout << "on_message called with hdl: " << hdl.lock().get()
              << " and message: " << msg->get_payload()
              << std::endl;

    websocketpp::lib::error_code ec;

    // 收到的消息再发回服务器(echo)
    c->send(hdl, msg->get_payload(), msg->get_opcode(), ec);
    if (ec) {
        std::cout << "Echo failed because: " << ec.message() << std::endl;
    }
}

// ==============================
// 主函数
// ==============================
int main(int argc, char* argv[]) {
    client c; // 创建 WebSocket 客户端对象
    std::string uri = "ws://localhost:9002"; // 服务器的连接地址

    if (argc == 2) {
        uri = argv[1]; // 如果提供了命令行参数,就用参数作为 URI
    }

    try {
        // 开启日志输出(除了消息内容)
        c.set_access_channels(websocketpp::log::alevel::all);
        c.clear_access_channels(websocketpp::log::alevel::frame_payload);

        c.init_asio(); // 初始化 Asio(必须要调用)

        // 设置消息处理函数(收到服务器消息后调用)
        c.set_message_handler(bind(&on_message, &c, _1, _2));

        // 尝试建立连接(但还不会真的连接)
        websocketpp::lib::error_code ec;
        client::connection_ptr con = c.get_connection(uri, ec);
        if (ec) {
            std::cout << "could not create connection because: " << ec.message() << std::endl;
            return 0;
        }

        // 建立连接(注册连接请求)
        c.connect(con);

        // 启动 IO 事件循环(连接、收发消息都在这里完成)
        c.run(); // run 会阻塞直到连接关闭
    } catch (websocketpp::exception const & e) {
        std::cout << e.what() << std::endl;
    }
}