一、介绍

版本:WebSocket++ (0.8.2)

1、readme.md

参照readme.md

WebSocket++是一个只有头文件(只有hpp文件)的c++库,它实现了RFC6455(WebSocket协议)。

它允许将WebSocket客户端和服务端集成到c++程序中。

它使用了可互换的网络传输模块,包括:

基于原始字符缓冲区的模块

基于c++的iostreams的模块

基于Asio的模块(Boost版或独立版)

最终用户可以根据需要编写额外的传输策略来支持其他网络或事件库。

主要特性

完全支持RFC6455

部分支持Hixie 76/Hybi 00, 07-17草案(仅支持服务端)

基于接口的消息/事件

支持安全websocket(TLS),ipv6和显式代理

灵活的依赖关系管理(c++标准库或Boost)

可互换的网络传输模块(raw, iostream, Asio, or custom)、

便携式/跨平台(Posix/Windows, 32/64bit, Intel/ARM/PPC)

线程安全

2、 开始

参照docs/getting_started.dox

2.1 参考

代码:https://github.com/zaphoyd/websocketpp

官方项目主页:http://www.zaphoyd.com/websocketpp

使用问题:http://groups.google.com/group/websocketpp/

bugs:https://github.com/zaphoyd/websocketpp/issues

2.2 目录

  • docs: 文档
  • examples: 示例程序,演示如何为WebSocket客户端和服务器构建一些常用模式的基本版本。
  • test: 单元测试,可以确认您的代码是否正常工作,并有助于检测特定于平台的问题。
  • tutorials: 教程, 选择一组示例程序的详细演练。
  • websocketpp: 所有的库代码及默认配置文件。

2.3 使用

WebSocket++仅有头文件。可通过在程序中包含websocketpp源目录和WebSocket++头文件开始使用。也可以包含并且/或链接合适的Boost/system库。更多信息参考示例程序

WebSocket++包含了cmake和scons脚本,供构建示例程序和单元测试使用。除非您想以自动化的方式构建测试或示例,否则这两个系统都不需要。

2.4 编译

参考:websocketpp-0.8.2/examples/sip_client/README.txt

参考:https://blog.csdn.net/weixin_29274337/article/details/116775859

在解压后第一层目录下输入:

mkdir build
cd build

注意CMakeLists.txt中218行会导致cmake编译错误,需要进行

将find_package (Boost 1.39.0 COMPONENTS "${WEBSOCKETPP_BOOST_LIBS}")中的引号删去,如下

find_package (Boost 1.39.0 COMPONENTS ${WEBSOCKETPP_BOOST_LIBS})

输入

cmake -G 'Unix Makefiles' \
     -D BUILD_EXAMPLES=ON \
     -D WEBSOCKETPP_ROOT=/usr/local/include/ \
     -D ENABLE_CPP11=OFF ..

编译例子

make -C ../examples/sip_client

运行

bin/sip_client ws://ws-server:80

3、 教程

目录:tutorials

平台:ubuntu18.04.6

boost版本:1.83.0,通过源码安装

注:此例中的编译和程序均根据运行环境进行修改并执行通过

3.1 客户端

参照tutorials/utility_client/utility_client.md

3.1.1 初始设置和基础

主要内容:设置基础类型,打开和关闭连接,发送和接收消息

步骤1:用户交互
①目标

用户交互

设置基本的程序循环提示用户输入命令,然后进行处理,之后将修改此程序以执行任务并通过WebSocket连接从远程服务器取数据。

②构建
clang++ step1.cpp
③代码
#include <iostream>
#include <string>

int main() {
    bool done = false;
    std::string input;

    while (!done) {
        std::cout << "Enter Command: ";
        std::getline(std::cin, input);

        if (input == "quit") {
            done = true;
        } else if (input == "help") {
            std::cout 
                << "\nCommand List:\n"
                << "help: Display this help text\n"
                << "quit: Exit the program\n"
                << std::endl;
        } else {
            std::cout << "Unrecognized Command" << std::endl;
        }
    }

    return 0;
}

循环接受用户输入,若命令为quit则代码结束退出,若命令为help打印帮助信息,其他为未识别的命令。

步骤2:头文件+端点
①目标

添加WebSocket++头文件并设置端点类型

#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>
typedef websocketpp::client<websocketpp::config::asio_client> client;

两个对象类型:端点和连接

端点:创建、启动新连接,维护连接的默认设置,管理共享的网络资源

连接:存储每个WebSocket对话的具体信息

注释:

一旦连接建立,端点和连接之间毫无关系,所有默认设置从端点被复制到新连接中。端点上默认设置的改变只影响未来的连接。

连接不会维护相关的端点,店端不会维护未完成的连接列表。若应用程序需要迭代所有的连接需要自己维护列表。

构建端点:WebSocket++端点是通过将端点角色与端点配置相结合来构建的,有两种不同类型的端点角色,一种用于WebSocket会话中的客户端和服务器角色。

客户端角色:websocketpp::client,头文件<websocketpp/client.hpp>

术语: Endpoint Config端点配置

WebSocket++端点有一组设置,这些设置可以在编译时通过“config”模板参数进行配置。config是一个结构体,它包含用于生成具有特定属性的端点的类型和静态常量。根据使用的config,端点将具有不同的可用方法,并且可能具有额外的第三方依赖关系。

config设置:端点角色采用一个名为“config”的模板参数,该参数用于在编译时配置端点的行为。在本例中,我们将使用名为“asio_client”的库提供的默认配置,该库由<websockettpp/config/asio_no_tls_client.hpp>提供。这是一个使用boost::asio提供网络传输的客户端配置,不支持基于TLS的安全性。稍后,我们将讨论如何将基于TLS的安全性引入WebSocket++应用程序,更多关于其他备用配置的信息,以及如何构建自己的自定义配置。

将配置config与端点角色组合以生成完全配置的端点。这种类型会经常使用,所以建议在这里使用typedef。

typedef websocketpp::client<websocketpp::config::asio_client> client;
②构建
clang++ step2.cpp -lboost_system -lboost_thread -lpthread

依赖:

1° WebSocket++和Boost库头必须位于构建系统的include搜索路径中。

  • boost头文件及库文件依赖

在此使用的是boost_1_83_0版本,在ubuntu上下载源码编译安装

  • WebSocket++头文件依赖

将头文件拷贝至系统目录下:cp -r websocketpp /usr/local/include/

2° 链接库,后两个是在ubuntu平台添加

③代码
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>

#include <iostream>
#include <string>

typedef websocketpp::client<websocketpp::config::asio_client> client;

int main() {
    bool done = false;
    std::string input;

    while (!done) {
        std::cout << "Enter Command: ";
        std::getline(std::cin, input);

        if (input == "quit") {
            done = true;
        } else if (input == "help") {
            std::cout
                << "\nCommand List:\n"
                << "help: Display this help text\n"
                << "quit: Exit the program\n"
                << std::endl;
        } else {
            std::cout << "Unrecognized Command" << std::endl;
        }
    }

    return 0;
}

较步骤一添加了头文件和客户端端点类型的别名(通过将端点角色与端点配置相结合构建),主函数未调用,不具备测试点

typedef websocketpp::client<websocketpp::config::asio_client> client;
步骤3:端点包装器
①目标

创建端点包装器对象:处理初始化和后台线程的创建

为了在后台进行网络处理时处理用户输入,我们将为WebSocket++处理循环使用一个单独的线程。这使得主线程可以自由处理前台用户输入。为了为线程和端点实现简单的RAII风格的资源管理,我们将使用一个包装器对象,在其构造函数中对它们进行配置。

术语: websocketpp::lib namespace

WebSocket++被设计为与C++11标准库一起使用。由于这在流行的构建系统中并不普遍可用,Boost库可以在C++98构建环境中用作C++11标准库的充当库。库及其相关示例使用 websocketpp::lib 命名空间来抽象两者之间的区别。websocketpp::lib::shared_ptr 将在C++11环境中为std::shared_ptr,否则为boost::shared_ptr

由于读者构建环境位置,本教程使用websocketpp::lib包装器。对于您的应用程序,除非您对类似的可移植性感兴趣,否则可以直接使用这些类型的boost或std版本。

websocket_endpoint 构造函数中:

1° 清除日志:通过清除所有访问和错误日志记录通道,将端点日志记录行为设置为静默。

m_endpoint.clear_access_channels(websocketpp::log::alevel::all);
m_endpoint.clear_error_channels(websocketpp::log::elevel::all);

2° 初始化:初始化端点下的传输系统并设置为永久模式(端点的处理循环在没有连接时不会自动退出)

(初始化了一个异步传输层(ASIO)并启动了一个永久循环,以保持WebSocket连接活动)

这一点很重要,因为我们希望这个端点在应用程序运行时保持活动状态,并根据需要处理新WebSocket连接的请求。这两种方法都是特定于asio传输的。它们将不必要,也不存在于使用非asio配置的端点中。

m_endpoint.init_asio();
m_endpoint.start_perpetual();

3° run:启动线程已运行客户端端点的run方法(它创建了一个线程来运行client::run方法,该线程由m_thread智能指针管理。)

当端点运行时,它将处理连接任务(读取和传递传入消息、构建和发送传出的消息等)。因为它是在永久模式下运行的,所以当没有活动的连接时,它将等待新的连接。

// m_thread.reset(new websocketpp::lib::thread(&client::run, &m_endpoint));
// 使用共享指针来管控
m_thread = websocketpp::lib::make_shared<websocketpp::lib::thread>(&client::run, &m_endpoint);
②构建

依赖:random和thread库

现在,我们的客户端端点模板实际上已经实例化,将显示更多的链接依赖项。

1° WebSocket客户端需要一个加密安全的随机数生成器,可使用boost_random或c++11标准库random

2° 此例也使用了线程,若c++11的std::thread无法使用,需要包含boost_thread

clang++ step3.cpp -lboost_system -lboost_random -lpthread
③代码
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>

#include <websocketpp/common/thread.hpp>
#include <websocketpp/common/memory.hpp>

#include <iostream>
#include <string>

typedef websocketpp::client<websocketpp::config::asio_client> client;

class websocket_endpoint {
public:
    websocket_endpoint () {
        m_endpoint.clear_access_channels(websocketpp::log::alevel::all);
        m_endpoint.clear_error_channels(websocketpp::log::elevel::all);

        m_endpoint.init_asio();
        m_endpoint.start_perpetual();

        m_thread = websocketpp::lib::make_shared<websocketpp::lib::thread>(&client::run, &m_endpoint);
    }
private:
    client m_endpoint;
    websocketpp::lib::shared_ptr<websocketpp::lib::thread> m_thread;
};

int main() {
    bool done = false;
    std::string input;
    websocket_endpoint endpoint;

    while (!done) {
        std::cout << "Enter Command: ";
        std::getline(std::cin, input);

        if (input == "quit") {
            done = true;
        } else if (input == "help") {
            std::cout 
                << "\nCommand List:\n"
                << "help: Display this help text\n"
                << "quit: Exit the program\n"
                << std::endl;
        } else {
            std::cout << "Unrecognized Command" << std::endl;
        }
    }

    return 0;
}

较步骤2添加了websocket_endpoint客户端端点包装器类,此类包括

私有变量

1° 客户端对象m_endpoint

2° 单独处理的后台线程m_thread(使用了共享指针指向线程)

构造函数websocket_endpoint:初始化操作-

​ 1° 配置日志

​ 2° 初始化异步传输层和设置永久模式

​ 3° 它创建了一个线程来运行client::run方法,该线程由m_thread智能指针管理。

并在主函数创建了此类的一个实例。

步骤4:打开连接
①目标

打开WebSocket连接

此步骤增加了两个utility_client的命令:打开新连接的能力以及查看有关先前打开的连接信息的能力。每个打开的连接都会被分配一个整型连接id,程序用户可以使用该id与该连接交互。

1° 新连接元数据对象

为了跟踪每个连接的信息,定义了connection_metadata对象。此对象存储数字类型的连接id和处理连接时填充的许多字段。最初,这包括连接的状态(opening, open, failed, closed, etc),连接到原始URI,服务端的标识值及连接失败/关闭的原因描述。未来的步骤将向该元数据对象添加更多信息。

2° 更新websocket_endpoint

websocket_endpoint已经定义了一些新数据成员和方法。它现在需要跟踪连接IDs及其相关元数据之间的映射,以及需要分发下一个顺序ID号。connect()方法启动一个新连接。get_metadata方法获取给定ID的元数据。

3° connect方法

一个新的WebSocket连接是通过一个三步过程启动的。首先,连接请求由endpoint::get_connection(uri)创建。接下来,配置连接请求。最后,连接请求通过endpoint::connect()提交回端点,后者将其添加进要建立的新连接的队列中。

3.1° 连接请求由endpoint::get_connection(uri)创建

术语:connection_ptr

WebSocket++使用引用计数的共享指针跟踪与连接相关的资源。此指针的类型为endpoint::connection_ptr。一个connection_ptr允许直接访问与连接有关的信息并允许改变设置。由于这种直接访问和在库中的内部资源管理角色,除非在下面详细说明的特定情况下,否则最终应用程序使connection_ptr是不安全的。

什么时候使用connection_ptr是安全的?

  • endpoint::get_connection(...)后和在endpoint::connect()之前: get_connection 返回connection_ptr。使用此指针配置新连接是安全的。一旦将连接提交给connect,就不能再使用connection_ptr,应该立即将其丢弃,以实现最佳内存管理。
  • 在处理程序期间:webSocket++允许为连接生命周期内发生的特定事件注册hooks / callbacks / event 处理程序handlers。在调用其中一个处理程序handler期间,库保证了对当前运行的处理程序关联的连接使用connection_ptr` 是安全的。

术语:connection_hdl

由于connection_ptr 的线程安全有限,库还提供了更灵活的连接标识符connection_hdlconnection_hdl的类型为websocketpp::connection_hdl,在<websocketpp/common/connection_hdl.hpp>中定义。请注意,与connection_ptr不同,这不取决于端点的类型或配置。简单存储或传输connection_hdl但不使用它们的代码只能包括上面的头,并且可以像对待hdl一样对待其值(???)。

直接使用连接句柄。通过端点的识别所需操作目标的方法使用。例如,端点的发送新消息的方法把以发送消息连接的hdl作为参数。

什么时候使用connection_hdl是安全的?

connection_hdl可以在任意时候从任意线程使用。可被复制和存储在容器中。删除hdl不会影响连接。在处理程序handler调用期间,可以使用endpoint::get_con_from_hdl()将句柄handles升级为connection_ptr。生成的connection_ptr`在该处理程序handler调用期间可以安全使用。

什么时候使用“connection_hdl”是安全的
connection_hdl可以在任何时候从任何线程使用。它们可以被复制并存储在容器中。删除hdl不会以任何方式影响连接。在处理程序调用期间,可以使用endpoint::get_con_from_hdl()将句柄升级为connection_ptr。生成的connection_ptr在该处理程序调用期间可以安全使用。

