【c++中间件】WebSocket介绍 && WebSocketpp库的使用

文章目录

安装

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

​ 安装完毕后,若在 /usr/include 下有了 websocketpp 目录就表示安装成功了。

shell 复制代码
ls /usr/include/websocketpp/

Ⅰ. WebSocket

一、简介

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

​ 传统的 web 程序都是属于 "一问一答" 的形式,即客户端给服务器发送了一个 HTTP 请求,服务器给客户端返回一个 HTTP 响应。这种情况下服务器是属于被动的一方,如果客户端不主动发起请求服务器就无法主动给客户端响应

​ 像网页即时聊天或者我们做的五子棋游戏这样的程序都是非常依赖 "消息推送" 的,即需要服务器主动推动消息到客户端。如果只是使用原生的 HTTP 协议,要想实现消息推送一般需要通过 "轮询" 的方式实现, 而轮询的成本比较高并且也不能及时的获取到消息的响应。

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

​ 并且要注意,WebSocket 和我们平时说的 Socket 是没有半毛钱关系的,注意区分开来!

二、特点

  • 建立在 TCP 协议之上,支持双向通信,实时性更强。
  • HTTP 协议有着良好的兼容性,握手阶段采用 HTTP 协议,默认端口是 80443
  • 数据格式比较轻量,性能开销小、通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是 ws (如果加密,则为 wss ),形式:ws://echo.websocket.org
  • 支持扩展。ws 协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

三、原理解析

WebSocket 协议本质上是一个基于 TCP 的协议。

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

​ 通过一些抓包来分析一下切换过程中 HTTP请求HTTP响应 的包有何不同:

HTTP请求

http 复制代码
GET /ws HTTP/1.1								// URL通常会设置为/ws表示这是websocket
Host: localhost:2021							
Upgrade: websocket                              // 希望升级的协议格式
Connection: Upgrade							  // 希望升级协议
Sec-Websocket-Key: mViTimINUhcF0fBHeX+wqA==		// 是客户端发送的一个base64编码的秘文,
Sec-Websocket-Version: 13					   // 该websocket的版本
  • Sec-Websocket-Key 是客户端发送的一个 base64 编码的秘文,要求服务端返回一个对应加密的 Sec-Websocket-Accept 应答,否则客户端会抛出 Error during WebSocket handshake 错误,并关闭连接。
  • Sec-Websocket-Version: 13 表示 websocket 的版本,如果服务端不支持该版本,需要返回一个 Sec-Websocket-Version 里面包含服务端支持的版本号。

HTTP响应

http 复制代码
HTTP/1.1 101 Switching Protocols                       	  // 表示服务端接受websocket协议的
Connection: Upgrade									   // 升级协议
Upgrade: websocket									   // 升级的协议格式
Sec-Websocket-Accept: YLcYR/p/mS8hENqlgMXtFTggdv8=
  • Sec-Websocket-Accept 是服务端采用与客户端一致的秘钥计算出来后返回客户端。
  • HTTP/1.1 101 Switching Protocols 表示服务端接受 WebSocket 协议的客户端连接。

四、报文格式

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

  • FIN
    • WebSocket 协议传输数据以消息为概念单位,⼀个消息有可能由⼀个或多个帧组成
    • 1 个比特大小:
      • 如果是 1,表示这是消息的最后一个分片
      • 如果是 0,表示这不是消息的最后一个分片
  • RSV1RSV2RSV3
    • 各占 1 个比特大小:
      • 保留字段,只在扩展时使用,若未启用扩展则应置 1 ,若收到不全为 0 的数据帧,且未协商扩展则立即终止连接。
  • Opcode
    • 4 个比特,标志当前数据帧的类型:
      • 0x0 :表示一个延续帧。当 Opcode0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
      • 0x1 :表示这是一个文本帧(frame
      • 0x2 :表示这是一个二进制帧(frame
      • 0x3-0x7:保留的操作代码,用于后续定义的非控制帧
      • 0x8 :表示连接断开
      • 0x9 :表示这是一个 ping 操作
      • 0xA :表示这是一个 pong 操作
      • 0xB-0xF:保留的操作代码,用于后续定义的控制帧
  • Mask
    • 表示是否要对数据载荷 也就是 Payload数据 字段进行掩码操作:
      • 从客户端向服务端发送数据时,需要 对数据进行掩码操作。如果服务端接收到的数据没有进行掩码操作,服务端需要断开连接!
      • 从服务端向客户端发送数据时,不需要 对数据进行掩码操作。
    • 1 个比特大小:
      • 如果为 1 ,那么 Masking-key 字段中会定义一个掩码键,并用这个掩码键对数据载荷进行反掩码。
  • Payload length
    • 数据载荷的长度,单位是 字节 。有可能为 7 位、7+16 位、7+64 位,假设 x 为数据载荷的长度,则其表示如下:
      • x[0,126) :数据的长度就是 Payload length 表示的大小
      • x126 :后续 2 个字节代表⼀个 16 位的无符号整数,该无符号整数的值为数据的长度
      • x127 :后续 8 个字节代表⼀个 64 位的无符号整数(最高位为 0),该无符号整数的值为数据的长度
  • Masking-key
    • 04 字节
      • Mask1 ,则包含 4 字节的 Masking-key
      • Mask0 ,则不包含 Masking-key
  • Payload data
    • 报文携带的载荷数据
    • 如果没有协商使用扩展的话,扩展数据为 0 字节。所有的扩展都必须声明扩展数据的长度,扩展如何使用必须在握手阶段就协商好

Ⅱ. WebSocketpp

一、介绍

WebSocketpp 是一个跨平台的开源(BSD 许可证)头部专用 C++库 ,它实现了 RFC6455WebSocket 协议)和 RFC7692WebSocketCompression Extensions )。它允许将 WebSocket 客户端和服务器功能集成到 C++ 程序中。

