原文:
https://docs.websocketpp.org/md_tutorials_utility_client_utility_client.html
注意网页上的内容不完整,需要在github里面获取完整的文档。
一. 初始设置与基础知识
设置基本类型,打开和关闭连接,发送和接收消息。
Step 1
一个基本的程序循环,提示用户输入命令,然后进行处理。在本教程中,我们将修改这个程序,通过WebSocket连接从远程服务器执行任务并获取数据。
Build
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;
}
Step 2 添加WebSocket++的包含文件并设置一个端点类型
添加WebSocket++的包含文件并设置一个端点类型。
WebSocket++包含两种主要的对象类型:端点和连接。端点创建和启动新的连接,并维护这些连接的默认设置。端点还管理任何共享的网络资源。
连接存储与每个WebSocket会话相关的信息。
注意:一旦连接启动,端点和连接之间就没有联系了。所有默认设置都由端点复制到新连接中。更改端点的默认设置只会影响未来的连接。连接不会保留与其关联的端点的链接。端点也不会维护一个未完成连接的列表。如果您的应用程序需要遍历所有连接,则需要自行维护一个连接列表
。
注意:一旦连接启动,端点和连接之间就没有联系了。所有默认设置都由端点复制到新连接中。更改端点的默认设置只会影响未来的连接。连接不会保留与其关联的端点的链接。端点也不会维护一个未完成连接的列表。如果您的应用程序需要遍历所有连接,则需要自行维护一个连接列表。
WebSocket++端点是通过将端点角色与端点配置结合而成的。有两种不同类型的端点角色,分别用于WebSocket会话中的客户端和服务器角色。由于这是一个客户端教程,我们将使用客户端角色websocketpp::client,该角色由<websocketpp/client.hpp>头文件提供。
术语:
端点配置
WebSocket++端点具有一组可以在编译时通过config模板参数进行配置的设置。配置是一个包含类型和静态常量的结构体,用于生成具有特定属性的端点。根据使用的配置,端点将具有不同的可用方法,并可能具有其他第三方依赖项
。
端点角色使用一个名为config的模板参数来在编译时配置端点的行为。对于此示例,我们将使用库中提供的名为asio_client的默认配置,该配置由<websocketpp/config/asio_no_tls_client.hpp>提供。这是一个使用boost::asio提供网络传输且不支持基于TLS的安全性的客户端配置。稍后我们将讨论如何在WebSocket++应用程序中引入基于TLS的安全性,以及其他库存配置的更多信息以及如何构建自定义配置。
将配置与端点角色结合起来生成一个完全配置的端点。由于这个类型会经常使用,所以我建议在这里使用
typedef
。
typedef websocketpp::client<websocketpp::config::asio_client> client
Build
添加WebSocket++库后,我们的程序增加了一些依赖项,必须在构建系统中进行处理。首先,WebSocket++和Boost库的头文件必须在构建系统的包含搜索路径中。具体如何做取决于WebSocket++头文件的安装位置和使用的构建系统。
除了新的头文件外,
boost::asio
还依赖于
boost_system
共享库。需要将其添加到链接器中(可以是静态或动态)。请参考构建环境文档以获取关于链接到共享库的说明。
clang++ step2.cpp -lboost_system
迄今为止的代码
#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;
}
Step 3 创建处理初始化和设置后台线程的端点包装对象
创建处理初始化和设置后台线程的端点包装对象。
为了在后台进行网络处理时处理用户输入,我们将使用单独的线程来处理WebSocket++处理循环。这样,主线程就可以自由地处理前台用户输入。为了为我们的线程和端点启用简单的RAII风格资源管理,我们将使用一个包装对象,在其构造函数中配置它们。
术语:websocketpp::lib命名空间
WebSocket++设计用于与C++11标准库一起使用。由于这在流行的构建系统中并不普遍可用,所以在C++98构建环境中可以使用Boost库作为C++11标准库的补充。
websocketpp::lib
命名空间由库及其相关示例使用,以抽象出两者之间的区别。websocketpp::lib::shared_ptr将在C++11环境中评估为
std::shared_ptr
,否则评估为
boost::shared_ptr
。
本教程使用
websocketpp::lib
包装器,因为它不知道读者的构建环境是什么。对于您的应用程序,除非您对类似的可移植性感兴趣,否则可以直接使用
boost
或
std
版本的这些类型。
在
websocket_endpoint
构造函数中,发生了几件事情:
首先,我们通过清除所有访问和错误记录通道将端点的日志记录行为设置为静默。
m_endpoint.clear_access_channels(websocketpp::log::alevel::all);
m_endpoint.clear_error_channels(websocketpp::log::elevel::all);
接下来,我们初始化底层的传输系统,并将其设置为永久模式。在永久模式下,当端点没有连接时,其处理循环不会自动退出。这很重要,因为我们希望在应用程序运行时该端点保持活动状态,并根据需要处理新的 WebSocket 连接请求。这两种方法都是针对 asio 传输的特定方法。在使用非 asio 配置的端点中,它们将不是必需的或存在的。
m_endpoint.init_asio();
m_endpoint.start_perpetual();
最后,我们启动一个线程来运行客户端端点的运行方法。当端点正在运行时,它将处理连接任务(读取和传递传入的消息,对传出的消息进行帧化和发送等)。由于它在永久模式下运行,当没有活动的连接时,它会等待新的连接。
m_thread.reset(new websocketpp::lib::thread(&client::run, &m_endpoint));
Build
现在,我们的客户端端点模板实际上被实例化了,会出现一些更多的链接器依赖项。特别是,WebSocket客户端需要一个具有密码学安全性的随机数生成器。WebSocket++能够使用
boost_random
或C++11标准库来实现这个目的。因为此示例还使用了线程,如果我们没有可用的C++11 std::thread,我们将需要包含
boost_thread
。
Clang (C++98 & boost)
clang++ step3.cpp -lboost_system -lboost_random -lboost_thread
Clang (C++11)
clang++ -std=c++0x -stdlib=libc++ step3.cpp -lboost_system -D_WEBSOCKETPP_CPP11_STL_
G++ (C++98 & Boost)
g++ step3.cpp -lboost_system -lboost_random -lboost_thread
G++ v4.6+ (C++11)
g++ -std=c++0x step3.cpp -lboost_system -D_WEBSOCKETPP_CPP11_STL_
迄今为止的代码
#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.reset(new 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;
}
Step 4 打开WebSocket连接
这一步将在utility_client中添加两个新的命令。一个是打开新连接的能力,另一个是查看先前打开的连接的信息的能力。每个打开的连接都会被分配一个整数连接ID,程序的用户可以使用该ID与该连接进行交互。
新连接的元数据对象
为了跟踪每个连接的信息,定义了一个
connection_metadata
对象。该对象存储了数字连接ID和一些字段,这些字段在连接被处理时将被填充。最初,这包括连接的状态(正在打开、打开、失败、关闭等)、连接到的原始URI、来自服务器的标识值以及连接失败/关闭的原因的描述。未来的步骤将向此元数据对象添加更多信息。
更新websocket_endpoint
websocket_endpoint
对象增加了一些新的数据成员和方法。它现在跟踪连接ID与其关联的元数据之间的映射,以及下一个顺序ID号以供分配。
connect()
方法启动新的连接。
get_metadata
方法根据ID检索元数据。
connect方法
通过三个步骤来启动一个新的WebSocket连接。首先,通过
endpoint::get_connection(uri)
创建一个连接请求。接下来,对连接请求进行配置。最后,通过
endpoint::connect()
将连接请求提交回
endpoint
,将其添加到新连接的队列中。
术语: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++允许您为连接的生命周期中发生的特定事件注册钩子/回调/事件处理程序。在调用这些处理程序之一期间,库保证使用与当前运行的处理程序相关联的连接的
connection_ptr
是安全的。
术语:connection_hdl
由于
connection_ptr
的有限线程安全性,该库还提供了一种更灵活的连接标识符
connection_hdl
。
connection_hdl
的类型为
websocketpp::connection_hdl
,定义在<websocketpp/common/connection_hdl.hpp>中。请注意,与
connection_ptr
不同,
connection_hdl
不依赖于端点的类型或配置。仅仅存储或传输
connection_hdl
而不使用它们的代码可以只包含上述头文件,并且可以像处理值一样处理其hdl。
Connection handles不直接使用。它们由端点方法用于标识所需操作的目标。例如,发送新消息的端点方法将以参数的形式接受要发送消息的连接的hdl。
何时可以安全使用connection_hdl?
connection_hdl
可以在任何时间从任何线程使用。它们可以被复制并存储在容器中。删除hdl不会以任何方式影响连接。在处理程序调用期间,可以使用
endpoint::get_con_from_hdl()
将句柄升级为
connection_ptr
。在该处理程序调用的持续时间内,生成的
connection_ptr
是安全使用的。
connection_hdl
常见问题解答
在程序中,
connection_hdl
是保证唯一的。单个程序中的多个端点将始终创建具有唯一句柄的连接。
使用与创建其关联连接的不同端点的
connection_hdl
将导致未定义的行为。
使用已关闭或删除其关联连接的
connection_hdl
是安全的。端点将返回一个特定的错误,指示由于关联连接不存在,无法完成操作。
&emsp
;websocket_endpoint::connect()
函数首先使用作为参数传递的uri调用
endpoint::get_connection()
。此外,还传递了一个错误输出值来捕获可能发生的任何错误。如果确实发生了错误,则打印错误通知以及描述性消息,并将
-1 / 'invalid'
值作为新的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::exception
的
catch
块来捕获它。可以使用
websocketpp::exception::code()
方法从异常中提取机器可读的错误码值。
为了清楚地处理错误,utility_client 示例仅使用这些方法中不抛出异常的变体。您的应用程序可以选择使用其中之一。
如果连接创建成功,将生成下一个顺序连接 ID,并在该 ID 下将一个
connection_metadata
对象插入到连接列表中。最初,
metadata
对象存储连接 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;
术语:Registering handlers
WebSocket++提供了多个执行点,您可以在其中注册处理程序。您的端点可以使用的这些点将取决于其配置。例如,非TLS端点上将不存在TLS处理程序。可以在http://www.zaphoyd.com/websocketpp/manual/reference/handler-list找到处理程序的完整列表。
处理程序可以在端点级别和连接级别注册。端点处理程序在创建新连接时被复制到新连接中。更改端点处理程序将仅影响将来的连接。在连接级别注册的处理程序将仅绑定到该特定连接。
处理程序绑定方法的签名对于端点和连接是相同的。格式为:
set_*_handler(...)
。其中,*是处理程序的名称。例如,
set_open_handler
(…)将设置在打开新连接时调用的处理程序。
set_fail_handler(...)
将设置在连接失败时调用的处理程序。
所有处理程序都接受一个参数,可转换为具有正确数量和类型的参数的
std::function
的可调用类型。您可以传递具有匹配参数列表的自由函数、函数对象和Lambda作为处理程序。此外,您还可以使用
std::bind
(或
boost::bind
)来注册具有不匹配参数列表的函数。这对于传递在处理程序签名中不存在的其他参数或需要携带“this”指针的成员函数非常有用。
每个处理程序的函数签名可以在上面链接的手册中的列表中查找。一般来说,所有处理程序都包括作为第一个参数的
connection_hdl
,用于标识与此事件相关联的连接。某些处理程序(如消息处理程序)包括其他参数。大多数处理程序具有void返回值,但某些处理程序(
validate、ping、tls_init
)没有。返回值的具体含义在上面链接的处理程序列表中有文档记录。
utility_client注册了一个打开处理程序和一个失败处理程序。我们将使用这些处理程序来跟踪每个连接是否成功打开或失败。如果成功打开,我们将从握手过程中收集一些信息,并将其与我们的连接元数据一起存储。
在此示例中,我们将设置连接特定的处理程序,这些处理程序直接绑定到与我们的连接关联的元数据对象。这样可以避免在每个处理程序中执行查找以找到我们计划更新的元数据对象,这样更高效一些。
让我们详细查看被发送到
bind
的参数:
con->set_open_handler(websocketpp::lib::bind(
&connection_metadata::on_open,
metadata,
&m_endpoint,
websocketpp::lib::placeholders::_1
));
&
connection_metadata::on_open
是
connection_metadata
类的
on_open
成员函数的地址。
metadata_ptr
是指向与该类关联的connection_metadata对象的指针。它将被用作
on_open
成员函数将被调用的对象。&
m_endpoint
是正在使用的
endpoint
的地址。此参数将按原样传递给
on_open
方法。最后,
websocketpp::lib::placeholders::_1
是一个占位符,表示绑定函数应该接受一个额外的参数,在稍后的时间填充。WebSocket++将使用
connection_hdl
在调用处理程序时填充这个占位符。
最后,我们在配置好的连接请求上调用
endpoint::connect()
并返回新的连接ID。
处理程序成员函数
我们注册的打开处理程序
connection_metadata::on_open
将
status
元数据字段设置为”Open”,并从远程端点的HTTP响应中检索”Server”标头的值并将其存储在元数据对象中。服务器通常在此标头中设置一个标识字符串。
我们注册的失败处理程序
connection_metadata::on_fail
将
status
元数据字段设置为”Failed”,类似于on_open,还检索描述连接失败原因的错误代码。与该错误代码相关联的可读的消息保存在元数据对象中。
新的命令
设置了两个新的命令。”connect [uri]”将URI传递给websocket_endpoint connect方法,并报告错误或新连接的连接ID。”show [connection id]”将检索并打印与该连接关联的元数据。帮助文本已相应更新。
} 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;
}
}
Build
同step 3
Run
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: Open
> Remote Server: Kaazing Gateway
> Error/close reason: N/A
Enter Command: connect ws://wikipedia.org
> Created connection with id 1
Enter Command: show 1
> URI: ws://wikipedia.org
> Status: Failed
> Remote Server: Apache
> Error/close reason: Invalid HTTP status.
迄今为止的代码
#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.reset(new 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(new 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;
}
Step 5 关闭连接
此步骤添加了一个命令,允许您关闭WebSocket连接,并调整
quit
命令,以便在退出之前干净地关闭所有未完成的连接。
从WebSocket++中获取连接关闭信息
术语:WebSocket关闭代码和原因
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”表示出现了问题,导致连接在未执行关闭握手的情况下关闭。
添加关闭处理程序
添加了
connection_metadata::on_close
方法。此方法从关闭握手中检索关闭代码和原因,并将其存储在本地错误原因字段中。
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_open
和
on_fail
类似,当建立新连接时,
websocket_endpoint::connect
会注册此关闭处理程序。
将close方法添加到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;
}
}
在命令循环和帮助信息中添加关闭选项
已添加一个关闭选项到命令循环中。它接受一个连接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);
}
在websocket_endpoint的析构函数中关闭所有未完成的连接
直到现在,退出程序会使未完成的连接和WebSocket++网络线程陷入困境。现在我们有了关闭连接的方法,我们可以正确地清理这个问题。
websocket_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();
}
Build
同Step 4
Run
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
迄今为止的代码
#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;
}
Step 6 发送和接收消息
这一步添加了一个命令,用于在给定的连接上发送消息,并更新了
show
命令,以打印该连接的所有发送和接收的消息的传输记录。
术语:WebSocket消息类型(操作码)
WebSocket消息的类型由它们的操作码指示。目前的协议为数据消息指定了两个不同的操作码,即文本和二进制。文本消息表示UTF8文本,并将被验证为此。二进制消息表示原始二进制字节,并直接传递,不进行验证。
WebSocket++提供了
websocketpp::frame::opcode::text
和
websocketpp::frame::opcode::binary
的值,可以用于指示如何发送传出消息,并检查传入消息的格式。
发送消息
使用
endpoint::send
方法发送消息。这是一个线程安全的方法,可以从任何地方调用,以将消息排队发送到指定的连接。有三个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之后安全地修改。
术语:传出的WebSocket消息排队和流量控制
在许多配置中,例如当使用基于Asio的传输时,WebSocket++是一个异步系统。因此,endpoint::send方法可能在实际写入传出套接字之前返回。在快速连续调用send的情况下,消息可能会合并并在同一操作甚至同一TCP数据包中发送。当这种情况发生时,消息边界是保留的(每次调用send都会生成一个单独的消息)。
对于从处理程序内部调用send的应用程序而言,这意味着在处理程序返回之前不会将任何消息写入套接字。如果您计划以这种方式发送多个消息,或者需要在继续之前将消息写入网络,请考虑使用多个线程或内置的计时器/中断处理程序功能。
如果传出套接字连接速度较慢,消息可能会在此队列中积累。您可以使用
connection::get_buffered_amount
来查询已写入消息队列的当前大小,以决定是否要更改发送行为。
为websocket_endpoint添加send方法
与
close
方法类似,
send
方法将首先在连接列表中查找给定的连接ID。接下来,将使用指定的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);
}
在命令循环和帮助信息中添加发送选项
在命令循环中添加了发送选项。它接受连接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);
}
接收消息
通过注册消息处理程序来接收消息。该处理程序将在每次接收到消息时调用,其签名为
void on_message(websocketpp::connection_hdl hdl, endpoint::message_ptr msg)
。
connection_hdl
参数是对接收到消息的连接的句柄,与其他处理程序的类似参数一样。message_ptr是指向一个对象的指针,可以查询该对象的消息负载、操作码和其他元数据。请注意,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()));
}
}
为了在接收到新消息时调用此处理程序,我们还需要将其注册到我们的连接中。请注意,与大多数其他处理程序不同,消息处理程序有两个参数,因此需要两个占位符。
con->set_message_handler(websocketpp::lib::bind(
&connection_metadata::on_message,
metadata_ptr,
websocketpp::lib::placeholders::_1,
websocketpp::lib::placeholders::_2
));
Build
同Step 5
Run
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.4.0
> Error/close reason: N/A
> Messages Processed: (2)
>> example message
<< example message
迄今为止的代码
#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;
}