connection_hdl FAQs

  • 保证connection_hdls在程序中是唯一的。单个程序的多个端点将始终创建具有唯一句柄handles的连接。
  • 使用与创建其关联连接的端点不同的端点connection_hdl将导致未定义的行为。
  • 使用关联连接已经被关闭或者删除的 connection_hdl安全的。端点会返回特定错误表示:因为关联连接不存在,操作无法完成。

websocket_endpoint::connect() 首先调用endpoint::get_connection()(使用uri为参数的)。此外,还会传递一个错误输出值,以捕获期间可能发生的任何错误。如果确实发生错误,则会打印一条错误通知和一条描述性消息,并将-1/“无效”值作为新ID返回。

术语:error handling: exceptions vs error_code

WebSocket++使用c++11<system_error> 库定义的错误处理系统。也可选择性地回退到Boost库提供的类似系统。所有可能失败的面向用户的端点方法会在输出参数中使用错误码error_code并在返回前存储那里发生的错误。成功返回空/默认构造值。

Exception throwing varients异常

所有接收并使用error_code参数的面向用户的端点方法的版本都会引发异常。除了缺少最终的ec参数外,这些方法在函数和签名上是相同的。引发的异常类型为websocketpp::exception,派生自std::exception,所以可以被std::exceptions的catch块捕获。 websocketpp::exception::code() 可以从异常中提取出机器可读的error_code值。

为了清楚地说明错误处理,utility_client例子清楚地说明。您的应用程序可以选择使用其中一种。

If connection creation succeeds, the next sequential connection ID is generated and a connection_metadata object is inserted into the connection list under that ID. Initially the metadata object stores the connection ID, the connection_hdl, and the URI the connection was opened to.

若连接创建成功,会生成下一个序列连接ID并将connection_metadata 会插入该ID下的连接列表。最初,元数据对象存储连接ID,connection_hdl和打开连接的URI。

int new_id = m_next_id++;
metadata_ptr metadata(new connection_metadata(new_id, con->get_handle(), uri));
m_connection_list[new_id] = metadata;

3.2° 配置连接请求

接下来,配置连接请求。对于这一步,我们将要做的唯一配置就是设置一些默认处理程序。稍后,我们将返回并演示此处可能发生的一些更详细的配置、(setting user agents, origin, proxies, custom headers, subprotocols, etc)。

术语: Registering handlers

WebSocket++提供了许多执行点,您可以在其中注册以运行处理程序。这些点中哪些点可用于端点将取决于其配置。例如TLS处理程序将不存在于非TLS端点上。处理程序的完整列表可在中找到http://www.zaphoyd.com/websocketpp/manual/reference/handler-list。

可以在端点级和连接级注册处理函数。端点处理函数在创建时可以复制到新连接。更改端点处理函之影响未来的连接。连接处理函数仅绑定到特性连接。

处理函数数绑定的方法名对于端点和连接是相同的。格式:set_*_handler(...),*是处理函数的命名。例如, set_open_handler(...) 在设置新连接打开时调用处理函数,set_fail_handler(...) 设置在连接建立失败时调用的处理函数。

所有处理程序都有一个参数,这是一种可调用类型,可以转换为具有正确参数计数和参数类型的 std::function。您可以传递带有匹配参数列表的自由functions, functors, and Lambdas作为处理程序。此外,您可以使用std::bind(或boost::bind)来注册具有不匹配参数列表的函数。这对于传递处理程序名或需要携带this指针的成员函数中不存在的其他参数非常有用。

每个处理程序的函数名可在在上面的列表中手动查找。通常,所有的处理函数都包含在 connection_hdl 作为第一个参数,该标识与哪个连接关联。某些处理函数例如消息处理函数包含其他参数。大部分的处理函数返回void但是有些不 (validate, ping, tls_init) 。返回值的具体含义在以上的处理函数列表中。

utility_client注册一个open和fail处理函数。我们将使用它们跟踪每个连接是成功打开或失败。若成功打开,我们从打开的握手中收集一些信息并将其与连接元数据一起存储。

在此例中,我们将设置特定的连接处理函数,他们直接绑定和连接相关的元数据对象。这让我们能够避免在每个处理程序出中执行查找,以找我们计划更新的元数据对象,这会更有效率。

让我们详细查看发送到绑定的参数:

con->set_open_handler(websocketpp::lib::bind(
    &connection_metadata::on_open,
    metadata,
    &m_endpoint,
    websocketpp::lib::placeholders::_1
));

&connection_metadata::on_openconnection_metadata 类的成员函数on_open 的地址。

metadata_ptr :指向与此类相关联的connection_metadata 对象的指针

​ 用作调用 on_open成员函数的对象。

&m_endpoint :正在使用的端点地址,按原样传递给 on_open方法。

websocketpp::lib::placeholders::_1:占位符,表明绑定函数以后应该使用一个额外的参数来填充, WebSocket++在调用处理程序时将使用connection_hdl填充此占位。

3.3° 连接

最后,在配置的连接请求上调用 endpoint::connect() 并返回新的连接ID。

4° 处理成员函数

已注册的open处理函数( connection_metadata::on_open)设置状态元数据字段为“Open"并获取来自远程端点的HTTP响应的"Server"头的值并将其存储到元数据对象中。服务端经常在此头中设置标识字符串。

已经注册的fail处理函数( connection_metadata::on_fail)设置状态元数据字段为“Failed",服务端字段类似 on_open,并且获取回描述为何连接失败的错误码。与该错误代码相关联的人类可读消息被保存在元数据对象中。

5° 两条 新命令

connect [uri]:传递URI给 websocket_endpoint连接方法并上报错误或新连接的连接ID。

show [connection id]:返回和打印与连接相关的元数据信息

help帮助文本相应更新。

} else if (input.substr(0,7) == "connect") {
    int id = endpoint.connect(input.substr(8));
    if (id != -1) {
        std::cout << "> Created connection with id " << id << std::endl;
    }
} else if (input.substr(0,4) == "show") {
    int id = atoi(input.substr(5).c_str());

    connection_metadata::ptr metadata = endpoint.get_metadata(id);
    if (metadata) {
        std::cout << *metadata << std::endl;
    } else {
        std::cout << "> Unknown connection id " << id << std::endl;
    }
}
②构建

同步骤3

clang++ step4.cpp -lboost_system -lboost_random -lpthread
③代码
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>

#include <websocketpp/common/thread.hpp>
#include <websocketpp/common/memory.hpp>

#include <cstdlib>
#include <iostream>
#include <map>
#include <string>
#include <sstream>

typedef websocketpp::client<websocketpp::config::asio_client> client;

class connection_metadata {
public:
    typedef websocketpp::lib::shared_ptr<connection_metadata> ptr;

    connection_metadata(int id, websocketpp::connection_hdl hdl, std::string uri)
      : m_id(id)
      , m_hdl(hdl)
      , m_status("Connecting")
      , m_uri(uri)
      , m_server("N/A")
    {}

    void on_open(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Open";

        client::connection_ptr con = c->get_con_from_hdl(hdl);
        m_server = con->get_response_header("Server");
    }

    void on_fail(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Failed";

        client::connection_ptr con = c->get_con_from_hdl(hdl);
        m_server = con->get_response_header("Server");
        m_error_reason = con->get_ec().message();
    }

    friend std::ostream & operator<< (std::ostream & out, connection_metadata const & data);
private:
    int m_id;
    websocketpp::connection_hdl m_hdl;
    std::string m_status;
    std::string m_uri;
    std::string m_server;
    std::string m_error_reason;
};

std::ostream & operator<< (std::ostream & out, connection_metadata const & data) {
    out << "> URI: " << data.m_uri << "\n"
        << "> Status: " << data.m_status << "\n"
        << "> Remote Server: " << (data.m_server.empty() ? "None Specified" : data.m_server) << "\n"
        << "> Error/close reason: " << (data.m_error_reason.empty() ? "N/A" : data.m_error_reason);

    return out;
}

class websocket_endpoint {
public:
    websocket_endpoint () : m_next_id(0) {
        m_endpoint.clear_access_channels(websocketpp::log::alevel::all);
        m_endpoint.clear_error_channels(websocketpp::log::elevel::all);

        m_endpoint.init_asio();
        m_endpoint.start_perpetual();

        m_thread = websocketpp::lib::make_shared<websocketpp::lib::thread>(&client::run, &m_endpoint);
    }

    int connect(std::string const & uri) {
        websocketpp::lib::error_code ec;

        client::connection_ptr con = m_endpoint.get_connection(uri, ec);

        if (ec) {
            std::cout << "> Connect initialization error: " << ec.message() << std::endl;
            return -1;
        }

        int new_id = m_next_id++;
        connection_metadata::ptr metadata_ptr = websocketpp::lib::make_shared<connection_metadata>(new_id, con->get_handle(), uri);
        m_connection_list[new_id] = metadata_ptr;

        con->set_open_handler(websocketpp::lib::bind(
            &connection_metadata::on_open,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));
        con->set_fail_handler(websocketpp::lib::bind(
            &connection_metadata::on_fail,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));

        m_endpoint.connect(con);

        return new_id;
    }

    connection_metadata::ptr get_metadata(int id) const {
        con_list::const_iterator metadata_it = m_connection_list.find(id);
        if (metadata_it == m_connection_list.end()) {
            return connection_metadata::ptr();
        } else {
            return metadata_it->second;
        }
    }
private:
    typedef std::map<int,connection_metadata::ptr> con_list;

    client m_endpoint;
    websocketpp::lib::shared_ptr<websocketpp::lib::thread> m_thread;

    con_list m_connection_list;
    int m_next_id;
};

int main() {
    bool done = false;
    std::string input;
    websocket_endpoint endpoint;

    while (!done) {
        std::cout << "Enter Command: ";
        std::getline(std::cin, input);

        if (input == "quit") {
            done = true;
        } else if (input == "help") {
            std::cout
                << "\nCommand List:\n"
                << "connect <ws uri>\n"
                << "show <connection id>\n"
                << "help: Display this help text\n"
                << "quit: Exit the program\n"
                << std::endl;
        } else if (input.substr(0,7) == "connect") {
            int id = endpoint.connect(input.substr(8));
            if (id != -1) {
                std::cout << "> Created connection with id " << id << std::endl;
            }
        } else if (input.substr(0,4) == "show") {
            int id = atoi(input.substr(5).c_str());

            connection_metadata::ptr metadata = endpoint.get_metadata(id);
            if (metadata) {
                std::cout << *metadata << std::endl;
            } else {
                std::cout << "> Unknown connection id " << id << std::endl;
            }
        } else {
            std::cout << "> Unrecognized Command" << std::endl;
        }
    }

    return 0;
}

较步骤3增加了连接处理、元数据展示功能,并增加了两条命令(连接uri,显式元数据)

实现如下

增加了connection_metadata类:连接相关联的元数据

类型别名-public

​ typedef websocketpp::lib::shared_ptr<connection_metadata> ptr;

成员变量

​ priavte:连接元数据

​ int m_id:连接id

​ websocketpp::connection_hdl m_hdl:连接句柄

​ std::string m_status:连接状态

​ std::string m_uri:连接uri

​ std::string m_server:连接服务端

​ std::string m_error_reason:连接失败原因

成员函数

​ public:

​ 构造函数connection_metadata:初始化成员变量

​ on_open处理函数:获取服务端连接信息,设置元数据信息:m_status、m_server

​ on_fail处理函数:获取服务端连接信息,设置元数据信息:m_server、m_server、m_error_reason

​ friend:public

​ 重载<<操作符:将元数据信息存储到返回值/变量(std::ostream&类型)中,输出使用

websocket_endpoint类中增加连接处理

步骤3中

成员变量

​ priavte:

​ client m_endpoint:客户端对象

​ shared_ptr<websocketpp::lib::thread> m_thread:单独处理的后台线程(使用了共享指针指向线程)

成员函数

​ public:

​ 构造函数websocket_endpoint:初始化操作-

​ 1° 配置日志

​ 2° 初始化异步传输层和设置永久模式

   3° 它创建了一个线程来运行`client::run`方法,该线程由`m_thread`智能指针管理。

步骤4中

类型别名-private

​ typedef std::map<int,connection_metadata::ptr> con_list;

成员变量

​ priavte:

​ client m_endpoint:客户端对象

​ shared_ptr<websocketpp::lib::thread> m_thread:单独处理的后台线程(使用了共享指针指向线程)

​ con_list m_connection_list:存储连接和元数据的关联关系

int m_next_id:下一个连接的id

成员函数

​ public:

​ 构造函数websocket_endpoint:增加m_next_id初始化

​ 连接函数connect(uri):返回新连接的id

​ 1° 创建连接请求:client::connection_ptr con = m_endpoint.get_connection(uri, ec);

​ 创建连接相关的元数据并加入映射关系

​ 2° 配置连接请求:设置句柄set_open_handler,set_fail_handler

​ 3° 提交连接请求:m_endpoint.connect(con);

​ 获取元数据get_metadata:查找m_connection_list对应id的元数据指针并返回

④运行

使用3.2.1的步骤1的服务端

客户端

Enter Command: connect not a websocket uri
> Connect initialization error: invalid uri
Enter Command: show 0
> Unknown connection id 0
Enter Command: connect ws://echo.websocket.org
> Created connection with id 0
Enter Command: show 0
> URI: ws://echo.websocket.org
> Status: Failed
> Remote Server: AmazonS3
> Error/close reason: Invalid HTTP status.
Enter Command: connect ws://wikipedia.org
> Created connection with id 1
Enter Command: show 1
> URI: ws://wikipedia.org
> Status: Connecting
> Remote Server: N/A
> Error/close reason: N/A
Enter Command: connect ws://localhost:9002
> Created connection with id 2
Enter Command: show 2
> URI: ws://localhost:9002
> Status: Open
> Remote Server: WebSocket++/0.8.2
> Error/close reason: N/A

ctrl+c退出

服务端

[2023-12-12 17:28:37] [connect] WebSocket Connection [::ffff:127.0.0.1]:43438 v13 "WebSocket++/0.8.2" / 101
[2023-12-12 17:29:05] [error] handle_read_frame error: websocketpp.transport:7 (End of File)
[2023-12-12 17:29:05] [disconnect] Disconnect close local:[1006,End of File] remote:[1006]
步骤5:关闭连接
①目标

关闭连接

增加一个关闭WebSocket命令并调整了quit命令(退出前能干净地关闭所有未完成的连接)

1° 从WebSocket++中获取连接关闭信息

术语: WebSocket close codes & reasons

WebSocket 关闭握手包括可选的机器可读关闭码和人类可读原因字符串的互换。每个端点发送独立的关闭详细信息。关闭码是短整型。原因是最多125字节的UTF8文本字符串。更多关于有效关闭码范围和含义的细节参照https://tools.ietf.org/html/rfc6455#section-7.4。