​ 在最常见的配置中,全功能网络 I/OAsio 网络库提供。

WebSocketpp 的主要特性包括:

  • 事件驱动的接口
  • ⽀持 HTTP/HTTPSWS/WSSIPv6
  • 灵活的依赖管理,如 Boost 库/ C++11标准库
  • 可移植性:Posix/Windows32/64 bit、Intel/ARM
  • 线程安全

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

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

二、常用接口

这里做一下下面接口和类的大概介绍:

  1. 下面的几个指定事件的回调事件,如 set_open_handler() 函数,它们都是用来 设置 针对不同事件而设置的处理函数,而处理函数是由我们自己来写的,因为 WebSocketpp 负责搭建服务器,它给不同的事件分配了不同的处理函数指针,比如 open_handler 其实就是握手成功后处理函数的指针,当服务器收到了指定的数据,触发了指定的事件之后,就会通过函数指针去调用我们自己写的那些处理函数!
  2. server 类是继承于 endpoint 类的 ,我们后面也是通过创建 server 类来完成搭建服务器的操作。
  3. connection 这个类也很重要,它提供了一些接口,用于处理连接的事件和状态 ,而 connection_hdl 是一个指向 connection 对象的轻量级句柄。它的作用是在 WebSocket 库内部,用于管理和操作 connection 对象,以及在回调函数中传递连接对象的引用。
  • 一般来说我们通过 server 类的 get_con_from_hdl() 函数,传入 connection_hdl 参数就能获得 connection_ptr 从而来操作 connection 类的函数!
  1. asio 是一个关键组件,主要用来处理底层的网络I/O操作asio 作为一个跨平台的库,提供了异步网络编程的能力,而 websocketpp 则构建在 asio 之上,实现了 WebSocket 协议。
cpp 复制代码
namespace websocketpp {
    
// 定义连接句柄类型,通常用于标识连接
typedef lib::weak_ptr<void> connection_hdl;

template <typename config>
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;     // 连接开启事件回调类型
    typedef lib::function<void(connection_hdl)> close_handler;    // 连接关闭事件回调类型
    typedef lib::function<void(connection_hdl)> http_handler;     // HTTP请求事件回调类型
    typedef lib::function<void(connection_hdl, message_ptr)> message_handler; // 消息接收事件回调类型

public:
    // 设置日志打印的等级
    void set_access_channels(log::level channels);

    // 清除指定的日志打印等级
    void clear_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); // 设置消息接收的回调函数
    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);

    // 初始化asio框架,必须在使用之前调用
    void init_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);

    // 取消定时器并立即触发回调函数
    std::size_t cancel();
};

template <typename config>
class server : public endpoint<connection<config>, config> {
    // 启动服务端的accept事件处理,即开始监听,要先于run()之前执行
    void start_accept();
};

template <typename config>
class connection : public config::transport_type::transport_con_type,
                   public config::connection_base {
public:
    // 发送数据
    error_code send(std::string& payload, frame::opcode::value op = frame::opcode::text);

    // 获取HTTP请求头部中指定key的值
    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请求对象
    request_type const& get_request();

    // 获取连接对应的连接句柄
    connection_hdl get_handle();
};

namespace http {
    namespace parser {

        // HTTP解析器类,用于解析HTTP头部和正文
        class parser {
        public:
            // 获取HTTP头部中指定key的值
            std::string const& get_header(std::string const& key);
        };

        // HTTP请求类,继承自parser类
        class request : public parser {
        public:
            // 获取请求方法
            std::string const& get_method();

            // 获取请求URI
            std::string const& get_uri();
        };
    }
}

namespace message_buffer {

    // 获取websocket请求中的payload数据类型
    frame::opcode::value get_opcode();

    // 获取websocket请求中的payload数据
    std::string const& get_payload();
}

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;  // 所有日志
    };
}

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 // 网络认证失败
        };
    }
}

