【视频点播系统】WebSocketpp 介绍及使用

WebSocketpp 介绍及使用

  • [一. Websocket 协议介绍](#一. Websocket 协议介绍)
    • [1. 原理解析](#1. 原理解析)
    • [2. 报文格式](#2. 报文格式)
  • [二. Websocketpp 介绍](#二. Websocketpp 介绍)
  • [三. WebSocketpp 安装](#三. WebSocketpp 安装)
  • [四. WebSocketpp 类与接口](#四. WebSocketpp 类与接口)
    • [1. 服务器类](#1. 服务器类)
    • [2. 日志等级](#2. 日志等级)
    • [3. 连接对象](#3. 连接对象)
    • [4. HTTP 相关类](#4. HTTP 相关类)
  • [五. Websocketpp 使用样例](#五. Websocketpp 使用样例)
    • [1. 目录结构](#1. 目录结构)
    • [2. 项目构建](#2. 项目构建)
    • [3. 代码实现](#3. 代码实现)
      • [1. 简单 HTTP/WebSocket 服务器](#1. 简单 HTTP/WebSocket 服务器)
      • [2. HTTP 客户端](#2. HTTP 客户端)
      • [3. Websocket 客户端](#3. Websocket 客户端)

一. Websocket 协议介绍

WebSocket 是从 HTML5 开始支持的一种网页端和服务端保持长连接的消息推送机制。

  • 传统的 web 程序都是属于 "一问一答" 的形式,即客户端给服务器发送了一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应。
  • 像网页即时聊天或者五子棋游戏这样的程序都是非常依赖 "消息推送" 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 "轮询" 的方式实现,而轮询的成本比较高并且也不能及时的获取到消息的响应。
    • 轮询 = 客户端定时向服务器发请求,是否有消息可以返回到客户端。
    • 长轮询 = 客户端问一次,服务器等到有消息再发送给客户端。

基于上述两个问题,就产生了 WebSocket 协议。WebSocket 更接近于 TCP 这种级别的通信方式,一旦连接建立完成客户端或者服务器都可以主动的向对方发送数据。

1. 原理解析

WebSocket 协议本质上是一个基于 TCP 的协议。为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程并升级协议的过程。

具体协议升级的过程如下:客户端向浏览器发送协议切换请求,服务器同意切换响应发送给客户端,之后客户端与服务器建立成功 websocket 协议,就可以进行长连接通信。

2. 报文格式

报文字段比较多,重点关注这几个字段:

  • FIN:WebSocket 传输数据以消息为概念单位,一个消息有可能由一个或多个帧组成,FIN 字段为1表示末尾帧。
  • RSV1~3:保留字段,只在扩展时使用,若未启用扩展则应置1,若收到不全为0的数据帧,且未协商扩展则立即终止连接。
  • opcode:标志当前数据帧的类型。
  • 0x0:表示这是个延续帧,当 opcode 为0表示本次数据传输采用了数据分片,当前收到的帧为其中一个分片。
    • 0x1:表示这是文本帧。
    • 0x2:表示这是二进制帧。
    • 0x3-0x7:保留,暂未使用。
    • 0x8:表示连接断开。
    • 0x9:表示 ping 帧。
    • 0xa:表示 pong 帧。
    • 0xb-0xf:保留,暂未使用。
  • mask:表示 Payload 数据是否被编码,若为1则必有 Mask-Key,用于解码 Payload 数据,仅客户端发送给服务端的消息需要设置。
  • Payload length:数据载荷的长度,单位是字节,有可能为7位、7+16位、7+64位,假设Payload length=x
    • x为0~126:数据的长度为x字节。
    • x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
    • x为127:后续8个字节代表一个64位的无符号整数 (最高位为0),该无符号整数的值为数据的长度。
  • Mask-Key:当 mask 为1,长度为4字节时,解码规则:DECODED[i]=ENCODED[i]MASK[i%4]
    Payload data:报文携带的载荷数据。

二. Websocketpp 介绍

WebSocketpp 是一个跨平台的开源 (BSD 许可证) 头部专用 C++ 库,它实现了 RFC6455 (WebSocket 协议) 和 RFC7692 (WebSocketCompression Extensions)。它允许将 WebSocket 客户端和服务器功能集成到 C++ 程序中。在最常见的配置中,全功能网络 I/O 由 Asio 网络库提供。

WebSocketpp 的主要特性包括:

  • 事件驱动的接口。
  • 支持 HTTP/HTTPS、WS/WSS、IPv6
  • 灵活的依赖管理 --- Boost 库/C++11 标准库。
  • 可移植性:Posix/Windows、32/64bit、Intel/ARM
  • 线程安全。

WebSocketpp 同时支持 HTTP 和 Websocket 两种网络协议,比较适用于本次的项目,所以选用该库作为项目的依赖库用来搭建 HTTP 和 WebSocket 服务器。

下面是该项目的一些常用网站:

三. WebSocketpp 安装

bash 复制代码
sudo apt-get install libboost-dev libboost-system-dev libwebsocketpp-dev

在环境搭建中已经内置 websocketpp,这里不需要再次安装了。

四. WebSocketpp 类与接口

1. 服务器类

cpp 复制代码
namespace websocketpp {
    typedef lib::weak_ptr<void> connection_hdl; // 连接句柄
    template <typename config>
    // 端点类:用于管理WebSocket连接的类
    class endpoint : public config::socket_type {
        typedef lib::shared_ptr<lib::asio::steady_timer> timer_ptr; // 定时器指针
        typedef typename connection_type::ptr connection_ptr; // 连接指针
        typedef typename connection_type::message_ptr message_ptr; // 消息指针

        typedef lib::function<void(connection_hdl)> open_handler; // WebSocket 连接成功时的回调函数
        typedef lib::function<void(connection_hdl)> close_handler; // WebSocket 连接关闭时的回调函数
        typedef lib::function<void(connection_hdl, message_ptr)> message_handler; // WebSocket 收到消息时的回调函数
        typedef lib::function<void(connection_hdl)> http_handler; // http 请求的回调函数

        void set_access_channels(log::level channels); // 设置允许访问的日志等级
        void clear_access_channels(log::level channels); // 清除指定等级的允许访问的日志等级
        void set_open_handler(open_handler h); // 设置 WebSocket 连接成功时的回调函数
        void set_close_handler(close_handler h); // 设置 WebSocket 连接关闭时的回调函数
        void set_message_handler(message_handler h); // 设置 WebSocket 收到消息时的回调函数
        void set_http_handler(http_handler h); // 设置 http 请求回调函数
        void send(connection_hdl hdl, std::string& payload, frame::opcode::value op); // 发送字符串数据
        void send(connection_hdl hdl, void* payload, size_t len, frame::opcode::value op); // 发送二进制数据
        void close(connection_hdl hdl, close::status::value code, std::string& reason); // 关闭连接
        connection_ptr get_con_from_hdl(connection_hdl hdl); // 获取连接指针
        void init_asio(); // 初始化asio框架
        void set_reuse_addr(bool value); // 设置是否启用地址重用
        void listen(uint16_t port); // 设置监听端口
        std::size_t run(); // 运行服务器
        timer_ptr set_timer(long duration, timer_handler callback); // 设置定时器
    };
    // 服务端类:用于处理WebSocket服务端的类
    template <typename config>
    class server : public endpoint<connection<config>, config> {
        void start_accept(); // 启动接受连接
    };
    // 消息缓冲区类:用于存储和管理WebSocket消息的类
    namespace message_buffer {
        frame::opcode::value get_opcode(); // 获取数据类型
        std::string const & get_payload(); // 获取数据内容
    };
}

2. 日志等级

websocketpp 库内有人家自己的日志输出模块 (无法替换,除非修改库内源码中所有的日志输出操作后重新编译库进行安装),这里主要是了解他的日志级别,将不需要的日志输出给禁用掉,避免其运行时的大量日志输出影响我们的视线。

cpp 复制代码
namespace log {
    struct alevel {
        static level const none = 0x0; // 无日志
        static level const connect = 0x1; // 连接日志
        static level const disconnect = 0x2; // 断开日志
        static level const control = 0x4; // 控制帧日志
        static level const frame_header = 0x8; // 帧头日志
        static level const frame_payload = 0x10; // 帧体日志
        static level const message_header = 0x20; // 消息头日志
        static level const message_payload = 0x40; // 消息体日志
        static level const endpoint = 0x80; // 端点日志
        static level const debug_handshake = 0x100; // 握手日志
        static level const debug_close = 0x200; // 关闭日志
        static level const devel = 0x400; // 开发日志
        static level const app = 0x800; // 应用日志
        static level const http = 0x1000; // http日志
        static level const fail = 0x2000; // 失败日志
        static level const access_core = 0x00003003; // 核心访问日志
        static level const all = 0xffffffff; // 所有日志
    };
}

3. 连接对象

cpp 复制代码
template <typename config>
// 连接类:用于处理HTTR/WebSocket连接的类
class connection : public config::transport_type::transport_con_type, public config::connection_base {
    error_code send(std::string&payload, frame::opcode::value op=frame::opcode::text); // 发送字符串数据
    std::string const & get_request_header(std::string const & key); // 获取 http 请求头部字段值
    std::string const & get_request_body(); // 获取 http 请求正⽂
    void set_status(http::status_code::value code); // 设置 http 响应状态码
    void set_body(std::string const & value); // 设置 http 响应正⽂
    void append_header(std::string const & key, std::string const & val); // 添加http响应头部字段
    typedef websocketpp::http::parser::request request_type; // http 请求对象类型
    request_type const & get_request(); // 获取 http 请求对象
    connection_hdl get_handle(); // 获取连接句柄
};

namespace frame {
    namespace opcode {
        // 数据类型枚举
        enum value {
            continuation = 0x0, // 继续帧
            text = 0x1, // 文本帧
            binary = 0x2, // 二进制帧
            rsv3 = 0x3, // 保留帧
            rsv4 = 0x4, // 保留帧
            rsv5 = 0x5, // 保留帧
            rsv6 = 0x6, // 保留帧
            rsv7 = 0x7, // 保留帧
            close = 0x8, // 关闭帧
            ping = 0x9, // 心跳帧
            pong = 0xA, // 心跳响应帧
            control_rsvb = 0xB, // 保留控制帧
            control_rsvc = 0xC, // 保留控制帧
            control_rsvd = 0xD, // 保留控制帧
            control_rsve = 0xE, // 保留控制帧
            control_rsvf = 0xF, // 保留控制帧
        };
    }
}

4. HTTP 相关类

cpp 复制代码
namespace http {
    namespace parser {
        // 解析http请求
        class parser {
            typedef std::map<std::string, std::string, utility::ci_less > header_list; // http请求头列表
            header_list const & get_headers() // 获取http请求头列表
            std::string const & get_header(std::string const & key) // 获取http请求头
            std::string const & get_body()  // 获取http请求体
        }
        // http请求类:用于解析http请求的类
        class request : public parser {
            std::string const & get_method() // 获取http请求方法
            std::string const & get_uri() // 获取http请求uri
            std::string const & get_version() // 获取http请求版本
        };
    }
};

namespace http {
    namespace status_code {
        // http状态码枚举
        enum value {
            uninitialized = 0, // 未初始化
            continue_code = 100, // 继续
            switching_protocols = 101, // 切换协议

            ok = 200, // 成功
            created = 201, // 创建成功
            accepted = 202, // 已接收
            non_authoritative_information = 203, // 非授权信息
            no_content = 204, // 无内容
            reset_content = 205, // 重置内容
            partial_content = 206, // 部分内容
            
            multiple_choices = 300, // 多选择
            moved_permanently = 301, // 永久移动
            found = 302, // 临时移动
            see_other = 303, // 参见其他
            not_modified = 304, // 未修改
            use_proxy = 305, // 使用代理
            temporary_redirect = 307, // 临时重定向

            bad_request = 400, // 错误请求
            unauthorized = 401, // 未授权
            payment_required = 402, // 需要支付
            forbidden = 403, // 禁止访问
            not_found = 404, // 未找到
            method_not_allowed = 405, // 不允许的请求方法
            not_acceptable = 406, // 不可接受的媒体类型
            proxy_authentication_required = 407, // 需要代理授权
            request_timeout = 408, // 请求超时
            conflict = 409, // 冲突
            gone = 410, // 已删除
            length_required = 411, // 需要内容长度
            precondition_failed = 412, // 预条件失败
            request_entity_too_large = 413, // 请求实体过大
            request_uri_too_long = 414, // 请求uri过长
            unsupported_media_type = 415, // 不支持的媒体类型   
            request_range_not_satisfiable = 416, // 请求范围不满足
            expectation_failed = 417, // 期望失败
            im_a_teapot = 418, // 我是一个茶壶
            upgrade_required = 426, // 需要升级
            precondition_required = 428, // 需要预条件
            too_many_requests = 429, // 太多请求
            request_header_fields_too_large = 431, // 请求头字段过大
            
            internal_server_error = 500, // 内部服务器错误
            not_implemented = 501, // 未实现
            bad_gateway = 502, // 错误网关
            service_unavailable = 503, // 服务不可用
            gateway_timeout = 504, // 网关超时
            http_version_not_supported = 505, // 不支持的http版本
            not_extended = 510, // 未扩展
            network_authentication_required = 511 // 需要网络授权
        };
    }
}

五. Websocketpp 使用样例

1. 目录结构

bash 复制代码
webscoketpp/
|-- makefile
|-- server.cc

2. 项目构建

bash 复制代码
# makefile
server: server.cc
	g++ -o $@ $^ -std=c++17 -lboost_system -pthread

.PHONY: clean
clean:
	rm -f server

3. 代码实现

1. 简单 HTTP/WebSocket 服务器

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

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

// WebSocket 连接成功时的回调函数
void onOpen(websocketsvr_t* server, websocketpp::connection_hdl hdl) {
    std::cout << "WebSocket 握手成功" << std::endl;
}
// WebSocket 连接关闭时的回调函数
void onClose(websocketsvr_t* server, websocketpp::connection_hdl hdl) {
    std::cout << "WebSocket 连接关闭" << std::endl;
}
// WebSocket 收到消息时的回调函数
void onMessage(websocketsvr_t* server, websocketpp::connection_hdl hdl, websocketsvr_t::message_ptr msg) {
    // WebSocket 是由 HTTP 升级而来的,所以连接中依然保存了 HTTP 请求的信息
    // 1.获取连接对象
    websocketsvr_t::connection_ptr conn = server->get_con_from_hdl(hdl);
    // 2.获取请求对象
    websocketpp::http::parser::request req = conn->get_request();
    // 3.打印请求信息
    std::cout << req.get_method() << " " << req.get_uri() << " " << req.get_version() << std::endl;
    for (auto& header : req.get_headers()) {
        std::cout << header.first << ": " << header.second << std::endl;
    }
    std::cout << std::endl;
    std::cout << req.get_body() << std::endl;
    std::cout << "收到 WebSocket 消息:" << msg->get_payload() << std::endl;
    // 4.回复消息
    conn->send("回显:" + msg->get_payload(), websocketpp::frame::opcode::text);
}
// HTTP 请求时的回调函数
void onHttp(websocketsvr_t* server, websocketpp::connection_hdl hdl) {
    // 1.获取连接对象
    websocketsvr_t::connection_ptr conn = server->get_con_from_hdl(hdl);
    // 2.获取请求对象
    websocketpp::http::parser::request req = conn->get_request();
    // 3.打印请求信息
    std::cout << req.get_method() << " " << req.get_uri() << " " << req.get_version() << std::endl;
    for (auto& header : req.get_headers()) {
        std::cout << header.first << ": " << header.second << std::endl;
    }
    std::cout << std::endl;
    std::cout << req.get_body() << std::endl;
    // 4.设置响应信息
    conn->set_status(websocketpp::http::status_code::value::ok);
    conn->set_body("<html><body><h1>Hello World</h1></body></html>");
    conn->append_header("Content-Type", "text/html");
}

int main(int argc, char* argv[]) 
{
    // 1.初始化服务器
    websocketsvr_t server;
    // 2.禁用日志
    server.clear_access_channels(websocketpp::log::alevel::all);
    // 3.设置回调函数
    server.set_open_handler(std::bind(&onOpen, &server, std::placeholders::_1));
    server.set_close_handler(std::bind(&onClose, &server, std::placeholders::_1));
    server.set_message_handler(std::bind(&onMessage, &server, std::placeholders::_1, std::placeholders::_2));
    server.set_http_handler(std::bind(&onHttp, &server, std::placeholders::_1));
    // 4.初始化asio框架
    server.init_asio();
    // 5.监听端口
    server.listen(9000);
    // 6.开始接受连接
    server.start_accept();
    // 7.运行服务器
    server.run();

    return 0;
}

2. HTTP 客户端

使用浏览器作为 http 客户端即可,访问自己设置的服务器 IP 地址和端口号。

3. Websocket 客户端

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test WebSocket</title>
</head>

<body>
    <input type="text" id="message" />
    <button id="submit">提交</button>

    <script>
        // 创建 WebSocket 实例
        const websocket = new WebSocket("ws://192.168.174.128:9000");
        // WebSocket 连接建立成功时的回调函数
        websocket.onopen = function () {
            console.log("连接建立");
        };
        // WebSocket 收到服务器消息时的回调函数
        websocket.onmessage = function (e) {
            // console.log("收到消息: " + e.data);
            alert("收到消息: " + e.data);
        };
        // WebSocket 连接发生错误时的回调函数
        websocket.onerror = function () {
            console.log("连接异常");
        };
        // WebSocket 连接关闭时的回调函数
        websocket.onclose = function () {
            console.log("连接关闭");
        };
        // 点击按钮发送消息
        const input = document.querySelector("#message");
        const button = document.querySelector("#submit");
        button.onclick = function () {
            console.log("发送消息: " + input.value);
            websocket.send(input.value);
        };
    </script>
</body>
</html>

在控制台中我们可以看到连接建立、客户端和服务器通信以及断开连接的过程 (关闭服务器就会看到断开连接的现象),通过 F12 打开浏览器的调试模式。

相关推荐
爱吃大芒果3 小时前
Flutter for OpenHarmony 实战:mango_shop 路由系统的配置与页面跳转逻辑
开发语言·javascript·flutter
学***54233 小时前
如何轻松避免网络负载过大
开发语言·网络·php
RANCE_atttackkk3 小时前
Springboot+langchain4j的RAG检索增强生成
java·开发语言·spring boot·后端·spring·ai·ai编程
梵刹古音3 小时前
【C语言】 格式控制符与输入输出函数
c语言·开发语言·嵌入式
Acrelhuang4 小时前
工商业用电成本高?安科瑞液冷储能一体机一站式解供能难题-安科瑞黄安南
大数据·开发语言·人工智能·物联网·安全
hello 早上好4 小时前
03_JVM(Java Virtual Machine)的生命周期
java·开发语言·jvm
沐雪架构师4 小时前
LangChain 1.0 Agent开发实战指南
开发语言·javascript·langchain
tod1134 小时前
力扣高频 SQL 50 题阶段总结(四)
开发语言·数据库·sql·算法·leetcode
2501_940007894 小时前
Flutter for OpenHarmony三国杀攻略App实战 - 战绩记录功能实现
开发语言·javascript·flutter