websocketpp::close::status命名空间包含所有IANA定义的关闭码的命名常量。还包括自由函数,用于确定值是保留的还是无效的,以及用于将代码转换为人类可读的文本表示。

在关闭处理函数调用期间,WebSocket++连接提供了以下访问关闭握手信息的方法:

  • connection::get_remote_close_code(): 获取远程端点上报的关闭码
  • connection::get_remote_close_reason(): 获取远程端点上报的关闭原因
  • connection::get_local_close_code(): 获取此端点发送的关闭码
  • connection::get_local_close_reason(): 获取此端点发送的关闭原因
  • connection::get_ec(): 获取更详细具体的WebSocket++ error_code ,表明最终导致了连接关闭的库错误

注释:有些特殊的关闭代码会报告一个实际上没有在线路上发送的代码。例如,1005//"no close code"表示端点完全省略了关闭代码,1006/"abnormal close"表示存在导致连接在未执行关闭握手的情况下关闭的问题。

2° 增加关闭处理函数

增加 connection_metadata::on_close方法。返回关闭码和关闭原因,并存储到本地错误原因字段中(m_status和m_error_reason)

void on_close(client * c, websocketpp::connection_hdl hdl) {
    m_status = "Closed";
    client::connection_ptr con = c->get_con_from_hdl(hdl);
    std::stringstream s;
    s << "close code: " << con->get_remote_close_code() << " (" 
      << websocketpp::close::status::get_string(con->get_remote_close_code()) 
      << "), close reason: " << con->get_remote_close_reason();
    m_error_reason = s.str();
}

类似on_openon_fail, 当新连接建立时websocket_endpoint::connect 注册此关闭函数

3° 增加websocket_endpoint的关闭方法

开始部分是在连接和元数据映射结构中根据给定id查找对应元数据。

接着用特定的WebSocket关闭码发送关闭请求给连接句柄,这通过调用endpoint::close完成,它是一个线程安全的方法(用来将关闭信号异步调度给定句柄的连接)。操作完成后触发连接关闭处理函数

void close(int id, websocketpp::close::status::value code) {
    websocketpp::lib::error_code ec;
    
    con_list::iterator metadata_it = m_connection_list.find(id);
    if (metadata_it == m_connection_list.end()) {
        std::cout << "> No connection found with id " << id << std::endl;
        return;
    }
    
    m_endpoint.close(metadata_it->second->get_hdl(), code, "", ec);
    if (ec) {
        std::cout << "> Error initiating close: " << ec.message() << std::endl;
    }
}

4° 命令循环中增加关闭选项和帮助信息

关闭选项:close <connection id> [<close code:default=1000>] [<close reason>]

关闭选项包括连接ID,可选的关闭码和关闭原因。若没有指定关闭码使用默认的1000/Normal。若没有指定原因,什么都不会发送。 endpoint::close方法会在你尝试和发送无效码和无效UTF8格式原因时做些错误检查并终止关闭请求。原因字符串超过125字符会被截断。

else if (input.substr(0,5) == "close") {
    std::stringstream ss(input);
    
    std::string cmd;
    int id;
    int close_code = websocketpp::close::status::normal;
    std::string reason;
    
    ss >> cmd >> id >> close_code;
    std::getline(ss,reason);
    
    endpoint.close(id, close_code, reason);
}

5° 在websocket_endpoint析构函数总关闭所有未完成的连接

到目前为止,退出该程序使未完成的连接和WebSocket++网络线程陷入困境。现在我们有了一种关闭连接的方法,我们可以正确地清理它。

websocket_endpoint 的析构函数先停止永久模式(在最后一个连接被关闭后运行线程会退出),然后迭代所有打开的连接的列表并对每个请求干净关闭。最后,运行线程被分离--join,这会让程序阻塞直到连接完全关闭。

~websocket_endpoint() {
    m_endpoint.stop_perpetual();
    
    for (con_list::const_iterator it = m_connection_list.begin(); it != m_connection_list.end(); ++it) {
        if (it->second->get_status() != "Open") {
            // Only close open connections
            continue;
        }
        
        std::cout << "> Closing connection " << it->second->get_id() << std::endl;
        
        websocketpp::lib::error_code ec;
        m_endpoint.close(it->second->get_hdl(), websocketpp::close::status::going_away, "", ec);
        if (ec) {
            std::cout << "> Error closing connection " << it->second->get_id() << ": "  
                      << ec.message() << std::endl;
        }
    }
    
    m_thread->join();
}
②构建

同步骤4

clang++ step4.cpp -lboost_system -lboost_random -lpthread
③代码
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>

#include <websocketpp/common/thread.hpp>
#include <websocketpp/common/memory.hpp>

#include <cstdlib>
#include <iostream>
#include <map>
#include <string>
#include <sstream>

typedef websocketpp::client<websocketpp::config::asio_client> client;

class connection_metadata {
public:
    typedef websocketpp::lib::shared_ptr<connection_metadata> ptr;

    connection_metadata(int id, websocketpp::connection_hdl hdl, std::string uri)
      : m_id(id)
      , m_hdl(hdl)
      , m_status("Connecting")
      , m_uri(uri)
      , m_server("N/A")
    {}

    void on_open(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Open";

        client::connection_ptr con = c->get_con_from_hdl(hdl);
        m_server = con->get_response_header("Server");
    }

    void on_fail(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Failed";

        client::connection_ptr con = c->get_con_from_hdl(hdl);
        m_server = con->get_response_header("Server");
        m_error_reason = con->get_ec().message();
    }
    
    void on_close(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Closed";
        client::connection_ptr con = c->get_con_from_hdl(hdl);
        std::stringstream s;
        s << "close code: " << con->get_remote_close_code() << " (" 
          << websocketpp::close::status::get_string(con->get_remote_close_code()) 
          << "), close reason: " << con->get_remote_close_reason();
        m_error_reason = s.str();
    }

    websocketpp::connection_hdl get_hdl() const {
        return m_hdl;
    }
    
    int get_id() const {
        return m_id;
    }
    
    std::string get_status() const {
        return m_status;
    }

    friend std::ostream & operator<< (std::ostream & out, connection_metadata const & data);
private:
    int m_id;
    websocketpp::connection_hdl m_hdl;
    std::string m_status;
    std::string m_uri;
    std::string m_server;
    std::string m_error_reason;
};

std::ostream & operator<< (std::ostream & out, connection_metadata const & data) {
    out << "> URI: " << data.m_uri << "\n"
        << "> Status: " << data.m_status << "\n"
        << "> Remote Server: " << (data.m_server.empty() ? "None Specified" : data.m_server) << "\n"
        << "> Error/close reason: " << (data.m_error_reason.empty() ? "N/A" : data.m_error_reason);

    return out;
}

class websocket_endpoint {
public:
    websocket_endpoint () : m_next_id(0) {
        m_endpoint.clear_access_channels(websocketpp::log::alevel::all);
        m_endpoint.clear_error_channels(websocketpp::log::elevel::all);

        m_endpoint.init_asio();
        m_endpoint.start_perpetual();

        m_thread = websocketpp::lib::make_shared<websocketpp::lib::thread>(&client::run, &m_endpoint);
    }

    ~websocket_endpoint() {
        m_endpoint.stop_perpetual();
        
        for (con_list::const_iterator it = m_connection_list.begin(); it != m_connection_list.end(); ++it) {
            if (it->second->get_status() != "Open") {
                // Only close open connections
                continue;
            }
            
            std::cout << "> Closing connection " << it->second->get_id() << std::endl;
            
            websocketpp::lib::error_code ec;
            m_endpoint.close(it->second->get_hdl(), websocketpp::close::status::going_away, "", ec);
            if (ec) {
                std::cout << "> Error closing connection " << it->second->get_id() << ": "  
                          << ec.message() << std::endl;
            }
        }
        
        m_thread->join();
    }

    int connect(std::string const & uri) {
        websocketpp::lib::error_code ec;

        client::connection_ptr con = m_endpoint.get_connection(uri, ec);

        if (ec) {
            std::cout << "> Connect initialization error: " << ec.message() << std::endl;
            return -1;
        }

        int new_id = m_next_id++;
        connection_metadata::ptr metadata_ptr = websocketpp::lib::make_shared<connection_metadata>(new_id, con->get_handle(), uri);
        m_connection_list[new_id] = metadata_ptr;

        con->set_open_handler(websocketpp::lib::bind(
            &connection_metadata::on_open,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));
        con->set_fail_handler(websocketpp::lib::bind(
            &connection_metadata::on_fail,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));
        con->set_close_handler(websocketpp::lib::bind(
            &connection_metadata::on_close,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));

        m_endpoint.connect(con);

        return new_id;
    }

    void close(int id, websocketpp::close::status::value code, std::string reason) {
        websocketpp::lib::error_code ec;
        
        con_list::iterator metadata_it = m_connection_list.find(id);
        if (metadata_it == m_connection_list.end()) {
            std::cout << "> No connection found with id " << id << std::endl;
            return;
        }
        
        m_endpoint.close(metadata_it->second->get_hdl(), code, reason, ec);
        if (ec) {
            std::cout << "> Error initiating close: " << ec.message() << std::endl;
        }
    }

    connection_metadata::ptr get_metadata(int id) const {
        con_list::const_iterator metadata_it = m_connection_list.find(id);
        if (metadata_it == m_connection_list.end()) {
            return connection_metadata::ptr();
        } else {
            return metadata_it->second;
        }
    }
private:
    typedef std::map<int,connection_metadata::ptr> con_list;

    client m_endpoint;
    websocketpp::lib::shared_ptr<websocketpp::lib::thread> m_thread;

    con_list m_connection_list;
    int m_next_id;
};

int main() {
    bool done = false;
    std::string input;
    websocket_endpoint endpoint;

    while (!done) {
        std::cout << "Enter Command: ";
        std::getline(std::cin, input);

        if (input == "quit") {
            done = true;
        } else if (input == "help") {
            std::cout
                << "\nCommand List:\n"
                << "connect <ws uri>\n"
                << "close <connection id> [<close code:default=1000>] [<close reason>]\n"
                << "show <connection id>\n"
                << "help: Display this help text\n"
                << "quit: Exit the program\n"
                << std::endl;
        } else if (input.substr(0,7) == "connect") {
            int id = endpoint.connect(input.substr(8));
            if (id != -1) {
                std::cout << "> Created connection with id " << id << std::endl;
            }
        } else if (input.substr(0,5) == "close") {
            std::stringstream ss(input);
            
            std::string cmd;
            int id;
            int close_code = websocketpp::close::status::normal;
            std::string reason;
            
            ss >> cmd >> id >> close_code;
            std::getline(ss,reason);
            
            endpoint.close(id, close_code, reason);
        }  else if (input.substr(0,4) == "show") {
            int id = atoi(input.substr(5).c_str());

            connection_metadata::ptr metadata = endpoint.get_metadata(id);
            if (metadata) {
                std::cout << *metadata << std::endl;
            } else {
                std::cout << "> Unknown connection id " << id << std::endl;
            }
        } else {
            std::cout << "> Unrecognized Command" << std::endl;
        }
    }

    return 0;
}

相较于步骤4,增加了关闭功能

connection_metadata类,主要增加了关闭处理函数

成员函数

​ public:

​ on_close处理函数:connection_hdl->connection_ptr,设置元数据信息:m_status,m_error_reason

​ get_hdl:返回成员变量m_hdl

​ get_id:返回成员变量m_id

​ get_status:返回成员变量m_status

websocket_endpoint类

成员函数

​ public:

​ 析构函数:停止永久模式,迭代每一个打开的连接关闭,线程阻塞至异步关闭结束

​ connect:增加set_close_handler关闭处理

​ close:根据id找到连接句柄关闭连接

主函数:增加关闭命令,quit命令干净退出

④运行

使用3.2.1的步骤1的服务端

客户端

Enter Command: connect ws://localhost:9002
> Created connection with id 0
Enter Command: close 0 1001 example message
Enter Command: show 0
> URI: ws://localhost:9002
> Status: Closed
> Remote Server: WebSocket++/0.4.0
> Error/close reason: close code: 1001 (Going away), close reason:  example message
Enter Command: connect ws://localhost:9002
> Created connection with id 1
Enter Command: close 1 1006
> Error initiating close: Invalid close code used
Enter Command: quit
> Closing connection 1

服务端

[2023-12-12 17:23:47] [connect] WebSocket Connection [::ffff:127.0.0.1]:43428 v13 "WebSocket++/0.8.2" / 101
[2023-12-12 17:23:57] [control] Control frame received with opcode 8
[2023-12-12 17:23:57] [frame_header] Dispatching write containing 1 message(s) containing 2 header bytes and 18 payload bytes
[2023-12-12 17:23:57] [frame_header] Header Bytes: 
[0] (2) 88 12 

[2023-12-12 17:23:57] [frame_payload] Payload Bytes: 
[0] (18) [8] 03 E9 20 65 78 61 6D 70 6C 65 20 6D 65 73 73 61 67 65 

[2023-12-12 17:23:57] [disconnect] Disconnect close local:[1001, example message] remote:[1001, example message]
[2023-12-12 17:24:08] [connect] WebSocket Connection [::ffff:127.0.0.1]:43430 v13 "WebSocket++/0.8.2" / 101
[2023-12-12 17:24:22] [control] Control frame received with opcode 8
[2023-12-12 17:24:22] [frame_header] Dispatching write containing 1 message(s) containing 2 header bytes and 2 payload bytes
[2023-12-12 17:24:22] [frame_header] Header Bytes: 
[0] (2) 88 02 

[2023-12-12 17:24:22] [frame_payload] Payload Bytes: 
[0] (2) [8] 03 E9 

[2023-12-12 17:24:22] [disconnect] Disconnect close local:[1001] remote:[1001]
步骤6:发送和接受消息
①目标

发送和接受消息

此步骤添加一个在给定连接上发送消息的命令,并更新show命令以打印该连接的所有发送和接收消息的记录。

术语: WebSocket message types (opcodes操作码)

WebSocket消息具有由其操作码指示的类型。该协议目前为数据消息指定了两种不同的操作码,文本和二进制。文本消息表示UTF8文本,并将被验证为UTF8文本。二进制消息表示原始二进制字节,并且直接通过而不进行验证。

WebSocket++提供了值websocketpp::frame::opcode::textwebsocketpp::frame::opcode::binary,可用于指导传出消息的发送方式和检查传入消息的格式

1° 发送消息

消息是使用endpoint::send发送的。这是一种线程安全的方法,可以从任何地方调用它来对消息进行排队,以便在指定的连接上发送。有三种发送重载可用于不同的场景。

每个方法都有一个connection_hdl参数来指示在哪个连接上发送消息,还有一个frame::opcode::value参数来指示将消息标记为哪个操作码。所有重载都可以使用一个无异常变量来填充状态/错误代码,而不是抛出。