namespace frame {
    namespace opcode {
        // WebSocket帧的操作码定义
        enum value {
            continuation = 0x0,  // 数据续传帧
            text = 0x1,          // 文本帧
            binary = 0x2,        // 二进制帧
            rsv3 = 0x3,          // 保留帧3
            rsv4 = 0x4,          // 保留帧4
            rsv5 = 0x5,          // 保留帧5
            rsv6 = 0x6,          // 保留帧6
            rsv7 = 0x7,          // 保留帧7
            close = 0x8,         // 关闭帧
            ping = 0x9,          // Ping帧
            pong = 0xA,          // Pong帧
            control_rsvb = 0xB,  // 控制帧RSVB
            control_rsvc = 0xC,  // 控制帧RSVC
            control_rsvd = 0xD,  // 控制帧RSVD
            control_rsve = 0xE,  // 控制帧RSVE
            control_rsvf = 0xF   // 控制帧RSVF
        };
    }
}

三、搭建服务器样例

  1. 实例化 server 对象
    • 一般我们使用 typedef websocketpp::server<websocketpp::config::asio> server_t 来定义服务器对象类型,防止实例化时候语句过长!
  2. 关闭日志输出
  3. 初始化 asio 和启用地址重用
  4. 设置回调函数
  5. 设置监听窗口,开始监听
  6. 启动服务器
cpp 复制代码
#include <websocketpp/config/asio_no_tls.hpp>
#include <websocketpp/server.hpp>

// 定义server类型
typedef websocketpp::server<websocketpp::config::asio> server_t;

void onOpen(websocketpp::connection_hdl hdl) {
    std::cout << "websocket长连接建立成功!\n";
}

void onClose(websocketpp::connection_hdl hdl) {
    std::cout << "websocket长连接断开!\n";
}

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::value::text);
}
 
int main()
{
    // 1. 实例化服务器对象
    server_t server;

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

    // 3. 初始化asio框架,启用地址重用
    server.init_asio();
    server.set_reuse_addr(true);

    // 4. 设置消息处理/连接握手成功/连接关闭回调函数
    server.set_open_handler(onOpen);
    server.set_close_handler(onClose);
    auto msg_hadler = std::bind(onMessage, &server, std::placeholders::_1, std::placeholders::_2);
    server.set_message_handler(msg_hadler);

    // 5. 设置监听端口,开始监听
    server.listen(9090);
    server.start_accept();

    // 6. 启动服务器
    server.run();
    return 0;
}

makefile 文件:

makefile 复制代码
main : main.cc
	g++ -std=c++17 $^ -o $@ -lpthread -lboost_system

客户端测试代码

​ 下面我们提供一个测试 websocket 的客户端代码,我们只需要生成一份 html 文件然后打开它即可!

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <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 实例
      // ws://192.168.51.100:8888
      // ws 表示 WebSocket 协议
      // 192.168.51.100 表示服务器地址
      // 8888 表示服务器绑定的端口
      let websocket = new WebSocket("ws://113.45.137.183:9090");

      // 处理连接打开的回调函数
      websocket.onopen = function() {
        console.log("连接建立");
      }

      // 处理收到消息的回调函数
      // 每当收到服务器的消息时,打印该消息
      websocket.onmessage = function(e) {
        console.log("收到消息: " + e.data);
      }

      // 处理连接异常的回调函数
      websocket.onerror = function() {
        console.log("连接异常");
      }

      // 处理连接关闭的回调函数
      websocket.onclose = function() {
        console.log("连接关闭");
      }

      // 实现点击按钮后,通过 WebSocket 实例向服务器发送请求
      let input = document.querySelector('#message'); // 获取输入框元素
      let button = document.querySelector('#submit'); // 获取提交按钮元素

      // 按钮点击事件
      button.onclick = function() {
        // 打印发送的消息
        console.log("发送消息: " + input.value);

        // 发送输入框中的内容到 WebSocket 服务器
        websocket.send(input.value);
      }
    </script>
  </body>
</html>

​ 在浏览器打开 html 文件后,建立长连接,可以在浏览器按 f12 进行查看:

​ 对话框输入内容后会有响应:

相关推荐
Zhao·o3 小时前
KafkaMQ采集指标日志
运维·中间件·kafka
Elias不吃糖4 小时前
LeetCode每日一练(209, 167)
数据结构·c++·算法·leetcode
Want5954 小时前
C/C++跳动的爱心②
c语言·开发语言·c++
初晴や4 小时前
指针函数:从入门到精通
开发语言·c++
铁手飞鹰4 小时前
单链表(C语言,手撕)
数据结构·c++·算法·c·单链表
无限进步_4 小时前
C语言动态内存管理:掌握malloc、calloc、realloc和free的实战应用
c语言·开发语言·c++·git·算法·github·visual studio
渡我白衣5 小时前
五种IO模型与非阻塞IO
运维·服务器·网络·c++·网络协议·tcp/ip·信息与通信
豐儀麟阁贵5 小时前
7.2内部类
java·开发语言·c++
FLPGYH5 小时前
从头开始c++ day4
开发语言·c++
dvlinker5 小时前
Windows中获取用户鼠标键盘闲置时间,以自动设置用户的离开状态以及处理离开状态下的自动消息回复(以QQ为例进行讲解,附源码)
c++·用户键盘闲置时间·lastinputinfo·sendinput·自动设置用户离开状态·自动回复消息