上一篇我们完成了项目环境的搭建,这一篇将深入 WebSocket 协议的原理,并使用 WebSocket++ 库搭建一个既能处理 HTTP 又能处理 WebSocket 的服务器,为后续的五子棋业务做好准备。

1. 为什么需要 WebSocket?

传统 HTTP 协议是“请求-响应”模式:客户端主动发起请求,服务器被动返回响应。服务器无法主动向客户端推送消息。

对于五子棋对战这类实时性要求高的场景,如果使用 HTTP 轮询(客户端每隔几秒询问一次),会有以下问题:

  • 实时性差(延迟取决于轮询间隔)
  • 资源浪费(大量无效请求)
  • 服务器压力大

WebSocket 应运而生,它:

  • 全双工:客户端和服务端可以随时向对方发送消息
  • 持久连接:握手成功后保持长连接,无需重复建立 TCP 连接
  • 低延迟:没有 HTTP 头部冗余,适合频繁交互

2. WebSocket 协议原理

2.1 握手过程

WebSocket 的握手通过 HTTP 请求升级协议完成。步骤如下:

  1. 客户端发送一个特殊的 HTTP GET 请求,包含 Upgrade: websocketConnection: Upgrade 头部。
  2. 服务器验证后返回 101 Switching Protocols 状态码,表示同意升级。
  3. 之后双方就在同一 TCP 连接上使用 WebSocket 帧格式通信。

客户端握手请求示例:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器成功响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

2.2 数据帧格式(简略)

WebSocket 传输的数据被分割成帧。我们主要关注几个字段:

  • FIN:是否为最后一帧(消息可能分多帧)
  • opcode:帧类型(文本、二进制、连接关闭、ping/pong)
  • Mask & Masking-Key:客户端发送给服务器的数据必须掩码,服务端发送不需要
  • Payload length:载荷长度(7位、7+16位、7+64位扩展)
  • Payload Data:实际数据(文本或二进制)

实际开发中,WebSocket++ 库已封装好帧解析与生成,我们无需手动处理这些细节。

3. WebSocket++ 库介绍

WebSocket++ 是一个跨平台、头文件为主的 C++ 库,实现了 RFC6455 标准,底层使用 Boost.Asio 或 C++11 标准库的 Asio。

主要特点:

  • 支持 HTTP 和 WebSocket(同时监听)
  • 支持客户端和服务端模式
  • 事件驱动(通过回调函数处理连接、消息、关闭等)
  • 线程安全(部分接口)

官方资源:

  • GitHub: https://github.com/zaphoyd/websocketpp
  • 文档: http://docs.websocketpp.org/

4. 环境安装(回顾)

上一篇我们已经安装过,再确认一下:

# Ubuntu 下
sudo apt install libboost-all-dev
git clone https://github.com/zaphoyd/websocketpp.git
cd websocketpp
mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
sudo make install

验证安装:ls /usr/include/websocketpp/ 应显示大量头文件。

5. 编写第一个 WebSocket++ 服务器

我们将创建一个 echo 服务器:客户端发来什么消息,服务器原样返回。

5.1 基础代码(echo_server.cpp)

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

using namespace std;

typedef websocketpp::server<websocketpp::config::asio> wsserver_t;
typedef wsserver_t::message_ptr message_ptr;

void on_open(wsserver_t* srv, websocketpp::connection_hdl hdl) {
    cout << "新连接建立" << endl;
}

void on_close(wsserver_t* srv, websocketpp::connection_hdl hdl) {
    cout << "连接关闭" << endl;
}

void on_message(wsserver_t* srv, websocketpp::connection_hdl hdl, message_ptr msg) {
    string payload = msg->get_payload();
    cout << "收到消息: " << payload << endl;
    // 原样返回
    srv->send(hdl, payload, msg->get_opcode());
}

int main() {
    wsserver_t server;

    // 关闭所有日志(可选)
    server.set_access_channels(websocketpp::log::alevel::none);

    // 初始化 asio
    server.init_asio();

    // 注册回调
    server.set_open_handler(bind(&on_open, &server, placeholders::_1));
    server.set_close_handler(bind(&on_close, &server, placeholders::_1));
    server.set_message_handler(bind(&on_message, &server, placeholders::_1, placeholders::_2));

    // 监听端口 9002
    server.listen(9002);
    server.start_accept();

    cout << "WebSocket 服务器启动,端口 9002" << endl;
    server.run();

    return 0;
}

5.2 编译与测试

g++ -std=c++11 echo_server.cpp -o echo_server -lpthread -lboost_system
./echo_server

使用浏览器控制台或在线 WebSocket 测试工具连接 ws://127.0.0.1:9002,发送消息,应该能收到相同回复。

5.3 同时支持 HTTP 静态页面

五子棋项目需要:HTTP 提供 HTML/CSS/JS 文件,WebSocket 处理游戏业务。WebSocket++ 允许在同一个端口同时处理两种协议。

我们添加一个 HTTP 回调函数:

#include <fstream>
#include <sstream>

void on_http(wsserver_t* srv, websocketpp::connection_hdl hdl) {
    auto conn = srv->get_con_from_hdl(hdl);
    string uri = conn->get_request().get_uri();

    // 简单处理根路径,返回 HTML
    string real_path = "./www/" + uri;
    if (uri == "/") real_path = "./www/index.html";

    ifstream file(real_path);
    if (file) {
        stringstream buf;
        buf << file.rdbuf();
        conn->set_body(buf.str());
        conn->set_status(websocketpp::http::status_code::ok);
        conn->append_header("Content-Type", "text/html");
    } else {
        conn->set_body("<h1>404 Not Found</h1>");
        conn->set_status(websocketpp::http::status_code::not_found);
    }
}

// 在 main 中注册
server.set_http_handler(bind(&on_http, &server, placeholders::_1));

这样,浏览器访问 http://127.0.0.1:9002 时能显示网页,同时 WebSocket 连接也走同一端口(路径需区分)。

6. 项目中的服务器设计思路

在五子棋对战中,我们会有两个不同的 WebSocket 端点:

  • 大厅 (/hall):用户匹配、在线状态
  • 房间 (/room):下棋、聊天

因此需要在 open_handler 中根据请求的 URI 分发到不同的处理逻辑。

6.1 区分 URI

void on_open(wsserver_t* srv, websocketpp::connection_hdl hdl) {
    auto conn = srv->get_con_from_hdl(hdl);
    string uri = conn->get_request().get_uri();
    if (uri == "/hall") {
        // 进入游戏大厅
    } else if (uri == "/room") {
        // 进入游戏房间
    }
}

类似地,on_messageon_close 也要根据 URI 分别处理。

6.2 管理连接

为了向特定用户推送消息,我们需要将用户 ID 与 connection_hdl 映射起来。后面篇章会详细介绍在线用户管理模块。

7. 本章小结

本篇我们学习了:

  • WebSocket 协议的核心原理:握手 + 全双工通信
  • WebSocket++ 库的基本使用方法
  • 搭建一个同时服务 HTTP 和 WebSocket 的服务器
  • 为五子棋项目设计区分 URI 的连接处理框架

下一步:我们将学习 JSON 数据格式与 JsonCpp 库,用于前后端结构化消息的序列化与反序列化。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