第一种重载connection_hdl hdl, std::string const & payload, frame::opcode::value op使用std::string. 字符串内容被拷贝至内部缓冲区,在调用send后可被安全修改。

第二种重载connection_hdl hdl, void const * payload, size_t len, frame::opcode::value op使用void *缓冲区和长度。缓冲区内容被复制,并且可以在调用send后安全地进行修改。

第三种重载connection_hdl hdl, message_ptr msg使用WebSocket++的message_ptr。此种重载允许在调用send前就地构建消息。它还可以允许在不复制的情况下多次发送单个消息缓冲区,包括发送到多个连接。这种情况是否真的发生取决于其他因素,例如是否启用了压缩。消息缓冲区的内容在发送后可能无法安全修改。

术语: Outgoing WebSocboundariest message queueing & flow control 传出WebSocket消息排队和流控制

在许多配置中,类似在使用基于传输的Asio时,WebSocket++是一个异步的系统。像这样的情况下endpoint::send 方法可能在任何字节实际写入传出套接字之前返回。在快速连续多次调用send的情况下,可以在同一操作甚至同一TCP数据包中合并和发送消息。当这种情况发生时,消息边界将被保留(每个send调用将生产一个独立的消息)。

对于从处理程序内部调用send的应用程序,这意味着在该处理程序返回之前,不会向套接字写入任何消息。如果计划在此工作区发送很多消息,或者需要在继续之前需要在网上写一条消息,应该考虑使用多线程或者内置的定时器/中断处理程序功能。

如果传出套接字连接很慢,消息可能会在队列中增加。可使用connection::get_buffered_amount查询写入消息队列的当前大小,以决定是否要更改发送行为。

websocket_endpoint添加send方法

类似close方法,开始部分是在连接列表中查找给定连接ID的项。接下来,发送一个send请求(包含指定的WebSocket消息和文本操作码)到连接句柄。最后,使用连接元数据对象记录发送的消息,以便之后的show connection命令可以打印发送的消息列表。

void send(int id, std::string message) {
    websocketpp::lib::error_code ec;
    
    con_list::iterator metadata_it = m_connection_list.find(id);
    if (metadata_it == m_connection_list.end()) {
        std::cout << "> No connection found with id " << id << std::endl;
        return;
    }
    
    m_endpoint.send(metadata_it->second->get_hdl(), message, websocketpp::frame::opcode::text, ec);
    if (ec) {
        std::cout << "> Error sending message: " << ec.message() << std::endl;
        return;
    }
    
    metadata_it->second->record_sent_message(message);
}

3° 命令循环添加send选项和帮助信息

send命令参数:connection ID,文本消息

帮助添加提示

else if (input.substr(0,4) == "send") {
    std::stringstream ss(input);
        
        std::string cmd;
        int id;
        std::string message = "";
        
        ss >> cmd >> id;
        std::getline(ss,message);
        
        endpoint.send(id, message);
}

connection_metadata 添加代码存储发送消息

为了存储在该连接上发送的消息,一些代码被添加到connection_metadata中。这包括一个新的数据成员std::vector<std::string> m_messages,用于跟踪发送和接收的所有消息,以及在该列表中添加已发送消息的方法:

void record_sent_message(std::string message) {
    m_messages.push_back(">> " + message);
}

最后更新连接元数据输出操作符以打印处理过的消息列表:

out << "> Messages Processed: (" << data.m_messages.size() << ") \n";

std::vector<std::string>::const_iterator it;
for (it = data.m_messages.begin(); it != data.m_messages.end(); ++it) {
    out << *it << "\n";
}

5° 接收消息

通过注册消息处理函数来接收消息。每个接收到的消息都会调用此处理程序一次,其签名为void on_message(websocketpp::connection_hdl hdl, endpoint::message_ptr msg)。与其他处理程序的类似参数一样, connection_hdl,是一个接收消息的连接句柄。message_ptr 是指向对象的指针,该对象可以查询消息payload、操作码和其他元数据。请,message_ptr类型及底层消息类型取决于端点的配置方式,并且对于不同的配置可能会有所不同。

connection_metadata添加消息处理函数

我们正在实现的消息接收行为是收集发送和接收的所有消息,并在运行show connection命令时按顺序打印它们。已将发送的消息添加到该列表中。现在我们添加一个消息处理程序,它也将接收到的消息插入到列表中。文本类型的消息按原样插入。二进制消息首先转换为可打印的十六进制格式。

void on_message(websocketpp::connection_hdl hdl, client::message_ptr msg) {
    if (msg->get_opcode() == websocketpp::frame::opcode::text) {
        m_messages.push_back(msg->get_payload());
    } else {
        m_messages.push_back(websocketpp::utility::to_hex(msg->get_payload()));
    }
}

为了在收到新消息时调用此处理程序,我们还将其注册到连接中。请注意,与大多数其他处理程序不同,消息处理程序有两个参数,因此需要两个占位符。

// bind绑定类的成员函数,第1个参数是要绑定的成员函数指针,第2个参数是类的实例指针,接着才是函数的参数
con->set_message_handler(websocketpp::lib::bind(
    &connection_metadata::on_message,
    metadata_ptr,
    websocketpp::lib::placeholders::_1,
    websocketpp::lib::placeholders::_2
));
②构建

同步骤5

clang++ step4.cpp -lboost_system -lboost_random -lpthread
③代码
#include <websocketpp/config/asio_no_tls_client.hpp>
#include <websocketpp/client.hpp>

#include <websocketpp/common/thread.hpp>
#include <websocketpp/common/memory.hpp>

#include <cstdlib>
#include <iostream>
#include <map>
#include <string>
#include <sstream>

typedef websocketpp::client<websocketpp::config::asio_client> client;

class connection_metadata {
public:
    typedef websocketpp::lib::shared_ptr<connection_metadata> ptr;

    connection_metadata(int id, websocketpp::connection_hdl hdl, std::string uri)
      : m_id(id)
      , m_hdl(hdl)
      , m_status("Connecting")
      , m_uri(uri)
      , m_server("N/A")
    {}

    void on_open(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Open";

        client::connection_ptr con = c->get_con_from_hdl(hdl);
        m_server = con->get_response_header("Server");
    }

    void on_fail(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Failed";

        client::connection_ptr con = c->get_con_from_hdl(hdl);
        m_server = con->get_response_header("Server");
        m_error_reason = con->get_ec().message();
    }
    
    void on_close(client * c, websocketpp::connection_hdl hdl) {
        m_status = "Closed";
        client::connection_ptr con = c->get_con_from_hdl(hdl);
        std::stringstream s;
        s << "close code: " << con->get_remote_close_code() << " (" 
          << websocketpp::close::status::get_string(con->get_remote_close_code()) 
          << "), close reason: " << con->get_remote_close_reason();
        m_error_reason = s.str();
    }

    void on_message(websocketpp::connection_hdl, client::message_ptr msg) {
        if (msg->get_opcode() == websocketpp::frame::opcode::text) {
            m_messages.push_back("<< " + msg->get_payload());
        } else {
            m_messages.push_back("<< " + websocketpp::utility::to_hex(msg->get_payload()));
        }
    }

    websocketpp::connection_hdl get_hdl() const {
        return m_hdl;
    }
    
    int get_id() const {
        return m_id;
    }
    
    std::string get_status() const {
        return m_status;
    }

    void record_sent_message(std::string message) {
        m_messages.push_back(">> " + message);
    }

    friend std::ostream & operator<< (std::ostream & out, connection_metadata const & data);
private:
    int m_id;
    websocketpp::connection_hdl m_hdl;
    std::string m_status;
    std::string m_uri;
    std::string m_server;
    std::string m_error_reason;
    std::vector<std::string> m_messages;
};

std::ostream & operator<< (std::ostream & out, connection_metadata const & data) {
    out << "> URI: " << data.m_uri << "\n"
        << "> Status: " << data.m_status << "\n"
        << "> Remote Server: " << (data.m_server.empty() ? "None Specified" : data.m_server) << "\n"
        << "> Error/close reason: " << (data.m_error_reason.empty() ? "N/A" : data.m_error_reason) << "\n";
    out << "> Messages Processed: (" << data.m_messages.size() << ") \n";

    std::vector<std::string>::const_iterator it;
    for (it = data.m_messages.begin(); it != data.m_messages.end(); ++it) {
        out << *it << "\n";
    }

    return out;
}

class websocket_endpoint {
public:
    websocket_endpoint () : m_next_id(0) {
        m_endpoint.clear_access_channels(websocketpp::log::alevel::all);
        m_endpoint.clear_error_channels(websocketpp::log::elevel::all);

        m_endpoint.init_asio();
        m_endpoint.start_perpetual();

        m_thread = websocketpp::lib::make_shared<websocketpp::lib::thread>(&client::run, &m_endpoint);
    }

    ~websocket_endpoint() {
        m_endpoint.stop_perpetual();
        
        for (con_list::const_iterator it = m_connection_list.begin(); it != m_connection_list.end(); ++it) {
            if (it->second->get_status() != "Open") {
                // Only close open connections
                continue;
            }
            
            std::cout << "> Closing connection " << it->second->get_id() << std::endl;
            
            websocketpp::lib::error_code ec;
            m_endpoint.close(it->second->get_hdl(), websocketpp::close::status::going_away, "", ec);
            if (ec) {
                std::cout << "> Error closing connection " << it->second->get_id() << ": "  
                          << ec.message() << std::endl;
            }
        }
        
        m_thread->join();
    }

    int connect(std::string const & uri) {
        websocketpp::lib::error_code ec;

        client::connection_ptr con = m_endpoint.get_connection(uri, ec);

        if (ec) {
            std::cout << "> Connect initialization error: " << ec.message() << std::endl;
            return -1;
        }

        int new_id = m_next_id++;
        connection_metadata::ptr metadata_ptr = websocketpp::lib::make_shared<connection_metadata>(new_id, con->get_handle(), uri);
        m_connection_list[new_id] = metadata_ptr;

        con->set_open_handler(websocketpp::lib::bind(
            &connection_metadata::on_open,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));
        con->set_fail_handler(websocketpp::lib::bind(
            &connection_metadata::on_fail,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));
        con->set_close_handler(websocketpp::lib::bind(
            &connection_metadata::on_close,
            metadata_ptr,
            &m_endpoint,
            websocketpp::lib::placeholders::_1
        ));
        con->set_message_handler(websocketpp::lib::bind(
            &connection_metadata::on_message,
            metadata_ptr,
            websocketpp::lib::placeholders::_1,
            websocketpp::lib::placeholders::_2
        ));

        m_endpoint.connect(con);

        return new_id;
    }

    void close(int id, websocketpp::close::status::value code, std::string reason) {
        websocketpp::lib::error_code ec;
        
        con_list::iterator metadata_it = m_connection_list.find(id);
        if (metadata_it == m_connection_list.end()) {
            std::cout << "> No connection found with id " << id << std::endl;
            return;
        }
        
        m_endpoint.close(metadata_it->second->get_hdl(), code, reason, ec);
        if (ec) {
            std::cout << "> Error initiating close: " << ec.message() << std::endl;
        }
    }

    void send(int id, std::string message) {
        websocketpp::lib::error_code ec;
        
        con_list::iterator metadata_it = m_connection_list.find(id);
        if (metadata_it == m_connection_list.end()) {
            std::cout << "> No connection found with id " << id << std::endl;
            return;
        }
        
        m_endpoint.send(metadata_it->second->get_hdl(), message, websocketpp::frame::opcode::text, ec);
        if (ec) {
            std::cout << "> Error sending message: " << ec.message() << std::endl;
            return;
        }
        
        metadata_it->second->record_sent_message(message);
    }

    connection_metadata::ptr get_metadata(int id) const {
        con_list::const_iterator metadata_it = m_connection_list.find(id);
        if (metadata_it == m_connection_list.end()) {
            return connection_metadata::ptr();
        } else {
            return metadata_it->second;
        }
    }
private:
    typedef std::map<int,connection_metadata::ptr> con_list;

    client m_endpoint;
    websocketpp::lib::shared_ptr<websocketpp::lib::thread> m_thread;

    con_list m_connection_list;
    int m_next_id;
};

int main() {
    bool done = false;
    std::string input;
    websocket_endpoint endpoint;

    while (!done) {
        std::cout << "Enter Command: ";
        std::getline(std::cin, input);

        if (input == "quit") {
            done = true;
        } else if (input == "help") {
            std::cout
                << "\nCommand List:\n"
                << "connect <ws uri>\n"
                << "send <connection id> <message>\n"
                << "close <connection id> [<close code:default=1000>] [<close reason>]\n"
                << "show <connection id>\n"
                << "help: Display this help text\n"
                << "quit: Exit the program\n"
                << std::endl;
        } else if (input.substr(0,7) == "connect") {
            int id = endpoint.connect(input.substr(8));
            if (id != -1) {
                std::cout << "> Created connection with id " << id << std::endl;
            }
        } else if (input.substr(0,4) == "send") {
            std::stringstream ss(input);
            
            std::string cmd;
            int id;
            std::string message;
            
            ss >> cmd >> id;
            std::getline(ss,message);
            
            endpoint.send(id, message);
        } else if (input.substr(0,5) == "close") {
            std::stringstream ss(input);
            
            std::string cmd;
            int id;
            int close_code = websocketpp::close::status::normal;
            std::string reason;
            
            ss >> cmd >> id >> close_code;
            std::getline(ss,reason);
            
            endpoint.close(id, close_code, reason);
        } else if (input.substr(0,4) == "show") {
            int id = atoi(input.substr(5).c_str());

            connection_metadata::ptr metadata = endpoint.get_metadata(id);
            if (metadata) {
                std::cout << *metadata << std::endl;
            } else {
                std::cout << "> Unknown connection id " << id << std::endl;
            }
        } else {
            std::cout << "> Unrecognized Command" << std::endl;
        }
    }

    return 0;
}

较步骤5增加了发送和接收消息的功能

connection_metadata:增加发送和接收消息的存储

成员变量

​ private:

​ std::vector<std::string> m_messages:元数据:存储发送和接收的所有消息

成员函数

​ public:

​ on_message处理函数:将消息添加进m_messages,文本-直接,二进制-先转化为16进制

​ record_sent_message:将发送消息添加进m_messages

友元函数

​ 重载<<:加上消息打印

websocket_endpoint:设置消息处理函数和发送消息

成员函数

​ public:

​ connect:增加设置消息处理函数

​ send:遍历连接列表根据id找到连接句柄,send发送,添加消息到元数据

主函数:增加发送命令,修改帮助命令

④运行

在这个示例运行中,我们连接到WebSocket++示例echo_server。此服务器将重复我们发送回它的任何消息。您也可以尝试在ws://echo.websocket.org上的echo服务器上进行测试,得到类似的结果。

客户端

Enter Command: connect ws://localhost:9002
> Created connection with id 0
Enter Command: send 0 example message
Enter Command: show 0
> URI: ws://localhost:9002
> Status: Open
> Remote Server: WebSocket++/0.8.2
> Error/close reason: N/A
> Messages Processed: (2)
>>  example message
<<  example message

Enter Command: quit
> Closing connection 0

服务端

[2023-12-12 17:14:55] [connect] WebSocket Connection [::ffff:127.0.0.1]:43420 v13 "WebSocket++/0.8.2" / 101
[2023-12-12 17:15:02] [frame_header] Dispatching write containing 1 message(s) containing 2 header bytes and 16 payload bytes
[2023-12-12 17:15:02] [frame_header] Header Bytes: 
[0] (2) 81 10 

[2023-12-12 17:15:02] [frame_payload] Payload Bytes: 
[0] (16) [1]  example message

[2023-12-12 17:15:12] [control] Control frame received with opcode 8
[2023-12-12 17:15:12] [frame_header] Dispatching write containing 1 message(s) containing 2 header bytes and 2 payload bytes
[2023-12-12 17:15:12] [frame_header] Header Bytes: 
[0] (2) 88 02 

[2023-12-12 17:15:12] [frame_payload] Payload Bytes: 
[0] (2) [8] 03 E9 

[2023-12-12 17:15:12] [disconnect] Disconnect close local:[1001] remote:[1001]
步骤7:待+

Using TLS / Secure WebSockets

  • Change the includes
  • link to the new library dependencies
  • Switch the config
  • add the tls_init_handler
  • configure the SSL context for desired security level
  • mixing secure and non-secure connections in one application.

3.1.2 中间功能

步骤8:待+

中间级功能

  • Subprotocol negotiation:子协议协商
  • Setting and reading custom headers:设置和读取自定义头
  • Ping and Pong
  • Proxies?
  • Setting user agent
  • Setting Origin
  • Timers and security
  • Close behavior
  • Send one message to all connections

核心websocket++控制流
握手,然后分成两条独立的控制链

  • 握手

​ --使用在调用endpoint::connect之前指定的信息来构造WebSocket握手请求。
​ --将WebSocket握手请求传递给传输策略。传输策略决定如何将这些字节获取到扮演服务器角色的端点。根据端点使用的传输策略,此方法会有所不同。
​ --从基础传输接收握手响应。对其进行分析并检查是否符合RFC6455。如果验证失败,则调用失败处理程序。否则将调用打开处理程序。

  • 在这一点上,控制分为两个独立的部分。一个从传入通道上的传输策略读取新字节,另一个从本地应用程序接受新消息,用于成帧和写入传出传输通道。
  • 读取链
    -- 读取并处理传输中的新字节
    -- 如果字节至少包含一个完整的消息,则通过调用适当的处理程序来调度每条消息。这要么是数据消息的消息处理程序,要么是每个相应控制消息的ping/pong/close处理程序。如果没有为特定消息注册处理程序,则会忽略该处理程序。
    -- 向传输层请求更多字节
  • 写入链
    -- 等待来自应用程序的消息
    -- 对消息输入执行错误检查,
    -- 根据RFC6455的帧消息
    -- 排队发送消息
    -- 将所有未处理的消息传递给传输策略以进行输出
    -- 当没有消息可发送时,返回等待
    --当没有消息可发送时,返回等待

3.2 服务端

目录:tutorials/utility_server/utility_server.md

本教程提供了关于构建基本WebSocket++服务器的逐步讨论。本教程的最终产品是示例部分中的utility_server示例应用程序。此服务器演示了以下功能:

  • 使用Asio进行网络传输
  • 一次接受多个WebSocket连接
  • 读取传入消息并根据路径执行一些基本操作(回显echo、广播broadcast、遥测telemetry、服务器命令)
  • 使用验证处理程序拒绝到无效路径的连接
  • 使用HTTP处理程序提供基本的HTTP响应
  • 优雅地退出服务器
  • 使用TLS加密连接

基于当前0.6.x版本

3.2.1 初始化设置和基础

步骤1:服务端类型
①目标

增加WebSocket++头文件并设置服务端端点类型

WebSocket++包括主要对象类型:端点和连接。端点创建和启动新连接并维护连接的默认设置,还管理任何共享的网络资源。

连接存储每个WebSocket对话的信息。

请注意: 一旦连接启动,端点和连接没有任何联系。所有默认设置都会由端点拷贝到新连接。改变端点的默认设置只会影响之后的连接。

连接并不维护和关联的观点之间的联系。端点也不维护未完成的连接列表。若应用程序需要迭代所有的连接,需要自己维护列表。

构建端点:WebSocket++端点是通过将端点角色与端点配置相结合来构建的,有两种不同类型的端点角色,用于WebSocket会话中的客户端和服务器角色。

服务端角色:websocketpp::server,头文件<websocketpp/server.hpp>

术语: Endpoint Config端点配置

WebSocket++端点有一组设置,这些设置可以在编译时通过“config”模板参数进行配置。config是一个结构体,它包含用于生成具有特定属性的端点的类型和静态常量。根据使用的config,端点将具有不同的可用方法,并且可能具有额外的第三方依赖关系。

端点角色采用一个名为“config”的模板参数,该参数用于在编译时配置端点的行为。在本例中,我们将使用名为“asio”的库提供的默认配置,该库由<websockettpp/config/asio_no_tls.hpp>提供。这是一个使用asio库提供网络传输的服务器配置,不支持基于tls的安全性。稍后,我们将讨论如何将基于TLS的安全性引入WebSocket++应用程序,更多关于其他配置的信息,以及如何构建自己的自定义配置。

将配置与端点角色组合以生成完全配置的端点。这种类型会经常使用,所以我建议在这里使用typedef。
typedef websocketpp::server<websocketpp::config::asio> server

utility_server构造函数

端点类型是将跟踪服务器的状态的utility_server对象的基础。在utility_server构造函数中会发生以下几件事:

1.1° 日志行为

首先,调整端点日志行为以包含所有错误日志通道和除了帧负载的所有访问日志通道,帧负载特别噪杂且仅对调试有用。

m_endpoint.set_error_channels(websocketpp::log::elevel::all);
m_endpoint.set_access_channels(websocketpp::log::alevel::all ^ websocketpp::log::alevel::frame_payload);

1.2° 初始化

接下来,初始化端点下的传输系统。此方法是特定于Asio传输而非WebSocket++核心。它将不必要,也不存在于使用非asio配置的端点中。

请注意: 此例使用内部的Asio io_service ,它由端点自己管理。这是一个简单的安排,适用于WebSocket++是唯一使用Asio的代码的程序。如果您有一个已经管理io_service对象的现有程序,或者想要构建一个新程序,其中WebSocket++处理程序与其他处理程序共享io_service,则可以将您希望WebSocket++将其处理程序注册到init_asio()方法的io_server传递给它,它将使用它,而不是生成和管理自己的处理程序。

m_endpoint.init_asio();

utility_server::run方法

增加run方法以设置正在监听的套接字,开始接收连接,启动Asio io_service事件循环。

// Listen on port 9002
m_endpoint.listen(9002);

// Queues a connection accept operation
m_endpoint.start_accept();

// Start the Asio io_service run loop
m_endpoint.run();

最后一行,m_endpoint.run();将会阻塞知道端点被指示停止监听新的连接。运行时,它将监听和处理新连接,并接受和处理现有连接的新数据和控制消息。WebSocket++以异步模式使用Asio,在异步模式下,多个连接可以在单个线程内高效地同时提供服务。

②构建

c++ -std=c++11 step1.cpp (Asio Standalone)

c++ -std=c++11 step1.cpp -lboost_system (Boost Asio)

编译提示错误: fatal error: asio/version.hpp: 没有那个文件或目录

解决:安装asio

在Ubuntu上安装Asio,可以通过以下步骤进行:
首先,你需要安装boost库,Asio依赖于boost。打开终端,输入以下命令进行安装:
sudo apt-get install libboost-all-dev
接着,你需要安装Asio库。输入以下命令进行安装:
sudo apt-get install libasio-dev
接下来,你需要在你的代码中包含Asio库。在你的源代码文件中,你需要包含Asio的头文件。例如:
#include <boost/asio.hpp>
最后,你需要编译和链接你的程序。例如,如果你的源代码文件名为main.cpp,你可以使用以下命令来编译和链接你的程序:
g++ main.cpp -o main -lboost-system -lasio
③代码

实现连接功能

#define ASIO_STANDALONE

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

#include <functional>

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

class utility_server {
public:
    utility_server() {
         // Set logging settings
        m_endpoint.set_error_channels(websocketpp::log::elevel::all);
        m_endpoint.set_access_channels(websocketpp::log::alevel::all ^ websocketpp::log::alevel::frame_payload);

        // Initialize Asio
        m_endpoint.init_asio();
    }

    void run() {
        // Listen on port 9002
        m_endpoint.listen(9002);

        // Queues a connection accept operation
        m_endpoint.start_accept();

        // Start the Asio io_service run loop
        m_endpoint.run();
    }
private:
    server m_endpoint;
};

int main() {
    utility_server s;
    s.run();
    return 0;
}

全局:服务端端点类型别名

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

utility_server服务端类

成员变量

​ private:

​ server m_endpoint:端点

成员函数

​ public:

​ 构造函数:日志设置,初始化asio

​ run:监听9002端口,等待连接,启动Asio io_service的run循环

主函数:启动服务端

utility_server s;
s.run();
④运行

使用3.1.1的步骤6的客户端测试连接

客户端

Enter Command: connect ws://localhost:9002
> Created connection with id 0
Enter Command: show 0
> URI: ws://localhost:9002
> Status: Open
> Remote Server: WebSocket++/0.8.2
> Error/close reason: N/A
> Messages Processed: (0)

Enter Command: quit
> Closing connection 0

服务端

[2023-12-12 17:18:53] [connect] WebSocket Connection [::ffff:127.0.0.1]:43424 v13 "WebSocket++/0.8.2" / 101
[2023-12-12 17:18:59] [control] Control frame received with opcode 8
[2023-12-12 17:18:59] [frame_header] Dispatching write containing 1 message(s) containing 2 header bytes and 2 payload bytes
[2023-12-12 17:18:59] [frame_header] Header Bytes: 
[0] (2) 88 02 

[2023-12-12 17:18:59] [frame_payload] Payload Bytes: 
[0] (2) [8] 03 E9 

[2023-12-12 17:18:59] [disconnect] Disconnect close local:[1001] remote:[1001]
步骤2:消息处理函数
①目标

设置回显有原始用户的回复的消息处理函数

术语: Registering handlers

术语: Registering handlers

WebSocket++提供了许多执行点,您可以在其中注册以运行处理程序。这些点中哪些点可用于端点将取决于其配置。例如TLS处理程序将不存在于非TLS端点上。处理程序的完整列表可在中找到http://www.zaphoyd.com/websocketpp/manual/reference/handler-list。

可以在端点级和连接级注册处理函数。端点处理函数在创建时可以复制到新连接。更改端点处理函之影响未来的连接。连接处理函数仅绑定到特性连接。

处理函数数绑定的方法名对于端点和连接是相同的。格式:set_*_handler(...),*是处理函数的命名。例如, set_open_handler(...) 在设置新连接打开时调用处理函数,set_fail_handler(...) 设置在连接建立失败时调用的处理函数。

所有处理程序都有一个参数,这是一种可调用类型,可以转换为具有正确参数计数和参数类型的 std::function。您可以传递带有匹配参数列表的自由functions, functors, and Lambdas作为处理程序。此外,您可以使用std::bind(或boost::bind)来注册具有不匹配参数列表的函数。这对于传递处理程序名或需要携带this指针的成员函数中不存在的其他参数非常有用。

每个处理程序的函数名可在在上面的列表中手动查找。通常,所有的处理函数都包含在 connection_hdl 作为第一个参数,该标识与哪个连接关联。某些处理函数例如消息处理函数包含其他参数。大部分的处理函数返回void但是有些不 (validate, ping, tls_init) 。返回值的具体含义在以上的处理函数列表中。

②构建
c++ -std=c++11 step1.cpp
③代码

回显服务器

#define ASIO_STANDALONE

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

#include <functional>

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

class utility_server {
public:
    utility_server() {
         // Set logging settings
        m_endpoint.set_error_channels(websocketpp::log::elevel::all);
        m_endpoint.set_access_channels(websocketpp::log::alevel::all ^ websocketpp::log::alevel::frame_payload);

        // Initialize Asio
        m_endpoint.init_asio();

        // Set the default message handler to the echo handler
        m_endpoint.set_message_handler(std::bind(
            &utility_server::echo_handler, this,
            std::placeholders::_1, std::placeholders::_2
        ));
    }

    void echo_handler(websocketpp::connection_hdl hdl, server::message_ptr msg) {
        // write a new message
        m_endpoint.send(hdl, msg->get_payload(), msg->get_opcode());
    }

    void run() {
        // Listen on port 9002
        m_endpoint.listen(9002);

        // Queues a connection accept operation
        m_endpoint.start_accept();

        // Start the Asio io_service run loop
        m_endpoint.run();
    }
private:
    server m_endpoint;
};

int main() {
    utility_server s;
    s.run();
    return 0;
}

全局:服务端端点类型别名

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

utility_server服务端类

成员变量

​ private:

​ server m_endpoint:端点

成员函数

​ public:

​ 构造函数:日志设置,初始化asio,设置默认消息处理函数以回显

​ run:监听9002端口,等待连接,启动Asio io_service的run循环

​ echo_handler:写消息send

主函数:启动服务端

utility_server s;
s.run();
④运行

见3.1.1的步骤6的④运行

步骤3:待

错误处理

步骤4:待

设置打开和关闭处理函数和连接数据结构

步骤5:待

更改基于URI的连接的消息处理函数,增加拒绝无效URIs的验证处理函数

步骤6:待

增加Admin命令(report total clients, cleanly shut down server)

步骤7:待

增加Broadcast命令

步骤8:待

增加TLS

3.3 广播-待

目录:tutorials/broadcast_tutorial/broadcast_tutorial.md

本教程将深入探讨如何创建高可扩展性、用于类似工作流的高性能websocket服务器。
将进入以下功能:

  • minimizing work done in handlers
  • using asio thread pool mode
  • teaming multiple endpoints
  • setting accept queue depth
  • tuning compile time buffer sizes
  • prepared messages
  • flow control
  • basic operating system level tuning, particularly increasing file descriptor limits.
  • measuring performance with wsperf / autobahn
  • tuning permessage-deflate compression settings

3.4 聊天服务器-待

目录:tutorials/chat_tutorial/chat_tutorial.md

目标:实现实时websocket聊天服务器

服务端

  • Nicknames
  • Channels
  • Subprotocol
  • Origin restrictions
  • HTTP statistics page

4、例子

目录:examples

4.1 associative_storage

①实现功能

重新定义连接关联关系

​ 将连接和消息信息包装成一个结构体存储

​ 同时见在连接的关联关系信息增加了排序函数

②编译命令

g++ associative_storage.cpp -lpthread

③代码解析

1° 全局:

服务端端点类型别名

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

using

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

2° connection_data:连接数据结构体

struct connection_data {
    int sessionid;		// 连接id
    std::string name;	// 消息内容
};

3° print_server类:

成员变量

​ private:

​ typedef std::map<connection_hdl,connection_data,std::owner_less<connection_hdl>> con_list;

​ key:连接句柄:类型是typedef lib::weak_ptr<void> connection_hdl;

​ value:连接数据(id和消息)

排序比较函数owner_less比较两个对象,若第一个对象的所有权小于第二个对象的返回true

​ int m_next_sessionid:下一个连接id

​ server m_server:服务端端点

​ con_list m_connections:保存连接的关联关系

成员函数

​ public:

​ 构造函数:初始化m_next_sessionid为1,端点(初始化asio,设置处理函数--打开、关闭、消息)

​ on_open处理函数:维护m_connections,打开连接后添加连接数据(id递增消息清空)到连接列表中

​ on_close处理函数:维护m_connections,从连接列表中删除指定连接并输出相关信息

​ on_message处理函数:维护m_connections,更新连接数据中消息并输出相关信息

​ get_data_from_hdl:检索连接列表m_connections,获取连接句柄对应的连接数据结构的引用

​ run:监听端口,等待连接,启动Asio io_service的run循环

4° 主函数

print_server server;
server.run(9002);

④疑惑解答

1° 为什么要使用std::owner_less对连接重新排序?基于什么考虑要什么设计

typedef std::map<connection_hdl,connection_data,std::owner_less<connection_hdl>> con_list;

⑤相关注释

1° std::owner_less

参考:https://cplusplus.com/reference/memory/owner_less/

头文件:<memory>

template <class Ptr> 
struct owner_less;
template <class T> 
struct owner_less<shared_ptr<T>>;
template <class T> 
struct owner_less<weak_ptr<T>>;

Owner-based less-than operation

函数对象,用于两个std::shared_ptrstd::weak_ptr对象的基于所有权的比较。

owned pointer:所有权指针--内存模型中的控制块中的指针

stored pointer:存储指针--内存模型中的指向对象的指针

std::owner_less是less或操作符<的替代,用于需要基于所有权指针而不是存储指针排序(<)的类型。

如果shared_ptr对象是别名(alias-constructed对象和他们的复制者),shared_ptr对象的存储指针(例如解引用指针*),可能区别于所有权指针(例如对象销毁时删除的指针)。

owner_less::operator()的比较依赖于shared_ptr或weak_ptr的成员函数owner_before。owner_less是用一个模仿binary_function的接口定义的,但带有额外的重载。

测试代码:

#include <iostream>
#include <memory>
#include <set>

int main()
{
    int *p = new int(10);
    std::cout << "*p: " << *p << std::endl; // 10

    std::shared_ptr<int> a(new int(20));                          // 创建一个值为20的int型智能指针a
    std::cout << "*a: " << *a << std::endl;                       // 20
    std::cout << "a.use_count(): " << a.use_count() << std::endl; // 1

    std::shared_ptr<int> b(a, p);                                 // 别名:
                                                                  // 内存模型中:指向对象的指针指向p,控制块中的指针指向a(共同拥有a)
    std::cout << "*a: " << *a << std::endl;                       // 20
    std::cout << "*b: " << *b << std::endl;                       // 10
    std::cout << "*(a.get()): " << *(a.get()) << std::endl;       // 20
    std::cout << "*(b.get()): " << *(b.get()) << std::endl;       // 10
    std::cout << "a.use_count(): " << a.use_count() << std::endl; // 2
    std::cout << "b.use_count(): " << b.use_count() << std::endl; // 2

    // standard set container: cannot contain duplicates.
    std::set<std::shared_ptr<int>> value_based; // uses std::less
    std::set<std::shared_ptr<int>, std::owner_less<std::shared_ptr<int>>> owner_based;

    value_based.insert(a);
    value_based.insert(b); // ok, different value

    owner_based.insert(a);
    owner_based.insert(b); // overwrites (same owned pointer)-所有权指针是同一个

    std::cout << "value_based.size() is " << value_based.size() << '\n'; // 2
    std::cout << "owner_based.size() is " << owner_based.size() << '\n'; // 1
}

4.2 broadcast_server

①实现功能

服务端的广播功能是指服务端向所有客户端发送消息的功能。当服务端需要向所有连接的客户端发送消息时,它可以通过广播功能将消息发送给所有客户端,而不需要单独发送给每个客户端。这种功能可以提高通信效率,减少服务端的通信开销。

核心:给每个连接都发送消息:m_server.send(*it,a.msg);

②编译命令

g++ broadcast_server.cpp -lpthread

③代码解析

全局:

action_type枚举类型

enum action_type {

SUBSCRIBE, // 打开连接

UNSUBSCRIBE, // 关闭连接

MESSAGE // 收到消息

};

action结构体:存储所有的动作(打开连接,关闭连接,收到消息等)

action_type type:动作类型

websocketpp::connection_hdl hdl:连接句柄

server::message_ptr msg:消息指针

内置无参构造和带参构造函数

broadcast_server类:

成员变量

​ private:

​ typedef std::set<connection_hdl,std::owner_less<connection_hdl>> con_list:存储所有连接

​ server m_server:服务端端点

​ con_list m_connections:连接列表

​ std::queue<action> m_actions:动作队列,多线程竞争资源,需要加锁控制

​ mutex m_action_lock:动作互斥量,控制m_actions

​ mutex m_connection_lock:连接

​ condition_variable m_action_cond:动作条件变量,处理函数进行通知

成员函数

​ public:

​ 构造函数:端点(初始化asio传输,设置处理函数--打开、关闭、消息)

​ run:监听端口,等待连接,启动Asio io_service的run循环

​ on_open处理函数:对动作互斥量加锁以添加SUBSCRIBE类型的动作,动作条件变量通知

​ on_close处理函数:对动作互斥量加锁以添加UNSUBSCRIBE类型的动作,动作条件变量通知

​ on_message处理函数:对动作互斥量加锁以添加MESSAGE类型的动作,动作条件变量通知

​ process_messages:死循环中,使用锁+条件变量等待动作通知,以对动作队列进行出队,根据最新的动类型对连接进行处理

​ SUBSCRIBE:对连接互斥量加锁,m_connections插入连接

​ UNSUBSCRIBE:对连接互斥量加锁,m_connections清除连接

​ MESSAGE:对连接互斥量加锁,遍历所有连接,广播消息

​ get_data_from_hdl:检索连接列表m_connections,获取连接句柄对应的连接数据结构的引用

主函数:

开启新线程t执行消息处理循环process_messages:动作处理-广播消息

主线程执行端点run函数,运行asio循环:连接处理

join阻塞等待t结束

总结:代码思路

为了统一处理服务端对所有连接广播消息:需要再打开和关闭连接时动态更新连接信息,当需要广播消息时针对这些连接遍历进行发送。

代码中将广播过程通过另一个线程进行处理,与asio循环线程分开,涉及到了共享变量的多线程访问问题:动作队列和连接列表,分别加锁进行控制。其中需要处理当动作准备好,需要执行相应的连接逻辑时,使用锁+条件变量的通知机制。

注意:多线程下

锁+条件变量:通知、控制共享变量

④疑惑解答

⑤相关注释

1° c++标准库中的锁

参考:https://blog.csdn.net/weixin_44477424/article/details/130694304

术语:RAII,Resource Acquisition Is Initialization

RAII原则是所有的资源都必须有管理对象,而资源的申请操作在管理对象的构造函数中进行,而资源的回收则在管理对象的析构函数中进行

C++新标准提供了lock_guard, unique_lock, shared_lock, 和 scoped_lock四种锁,用于各种复杂情况。这四种锁都是满足RAII风格。

lock_guard

在作用域内自动管理互斥量的锁定和解锁

lock_guard 对象被创建时,它会自动锁定互斥量,当对象离开作用域时,它会自动解锁互斥量。

lock_guard 不支持手动锁定和解锁,也不支持条件变量。

作用:线程之间对于共享变量的操作不互相干扰

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;
int shared_data = 0;

void increment() 
{
	std::lock_guard<std::mutex> lock(mtx);
	++shared_data;
	std::cout << "Incremented shared_data: " << shared_data << std::endl;
}

int main()
{
    std::thread t1(increment);
	std::thread t2(increment);

	t1.join();
	t2.join();
}

// 如果不加锁,increment后两行非原子性操作,多线程运行,输出存在共享变量shared_data可能已经被其他线程更改的情况

unique_lock

C++11中一个更灵活的锁,它允许手动锁定和解锁互斥量,以及与条件变量一起使用(是lock_guard的进阶版)。与 lock_guard 类似,unique_lock 也是一个 RAII 风格的锁,当对象离开作用域时,它会自动解锁互斥量。unique_lock 还支持延迟锁定、尝试锁定和可转移的锁所有权。

作用:通过条件变量实现互斥和协作,比如当一个线程准备好,通知另一个线程,比如实现pv操作

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
int shared_data = 0;
bool ready = false;

void increment() 
{
	std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; }); // 等待某个条件成立:lock 是一个已锁定的std::mutex对象。 { return ready; } 是一个lambda表达式,它返回一个布尔值,表示等待的条件
	++shared_data;
	std::cout << "Incremented shared_data: " << shared_data << std::endl;
}

void set_ready() 
{
	std::unique_lock<std::mutex> lock(mtx);
	std::cout << "set_ready finish!" << std::endl;
	ready = true;
	cv.notify_all();
}

int main()
{
    std::thread t1(increment);
	std::thread t2(increment);
    std::thread t3(set_ready);

	t1.join();
	t2.join();
    t3.join();
}
// 当t3线程中通过notify_all通知所有其他等待的线程已经准备好

shared_lock

C++14引入的锁,这是一个用于共享互斥量(如 std::shared_mutex 或 std::shared_timed_mutex)的锁,允许多个线程同时读取共享数据,但在写入数据时仍然保证互斥。shared_lock 也是一个 RAII 风格的锁,当对象离开作用域时,它会自动解锁共享互斥量。shared_lock 支持手动锁定和解锁,以及尝试锁定。

#include <iostream>
#include <shared_mutex>
#include <thread>

std::shared_mutex sh_mtx;
int shared_data = 0;

void read_data() 
{
	std::shared_lock<std::shared_mutex> lock(sh_mtx);
	std::cout << "Read shared_data: " << shared_data << std::endl;
}

void write_data() 
{
	std::unique_lock<std::shared_mutex> lock(sh_mtx);
	++shared_data;
    std::cout << "Incremented shared_data: " << shared_data << std::endl;
}

int main()
{
    std::thread t1(read_data);
	std::thread t2(write_data);
    std::thread t3(read_data);

	t1.join();
	t2.join();
    t3.join();
}

//  g++ lock.cpp -lpthread -std=c++17

scoped_lock

这是 C++17 引入的一个新锁,用于同时锁定多个互斥量,以避免死锁。scoped_lock 是一个 RAII 风格的锁,当对象离开作用域时,它会自动解锁所有互斥量。scoped_lock 不支持手动锁定和解锁,也不支持条件变量。它的主要用途是在需要同时锁定多个互斥量时提供简单且安全的解决方案。

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx1;
std::mutex mtx2;
int shared_data1 = 0;
int shared_data2 = 0;

void increment_both() {
    std::scoped_lock lock(mtx1, mtx2);
    ++shared_data1;
    ++shared_data2;
    std::cout << "Incremented shared_data1: " << shared_data1 << ", shared_data2: " << shared_data2 << std::endl;
}

int main() {
    std::thread t1(increment_both);
    std::thread t2(increment_both);

    t1.join();
    t2.join();

    return 0;
}
// g++ lock.cpp -lpthread -std=c++17

4.3 debug_client

①实现功能

增加时间打印,如建立连接、关闭连接、处理消息时

②编译命令

g++ debug_client.cpp -lpthread -lssl -lcrypto

③代码解析

perftest类

类型定义

​ public:

​ typedef std::chrono::duration<int,std::micro> dur_type; // 微秒

成员变量

​ private:

​ client m_endpoint:客户端端点

​ std::chrono::high_resolution_clock::time_point m_start:开始连接时间点

 std::chrono::high_resolution_clock::time_point m_socket_init:socket初始化时间点

​ std::chrono::high_resolution_clock::time_point m_tls_init:

​ std::chrono::high_resolution_clock::time_point m_open:打开连接时间点

​ std::chrono::high_resolution_clock::time_point m_message:

​ std::chrono::high_resolution_clock::time_point m_close:关闭连接时间点

成员函数

​ public:

​ 构造函数:日志设置,初始化asio,设置处理函数

​ start:连接uri,记录m_start,开启asio io_service异步循环

​ on_socket_init处理函数:记录m_socket_init

​ on_tls_init处理函数:初始化和配置一个SSL/TLS上下文

​ on_fail处理函数:打印连接句柄对应的连接指针的状态、原因等信息

​ on_open处理函数:记录m_open,发送text操作码-send

​ on_message处理函数:记录m_close,关闭连接-close

​ on_close处理函数:记录m_close,打印记录的时间点和时间段

主函数:调用端点的start函数

总结:增加了关键操作的时间点记录和打印

④疑惑解答

⑤相关注释

1° std::chrono

参考:https://blog.csdn.net/qq_21438461/article/details/131198438

概述

c++标准库中的一个组件:用于表示和处理时间

功能点

时间点:time_point

时间段:duration

时钟:当前时间点:system_clock:系统的实际时间,可能会受到系统时间调整的影响

​ steady_clock:稳定的时钟,时间从不调整

​ high_resolution_clock:提供最小的可表示的时间间隔

子类介绍和应用

system_clock:

#include <iostream>
#include <chrono>
#include <iomanip> //std::put_time:将时间格式为字符串

int main()
{
    // 1、获取当前时间点:now
    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
    // 2、从time_point获取具体时间:转化为年月日
    std::time_t tt = std::chrono::system_clock::to_time_t(now);
    std::tm* ptm = std::localtime(&tt);
    std::cout << "Current time is: " << std::put_time(ptm,"%c") << std::endl;
    // 3、进行时间运算
    std::chrono::system_clock::time_point in_an_hour = std::chrono::system_clock::now() + std::chrono::hours(1);
}

steady_clock

物理时间流逝的时钟,不受系统时间改变影响

#include <iostream>
#include <chrono>

int main()
{
    // 1、获取当前时间点:now
    std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
    // 2、计算经过的时间
    std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
    // 中间可能有些操作
    std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
    std::chrono::steady_clock::duration elapsed = end - start;
    // 3、转换时间单位:微秒-秒
    long long elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
}

high_resolution_clock

提供了最高的可用时间分辨率

#include <iostream>
#include <chrono>

int main()
{
    // 1、获取当前时间点:now
    std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now();
    // 2、计算经过的时间
    std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
    // 中间可能有些操作
    std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
    std::chrono::high_resolution_clock::duration elapsed = end - start;
    // 3、转换时间单位:纳秒-毫秒
    long long elapsed_microseconds = std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();

}

获取时间戳

用处:时间差,日志

#include <iostream>
#include <chrono>

int main()
{
    // 1、获取时间戳:now
    std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
    // 2、转换为具体的日期
    std::time_t t = std::chrono::system_clock::to_time_t(now);
    std::tm *now_tm = std::localtime(&t);

    char buffer[80];
    std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", now_tm);

    std::cout << "Current time: " << buffer << std::endl;
}

计时器实现

基本:计算时间差

#include <iostream>
#include <chrono>

int main()
{
    // 1、基本计时器:计算时间差
    std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now();
    // 中间操作
    std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> diff = end-start; // 计算时间差
    std::cout << "Code executed in " << diff.count() << " seconds" << std::endl;
}

高级计时器:暂停、重置,控制是否获取时间点

#include <iostream>
#include <chrono>

class Timer {
    private:
        bool running;
        std::chrono::time_point<std::chrono::high_resolution_clock> start_time, end_time;

    public:
        Timer() : running(false) {}

        void start() {
            running = true;
            start_time = std::chrono::high_resolution_clock::now();
        }

        void stop() {
            if (running) {
                end_time = std::chrono::high_resolution_clock::now();
                running = false;
            }
        }

        double elapsed() { // 只能记录每次开始到停止经过的时间,不能汇总统计
            if (running) {
                return std::chrono::duration<double>(std::chrono::high_resolution_clock::now() - start_time).count();
            }
            else {
                return std::chrono::duration<double>(end_time - start_time).count();
            }
        }

        void reset() {
            running = false;
        }
};

int main() {
    Timer timer;
    timer.start();

    // Some code

    timer.stop();
    std::cout << "Elapsed time: " << timer.elapsed() << " seconds." << std::endl;

    // Continue with the timer
    timer.start();

    // Some code

    timer.stop();
    std::cout << "Total elapsed time: " << timer.elapsed() << " seconds." << std::endl;

    return 0;
}

使用std::chrono作为通用的时间参数

std::chrono::duration的应用

#include <iostream>
#include <chrono>

int main()
{
    // 1、std::chrono::duration
    // 创建duration对象
    std::chrono::duration<int> twenty_seconds(20);
    std::chrono::duration<double, std::ratio<60>> half_a_minute(0.5);
    std::chrono::duration<long, std::ratio<1,1000>> one_millisecond(1);
    // 操作:加减、比大小等
}

时间单位转换

#include <iostream>
#include <chrono>

int main()
{
    // 2、时间单位转换
    // 毫秒-秒
    std::chrono::milliseconds ms(1000);
    std::chrono::seconds sec = std::chrono::duration_cast<std::chrono::seconds>(ms);
    // 使用radio自定义时间单位
    using half_seconds = std::chrono::duration<double, std::ratio<1, 2>>;
}

4.4 debug_server

①实现功能

自定义配置

②编译命令

g++ debug_server.cpp -lpthread

③代码解析

debug_custom结构体:自定义配置,继承自websocketpp::config::debug_asio

只是自定义和别名设置,暂未体会到有什么修改点

成员变量

​ static const long timeout_open_handshake = 0:打开连接超时时间

​ 结构体transport_config:传输配置,继承自websocketpp::config::debug_asio::transport_config

​ 类型别名

​ 类型别名

全局

// 使用自定义配置定义服务端端点
typedef websocketpp::server<debug_custom> server;

全局处理函数

validate验证处理函数:未作处理,返回true

on_http处理函数:获取连接句柄的连接指针,获取连接指针的请求体并设置

on_fail处理函数:输出失败信息

on_close处理函数:输出关闭

on_message处理函数:输出收到的发送并发送以回显

主函数

1、创建服务端端点

2、配置

设置日志

初始化Asio

注册处理函数:消息,http,失败,关闭连接,验证

3、监听端口

4、异步开启接受请求-循环

5、开启ASIO io_service运行循环-阻塞至所有的异步线程结束

④疑惑解答

⑤相关注释

4.5 dev+

①实现功能

②编译命令

g++ main.cpp -lboost_timer

③代码解析

④疑惑解答

⑤相关注释

4.6 echo_client

①实现功能

客户端收到消息发送给服务端,以在服务端回显

②编译命令

g++ echo_client.cpp -lpthread

③代码解析

特殊处理:on_message处理函数:收到消息后进行消息的发送以回显

⑤相关注释

4.7 echo_server

①实现功能

服务端收到消息发送给客户端,以在客户端回显

②编译命令

g++ echo_server.cpp -lpthread

③代码解析

特殊处理:on_message处理函数:收到消息后进行消息的发送以回显

④疑惑解答

⑤相关注释

4.8 echo_server_both

①实现功能

较4.7不同的是,定义和两个服务端端点实现回显功能

②编译命令

g++ echo_server.cpp -lpthread

③代码解析

定义和两个服务端端点实现回显功能:分别一般端点(监听80端口)和使用tls的端点(监听443端口)

其中

1° 直接使用asio::io_service绑定两个io对象,之后直接调用run函数阻塞至异步操作结束

2° on_message设置为模板函数,模板参数为服务端类型,适应不同端点的需要

④疑惑解答

⑤相关注释

4.9 echo_server_tls

①实现功能

安全传输层协议:增加设置TLS两种模式和证书

②编译命令

g++ echo_server_both.cpp -lpthread -lssl -lcrypto

③代码解析

④疑惑解答

⑤相关注释

4.10 enriched_storage

①实现功能

较4.1,减少了连接信息的存储和输出,使用了自定义配置

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.11 external_io_service⭐

①实现功能

asio::io_service service的充分使用;

有两个服务端端点对象,asio::io_service绑定了这两个对象后,使用run函数可以阻塞等待两个端点的相关异步操作结束。

②编译命令

③代码解析

两个服务端对象

1° tcp_echo_server结构体:自定义tcp端点类

成员变量:

​ asio::io_service & m_service:io服务

​ asio::ip::tcp::acceptor m_acceptor:tcp接收器

成员函数

​ 构造函数(io服务, 端口):初始化成员变量,调用start_accept开启连接接收

​ start_accept:创建新连接,async_accept异步接受连接(处理函数:handle_accept)

​ handle_accept:调用新连接的start函数

tcp_echo_session:tcp连接类

直接操作asio::ip::tcp::socket m_socket;

进行异步的读写

2° ws_echo_server:默认配置定义的端点类型

④疑惑解答

⑤相关注释

4.12 handler_switch

①实现功能

set_message_handler的处理函数内存对连接再次设置set_message_handler,以改变处理函数

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.13 iostream_server

①实现功能

服务端日志输出到文件中:分为缓冲和非缓冲

②编译命令

③代码解析

s->get_alog():端点->获取日志对象,写道文件中output.log

C++ iostream并不支持异步i/o。

这边使用了两种策略:

1° 缓冲I/O将以块形式从stdin读取,直到EOF。这对于重放屏蔽连接非常有效,就像在自动测试中一样。

2° 然而,如果服务器是实时使用的,假设输入是从其他地方实时传输的,则此策略将导致小消息永远被缓冲。下面的非缓冲策略一次从stdin读取一个字符。这是低效的,对于更严重的使用,应该用特定于平台的异步i/o技术来代替,如选择、轮询、IOCP等

④疑惑解答

⑤相关注释

4.14 print_client

①实现功能

on_message处理函数简单输出消息

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.15 print_client_tls⭐

①实现功能

②编译命令

③代码解析

on_tls_init处理函数:tls的初始化

WebSocket++核心和Asio传输不处理TLS上下文的创建和设置。提供此回调是为了让最终用户可以使用对其应用程序有意义的任何设置来设置TLS上下文。

由于Asio和OpenSSL没有为非常常见的连接情况提供很好的文档,并且没有实际执行服务器证书的基本验证,因此本示例包括以下合理默认设置和验证步骤的基本实现(使用Asio和OpenSSL):

禁止SSLv2和SSLv3

加载受信任的CA证书并验证服务器证书是否受信任。

验证主机名是否与证书上的通用名称或使用者替代名称之一匹配。

这并不是完美TLS客户端的详尽参考实现,而是构建安全TLS加密WebSocket客户端的合理起点

如果任何TLS、Asio或OpenSSL专家认为这些设置是糟糕的默认设置,或者严重缺少步骤,请打开GitHub问题或在项目邮件列表上留言。

请注意,捆绑的CA证书ca-chain.cert.pem是对与echo_server_tls捆绑的证书进行签名的CA证书。您可以将print_client_tls与此CA证书一起使用来连接到echo_server_tls,只要您使用/etc/hosts或类似的东西来伪造该证书上的一个名称(例如,websockettpp.org)。

④疑惑解答

⑤相关注释

4.16 print_server

①实现功能

on_message处理函数输出收到的消息

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.17 scratch_client

①实现功能

a snapshot of the WebSocket++ utility client tutorial

忽略

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.18 scratch_server

①实现功能

通过-d控制是否设置日志

自定义配置

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.19 simple_broadcast_server

①实现功能

较4.2不同的是,使用set存储连接关系,只简单的进行了连接句柄的保存,删除

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.20 sip_client

①实现功能

指定sip子协议:

con->add_subprotocol("sip");

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.21 subprotocol_server

①实现功能

获取收到消息的请求头中的Cache-Control的值,输出,并选择第一个为子协议

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.22 telemetry_client

①实现功能

每秒往服务端发送包含整型计数的消息

②编译命令

③代码解析

客户端开启异步线程执行telemetry_loop

​ 此函数中执行循环,连接未关闭,计数并发送值,等待1s后重复此步骤,若关闭,每隔一秒尝试

​ 连接开启标志m_open和连接关闭标志m_done由于在不同的线程中通过加锁访问和设置

④疑惑解答

⑤相关注释

4.23 telemetry_server

①实现功能

开启计时器,每个1s向客户端发送消息--很像心跳哎

②编译命令

③代码解析

telemetry_server类:

telemetry服务端接受连接,每秒给每个客户端发送一封包含整型计数的消息。此示例可以用作程序的基础,这些程序公开用于日志记录、仪表板等的遥测-telemetry数据流。

此示例使用基于定时器的并发方法,并且是自包含的和单线程的。有关使用线程而非定时器进行类似遥测设置的示例,请参阅遥测客户端。

此示例还包括一个示例简单HTTP服务器,该服务器为显示计数的web仪表板提供服务。这种简单的设计适用于将少量文件传递给少量客户端。它非常适合像嵌入式仪表板这样不希望额外的HTTP服务器为静态文件提供服务的复杂情况。

这种设计将在高流量或DoS条件下失效。在这种情况下,最好将HTTP请求代理到真正的HTTP服务器。

成员变量

​ private:

​ server m_endpoint:服务端端点

​ con_list m_connections:set,连接集合

 server::timer_ptr m_timer:计时器

​ std::string m_docroot:文件根目录

​ uint64_t m_count: 遥测数据 // Telemetry data

成员函数:

​ public:

​ 构造函数:初始化m_count,设置日志,初始化asio传输策略,设置处理函数

​ run:设置m_docroot,监听端口,开启异步接受连接,设置异步计时器,开始io_service的run方法

​ set_timer:在m_endpoint上设置一个定时器,该定时器在1000毫秒(1秒)后触发,触发时调用on_timer方法

​ on_timer:迭代所有连接,发送遥测数据,调用set_timer

​ on_http:提供客户端请求的文件服务

​ on_open/on_close:维护连接列表

主函数:调用端点的run方法

④疑惑解答

⑤相关注释

4.24 testee_client

①实现功能

连接服务端和代理服务端

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.25 testee_server

①实现功能

支持单线程和多线程,在多线程内部调用server::run的函数并使用join阻塞等待至线程结束

②编译命令

③代码解析

④疑惑解答

⑤相关注释

4.26 utility_client

①实现功能

a snapshot of the WebSocket++ utility client tutorial

②编译命令

③代码解析

④疑惑解答

⑤相关注释

5、编译

5.1 cmake

5.1.1 编译命令

在解压后第一层目录下输入:

mkdir build
cd build

注意CMakeLists.txt中218行会导致cmake编译错误,需要进行

将find_package (Boost 1.39.0 COMPONENTS "${WEBSOCKETPP_BOOST_LIBS}")中的引号删去,如下

find_package (Boost 1.39.0 COMPONENTS ${WEBSOCKETPP_BOOST_LIBS})

输入

cmake -G 'Unix Makefiles' \
     -D BUILD_EXAMPLES=ON \
     -D WEBSOCKETPP_ROOT=/usr/local/include/ \
     -D ENABLE_CPP11=OFF ..

编译所有

make

单独编译

make -C ../examples/sip_client

5.1.2 CMakeLists.txt

5.1.2.1 CMakeLists.txt
# 1、cmake最低版本要求,必须指定
cmake_minimum_required (VERSION 2.8.8)
# 2、Paths-项目路径设置-未用到
set (WEBSOCKETPP_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) #根目录:当前处理的CMakeLists.txt所在的目录
set (WEBSOCKETPP_INCLUDE ${WEBSOCKETPP_ROOT}/websocketpp) #头文件目录:根下include目录
set (WEBSOCKETPP_BUILD_ROOT ${CMAKE_CURRENT_BINARY_DIR}) #构建根目录,未用到
# CMAKE_CURRENT_BINARY_DIR指CMakeLists.txt对应的构建目录-事实就是其所在的目录
# 考虑到可能会新建一个目录分类编译文件和原始文件,需要一定的修改(???)
set (WEBSOCKETPP_BIN ${WEBSOCKETPP_BUILD_ROOT}/bin) #构建根目录下bin,未用到
set (WEBSOCKETPP_LIB ${WEBSOCKETPP_BUILD_ROOT}/lib) #构建根目录下lib,为用到
# 3、项目名和版本
set (WEBSOCKETPP_MAJOR_VERSION 0) #主版本号
set (WEBSOCKETPP_MINOR_VERSION 8) #次版本号
set (WEBSOCKETPP_PATCH_VERSION 2) #补丁版本号
set (WEBSOCKETPP_VERSION ${WEBSOCKETPP_MAJOR_VERSION}.${WEBSOCKETPP_MINOR_VERSION}.${WEBSOCKETPP_PATCH_VERSION}) #版本号:0.8.2
# ???
if(POLICY CMP0048)
  cmake_policy(GET CMP0048 _version_policy) #查询CMP0048的值存在变量_version_policy中,未用到
endif()
# 项目名
if(_version_allowed STREQUAL NEW)
  project (websocketpp VERSION ${WEBSOCKETPP_VERSION})
else()
  project (websocketpp)
endif()
# 要求在build文件夹内仍然保持相应的文件夹结构
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# 缓存变量INSTALL_INCLUDE_DIR,初始值include,类型CACHE,期待是一个路径,最后是描述信息
set(INSTALL_INCLUDE_DIR include CACHE PATH "Installation directory for header files")
# 设置DEF_INSTALL_CMAKE_DIR:指定安装的CMake目录
if (WIN32 AND NOT CYGWIN)
  set (DEF_INSTALL_CMAKE_DIR cmake)
else ()
  set (DEF_INSTALL_CMAKE_DIR lib/cmake/websocketpp)
endif ()
set (INSTALL_CMAKE_DIR ${DEF_INSTALL_CMAKE_DIR} CACHE PATH "Installation directory for CMake files")

# 相对路径变成绝对路径:未用到
foreach (p INCLUDE CMAKE)
  set (var INSTALL_${p}_DIR)
  if (NOT IS_ABSOLUTE "${${var}}") #不是绝对路径
    set (${var} "${CMAKE_INSTALL_PREFIX}/${${var}}") #CMAKE_INSTALL_PREFIX的默认/usr/local
  endif ()
endforeach ()

# 设置CMake库查找策略
if (COMMAND cmake_policy) #判断是否存在cmake_policy命令
    cmake_policy (SET CMP0003 NEW) #当设置为OLD时,find_package会尝试在模块路径中查找包,如果找不到,它会回退到系统路径。当设置为NEW时,find_package只会查找模块路径中的包,不会回退到系统路径。
    cmake_policy (SET CMP0005 NEW)
endif ()

# 禁止不必要的编译类型:
# CMAKE_CONFIGURATION_TYPES设置可用的构建配置类型
# 允许Release-发布版本;RelWithDebInfo-带有调试信息的发布版本;Debug-调试版本
set (CMAKE_CONFIGURATION_TYPES "Release;RelWithDebInfo;Debug" CACHE STRING "Configurations" FORCE) #FORCE强制覆盖现有的缓存值

# Include our cmake macros
set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) #将 CMAKE_CURRENT_SOURCE_DIR下的cmake子目录添加到CMAKE_CURRENT_SOURCE_DIR下的cmake子目录添加到CMAKE_MODULE_PATH的搜索路径中的搜索路径中。这样,CMake在搜索模块时就会在新的路径下查找。
# CMAKE_MODULE_PATH:指定CMake模块的搜索路径
# CMAKE_CURRENT_SOURCE_DIR:源代码目录
include (CMakeHelpers) #包含CMakeHelpers模块-另一个cmake模块

# 4、自定义构建-build
# Override from command line "CMake -D<OPTION>=TRUE/FALSE/0/1/ON/OFF"
# 4.1 构建选项设置
option (ENABLE_CPP11 "Build websocketpp with CPP11 features enabled." TRUE)
option (BUILD_EXAMPLES "Build websocketpp examples." FALSE)
option (BUILD_TESTS "Build websocketpp tests." FALSE)
# 4.1.1 BUILD_TESTS或BUILD_EXAMPLES选项
if (BUILD_TESTS OR BUILD_EXAMPLES)

    enable_testing ()

    # 4.1.1.1 编译特定设置:动态链接库,编译器标志(区别不同的编译器)

    set (WEBSOCKETPP_PLATFORM_LIBS "")
    set (WEBSOCKETPP_PLATFORM_TLS_LIBS "")
    set (WEBSOCKETPP_BOOST_LIBS "")

    # VC9 and C++11 reasoning
    if (ENABLE_CPP11 AND MSVC AND MSVC90)
        message("* Detected Visual Studio 9 2008, disabling C++11 support.")
        set (ENABLE_CPP11 FALSE)
    endif ()

    # 检测是否使用的是 Clang 编译器:Detect clang. Not officially reported by cmake.
    # 是:设置CMAKE_COMPILER_IS_CLANGXX标志为1
    execute_process(COMMAND "${CMAKE_CXX_COMPILER}" "-v" ERROR_VARIABLE CXX_VER_STDERR)
    if ("${CXX_VER_STDERR}" MATCHES ".*clang.*")
        set (CMAKE_COMPILER_IS_CLANGXX 1)
    endif ()

    # C++11 defines
    if (ENABLE_CPP11)
        if (MSVC)
            add_definitions (-D_WEBSOCKETPP_CPP11_FUNCTIONAL_)
            add_definitions (-D_WEBSOCKETPP_CPP11_SYSTEM_ERROR_)
            add_definitions (-D_WEBSOCKETPP_CPP11_RANDOM_DEVICE_)
            add_definitions (-D_WEBSOCKETPP_CPP11_MEMORY_)
        else()
            add_definitions (-D_WEBSOCKETPP_CPP11_STL_)
        endif()
    endif ()

    # Visual studio设置。。。
    
    # g++
    if (CMAKE_COMPILER_IS_GNUCXX)
        if (NOT APPLE)
        	# 设置动态链接库:线程库和实时扩展库(时间)
            set (WEBSOCKETPP_PLATFORM_LIBS pthread rt)
        else()
            set (WEBSOCKETPP_PLATFORM_LIBS pthread)
        endif()
        # 设置tls动态链接库
        set (WEBSOCKETPP_PLATFORM_TLS_LIBS ssl crypto)
        # 设置boost动态链接库
        set (WEBSOCKETPP_BOOST_LIBS system thread)
        # 设置C++代码编译器的标志
        set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")
        if (NOT APPLE)
        	# 为 C++编译器添加编译标志
            add_definitions (-DNDEBUG -Wall -Wcast-align) # todo: should we use CMAKE_C_FLAGS for these?
        endif ()

        # Try to detect version. Note: Not tested!
        execute_process (COMMAND ${CMAKE_CXX_COMPILER} "-dumpversion" OUTPUT_VARIABLE GCC_VERSION)
        if ("${GCC_VERSION}" STRGREATER "4.4.0")
            message("* C++11 support partially enabled due to GCC version ${GCC_VERSION}")
            set (WEBSOCKETPP_BOOST_LIBS system thread)
        endif ()
    endif ()

    # clang设置。。。
    
    # OSX, can override above.
    if (APPLE)
        add_definitions (-DNDEBUG -Wall)
    endif ()

    if (BUILD_EXAMPLES)
        list (APPEND WEBSOCKETPP_BOOST_LIBS random)
    endif()

    if (BUILD_TESTS)
        list (APPEND WEBSOCKETPP_BOOST_LIBS unit_test_framework) # 测试套件
    endif()

    # 4.1.1.2 依赖

	# BOOST_ROOT获取
	# ①环境变量,②cmake -DBOOST_ROOT=path命令行传递,③之前运行过的cmake缓存
    if (NOT "$ENV{BOOST_ROOT_CPP11}" STREQUAL "") #非空
        # Scons documentation for BOOST_ROOT_CPP11:
        # "look for optional second boostroot compiled with clang's libc++ STL library
        # this prevents warnings/errors when linking code built with two different
        # incompatible STL libraries."
        file (TO_CMAKE_PATH "$ENV{BOOST_ROOT_CPP11}" BOOST_ROOT)
        set (BOOST_ROOT ${BOOST_ROOT} CACHE PATH "BOOST_ROOT dependency path" FORCE)
    endif ()
    if ("${BOOST_ROOT}" STREQUAL "") #空
        file (TO_CMAKE_PATH "$ENV{BOOST_ROOT}" BOOST_ROOT)
        # Cache BOOST_ROOT for runs that do not define $ENV{BOOST_ROOT}.
        set (BOOST_ROOT ${BOOST_ROOT} CACHE PATH "BOOST_ROOT dependency path" FORCE)
    endif ()

    message ("* Configuring Boost")
    message (STATUS "-- Using BOOST_ROOT")
    message (STATUS "       " ${BOOST_ROOT}) #打印不出来???

    if (MSVC)
        set (Boost_USE_MULTITHREADED TRUE)
        set (Boost_USE_STATIC_LIBS TRUE)
    else ()
        set (Boost_USE_MULTITHREADED FALSE) #是否使用多线程版本的Boost库
        set (Boost_USE_STATIC_LIBS FALSE)	#是否使用静态链接的Boost库
    endif ()

	if (BOOST_STATIC)
		set (Boost_USE_STATIC_LIBS TRUE)
	endif ()

    if (NOT Boost_USE_STATIC_LIBS)
        add_definitions (-DBOOST_TEST_DYN_LINK)
    endif ()

    set (Boost_FIND_REQUIRED TRUE) #设置Boost库的查找为必需。若找不到Boost库,则会停止并显示错误。
    set (Boost_FIND_QUIETLY TRUE)  #在查找Boost库时保持安静,不显示任何输出
    set (Boost_DEBUG FALSE)		   #关闭Boost查找的调试输出
    set (Boost_USE_MULTITHREADED TRUE) #设置使用多线程版本的Boost库
    set (Boost_ADDITIONAL_VERSIONS "1.39.0" "1.40.0" "1.41.0" "1.42.0" "1.43.0" "1.44.0" "1.46.1" ) # todo: someone who knows better spesify these!
    # 列出要查找的Boost版本。这通常是为了兼容性或特定版本需求

    #find_package (Boost 1.39.0 COMPONENTS "${WEBSOCKETPP_BOOST_LIBS}")
    find_package (Boost 1.39.0 COMPONENTS ${WEBSOCKETPP_BOOST_LIBS}) #查找版本为1.39.0的Boost库,并指定要查找的组件

    if (Boost_FOUND)
        # Boost is a project wide global dependency.
        include_directories (${Boost_INCLUDE_DIRS}) #头文件
        link_directories (${Boost_LIBRARY_DIRS})	#库文件

        # Pretty print status:通过foreach循环,打印出找到的头文件和库文件目录以及库名称
        message (STATUS "-- Include Directories")
        foreach (include_dir ${Boost_INCLUDE_DIRS})
            message (STATUS "       " ${include_dir})
        endforeach ()
        message (STATUS "-- Library Directories")
        foreach (library_dir ${Boost_LIBRARY_DIRS})
            message (STATUS "       " ${library_dir})
        endforeach ()
        message (STATUS "-- Libraries")
        foreach (boost_lib ${Boost_LIBRARIES})
            message (STATUS "       " ${boost_lib})
        endforeach ()
        message ("")
    else ()
        message (FATAL_ERROR "Failed to find required dependency: boost")
    endif ()

    find_package(OpenSSL)
    find_package(ZLIB)
endif()

# 5、添加项目

# Add main library:主要代码在websocketpp中,都是头文件
add_subdirectory (websocketpp) #向当前构建中添加子目录

# Add examples
if (BUILD_EXAMPLES)
    include_subdirs ("examples") #添加构建子目录
endif ()

# Add tests
if (BUILD_TESTS)
    include_subdirs ("test") #添加构建子目录
endif ()

print_used_build_config() #打印

export (PACKAGE websocketpp) #导出构建目标

include(CMakePackageConfigHelpers) #添加配置文件模块
configure_package_config_file(websocketpp-config.cmake.in #输入文件路径:配置文件的模板文件,其中包含了需要被替换的变量占位符,以及可能的逻辑代码。
  "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/websocketpp-config.cmake" #输出文件路径:生成的配置文件的输出路径
  PATH_VARS INSTALL_INCLUDE_DIR #指定了需要在配置文件中定义的路径变量
  INSTALL_DESTINATION "${INSTALL_CMAKE_DIR}" #参数指定了安装时配置文件应该放置的目
  NO_CHECK_REQUIRED_COMPONENTS_MACRO #设置为true:不要在生成的配置文件中包含检查必需组件的宏
) #生成package的配置文件
write_basic_package_version_file("${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/websocketpp-configVersion.cmake" #输出文件名
  VERSION ${WEBSOCKETPP_VERSION} #指定版本号
  COMPATIBILITY ExactVersion) #兼容性策略 #生成一个基本的版本信息文件

# Install the websocketpp-config.cmake and websocketpp-configVersion.cmake
# 在构建完成后应该安装哪些文件或目录
install (FILES
  "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/websocketpp-config.cmake"
  "${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/websocketpp-configVersion.cmake"
  DESTINATION "${INSTALL_CMAKE_DIR}" COMPONENT dev) #安装的文件属于哪个组件

使用命令

# 输出变量
message("MY_VARIABLE: ${MY_VARIABLE}") 
# STREQUAL:比较两个字符串是否相等
# 打印属性USE_FOLDERS值
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
get_property(USE_FOLDERS GLOBAL PROPERTY USE_FOLDERS)  
if(USE_FOLDERS STREQUAL ON)  
  message("USE_FOLDERS is set to ON")  
else()  
  message("USE_FOLDERS is not set to ON")  
endif()

总结:

自定义选项和参数

ENABLE_CPP11:是否使用CPP11特性,默认TRUE

BUILD_EXAMPLES :是否构建websocketpp examples,默认FALSE

BUILD_TESTS:是否构建websocketpp tests,默认FALSE

-DBOOST_ROOT=path:

5.1.3 websocketpp-config.cmake.in

# - Config file for the websocketpp package
# It defines the following variables
#  WEBSOCKETPP_FOUND - indicates that the module was found
#  WEBSOCKETPP_INCLUDE_DIR - include directories

@PACKAGE_INIT@
set_and_check(WEBSOCKETPP_INCLUDE_DIR "@PACKAGE_INSTALL_INCLUDE_DIR@")
set(WEBSOCKETPP_FOUND TRUE)

#This is a bit of a hack, but it works well. It also allows continued support of CMake 2.8
if(${CMAKE_VERSION} VERSION_GREATER 3.0.0 OR ${CMAKE_VERSION} VERSION_EQUAL 3.0.0)
  add_library(websocketpp::websocketpp INTERFACE IMPORTED)
  set_target_properties(websocketpp::websocketpp PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${WEBSOCKETPP_INCLUDE_DIR}")
endif()

5.1.4 CMakeHelpers.cmake+

print_used_build_config

add_source_folder folder_name

init_target NAME

build_executable TARGET_NAME

build_test TARGET_NAME

final_target

link_boost

link_openssl

link_zlib

include_subdirs PARENT

5.2 python

①编译命令

初始化编译环境

apt-get update
apt-get install gcc g++ build-essential libssl-dev scons

安装完boost后(下载源码①./bootsrap.sh②./b2③./b2 install)配置环境变量

vim /etc/profile

添加以下两行

export BOOST_INCLUDES=/usr/local/include/boost
export BOOST_LIBS=/usr/local/lib

保存退出后,执行以下命令使之生效

source /etc/profile

切换到websocket 目录 , 进行编译。

scons

如果重复编译,清除上次编译结果,输入

scons -c

posted on 2023-12-20 16:25  circlelll  阅读(196)  评论(0编辑  收藏  举报