websocket计时器滴答后提升asio短读错误

Posted

技术标签:

【中文标题】websocket计时器滴答后提升asio短读错误【英文标题】:Boost asio short read error after websocket timer tick 【发布时间】:2019-12-05 19:19:53 【问题描述】:

我正在尝试使用 websocketpp 连接到安全的 websocket,但在计时器的第二个滴答声后出现了这个奇怪的错误:

[2019-12-05 10:48:55] [info] asio async_read_at_least error: asio.ssl:335544539 (short read)
[2019-12-05 10:48:55] [error] handle_read_frame error: websocketpp.transport:11 (Generic TLS related error)
[2019-12-05 10:48:55] [info] asio async_write error: asio.ssl:336396495 (protocol is shutdown)
[2019-12-05 10:48:55] [fatal] handle_write_frame error: websocketpp.transport:2 (Underlying Transport Error)
[2019-12-05 10:48:55] [info] asio async_shutdown error: asio.ssl:335544539 (short read)
close handler: Underlying Transport Error
[2019-12-05 10:48:55] [disconnect] Disconnect close local:[1006,Underlying Transport Error] remote:[1000]

我的代码是:

#define _WEBSOCKETPP_CPP11_STL_
#   ifdef _WIN32
#       pragma warning(disable: 4503)
#       pragma warning(disable: 4996)
#   endif
#   include <websocketpp/config/asio_client.hpp>
#   include <websocketpp/client.hpp>
#   include <websocketpp/frame.hpp>
#undef _WEBSOCKETPP_CPP11_STL_

#include <iostream>

using WSClient = websocketpp::client<websocketpp::config::asio_tls_client>;

int main()

    WSClient client;

    client.init_asio();

    client.set_tls_init_handler([](auto)
    
        auto result = websocketpp::lib::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::sslv23_client);

        result->set_verify_mode(boost::asio::ssl::verify_none);

        return result;
    );

    client.set_open_handler([&client](auto hdl)
    
        client.set_timer(40000, [hdl, &client](auto)
        
            if (auto con = client.get_con_from_hdl(hdl)) 
                con->send(std::string(R"("op":1,"d":1)"), websocketpp::frame::opcode::text);
            
        );
    );

    client.set_close_handler([&client](auto hdl)
    
        auto con = client.get_con_from_hdl(hdl);
        std::cout << "close handler: " << con->get_ec().message() << std::endl;
    );

    client.set_fail_handler([&client](auto hdl)
    
        auto con = client.get_con_from_hdl(hdl);
        std::cout << "fail handler: " << con->get_ec().message() << std::endl;
    );

    websocketpp::lib::error_code ec;

    const auto websocketUrl = "wss://gateway.discord.gg/?encoding=json&v=6";

    auto con = client.get_connection(websocketUrl, ec);

    if (ec) 
        std::cout << "Could not create WebSocket connection because " << ec.message() << std::endl;
        return 0;
    

    client.connect(con);
    client.run();

令人惊讶的是,如果我使用 NodeJS 做同样的事情,它可以正常使用:

const WebSocket = require('ws');

let websocketUrl = 'wss://gateway.discord.gg/?encoding=json&v=6'

const ws = new WebSocket(websocketUrl);

ws.on('open', function() 
    setInterval(() => 
        console.log('ping');

        ws.send('"op":1,"d":1');
    , 40000);
);

ws.on('error', function(e, v)
    console.log('error', e, v);
);

ws.on('unexpected-response', function(e, t, v)
    console.log('unexpected-response', e, t);
);

ws.on('close', function() 
    console.log('connection closed');
);

我在 C++ 版本中做错了什么?

环境:Windows 10、MSVC 14、Websocketpp 0.8.1、Boost 1.69

【问题讨论】:

【参考方案1】:

根据文档 [1],客户端必须发送 ping(手册称它们为心跳):

心跳:

用于维持活动的网关连接。必须在收到 Opcode 10 Hello 有效负载后每隔 heartbeat_interval 毫秒发送一次。内部的 d 键是客户端收到的最后一个序列号 s。如果您还没有收到,请发送 null。

您正在使用 WSClient::set_timer()(在 set_open 方法中)在 C++ 实现中发送 ping 消息。但是,WSClient::set_timer() 只调用一次 ping 函数(您可以使用 printf 检查或阅读该方法的文档)。因此,您只发送一条 ping 消息。因此,您的连接会在服务器一段时间后被终止。

相比之下,您在 NodeJS 实现中使用“setIntervall()”来设置周期性计时器。因此,此计时器会定期调用,并且服务器会定期接收您的 ping 消息。

我做了以下修复您的 C++ 代码[答案末尾复制粘贴的完整代码]:

1.) 添加一个处理程序来读取传入消息以进行调试:

client.set_message_handler([&client](auto hdl, auto msg_ptr)

    std::string message = msg_ptr->get_payload();
    std::cout << "received message: " << message << std::endl;
);

2.) 以非阻塞方式启动 websocket:

auto run_thread = std::thread[&client]()client.run();; 
while(! client_is_open)  //this variable is defined elsewhere, see full code
    sleep(1); //TODO: use an mutex instead

3.) 执行 ping 操作:

int heartbeat_interval = 41250; //in ms: TODO extract from message
while(true) 
     std::this_thread::sleep_for(std::chrono::milliseconds(heartbeat_interval));
     client.send(con, "\"op\":1, \"d\":null", websocketpp::frame::opcode::text);

请注意以下几点:

    您必须从第一条消息中提取 ping 间隔。 (我没有) ping 消息应该发送最后一个 ping 的序列号(请参阅相关文档)。我没有,它仍然有效,但不要指望它。 client.run() 是一个阻塞调用,有一些问题。

你可以在下面找到我的代码

[1]https://github.com/discordapp/discord-api-docs/blob/master/docs/topics/Gateway.md#DOCS_TOPICS_GATEWAY/heartbeat

#define _WEBSOCKETPP_CPP11_STL_
#   ifdef _WIN32
#       pragma warning(disable: 4503)
#       pragma warning(disable: 4996)
#   endif
#   define _WEBSOCKETPP_CPP11_STL_
#   include <websocketpp/config/asio_client.hpp>
#   include <websocketpp/client.hpp>
#   include <websocketpp/frame.hpp>
#undef _WEBSOCKETPP_CPP11_STL_

#include <iostream>

using WSClient = websocketpp::client<websocketpp::config::asio_tls_client>;;

int main()  WSClient client;

client.init_asio();
bool client_is_open = false;

client.set_tls_init_handler([](auto)

    auto result = websocketpp::lib::make_shared<boost::asio::ssl::context>(boost::asio::ssl::context::sslv23_client);

    result->set_verify_mode(boost::asio::ssl::verify_none);

    return result;
);

client.set_open_handler([&client,&client_is_open](auto hdl)

    client_is_open=true;
);

client.set_close_handler([&client](auto hdl)

    auto con = client.get_con_from_hdl(hdl);
    std::cout << "close handler: " << con->get_ec().message() << std::endl;
);

client.set_fail_handler([&client](auto hdl)

    auto con = client.get_con_from_hdl(hdl);
    std::cout << "fail handler: " << con->get_ec().message() << std::endl;
);

client.set_message_handler([&client](auto hdl, auto msg_ptr)

    std::string message = msg_ptr->get_payload();
    std::cout << "received message: " << message << std::endl;
);

websocketpp::lib::error_code ec;

const auto websocketUrl = "wss://gateway.discord.gg/?encoding=json&v=6";

auto con = client.get_connection(websocketUrl, ec);

if (ec) 
    std::cout << "Could not create WebSocket connection because " << ec.message() << std::endl;
    return 0;


client.connect(con);
auto run_thread = std::thread[&client]()client.run();;
while(! client_is_open) 
    sleep(1); //TODO: use an mutex instead

int heartbeat_interval = 41250; //in ms: TODO extract from message
while(true) 
    std::this_thread::sleep_for(std::chrono::milliseconds(heartbeat_interval));
    client.send(con, "\"op\":1, \"d\":null", websocketpp::frame::opcode::text);
 
run_thread.join();


【讨论】:

以上是关于websocket计时器滴答后提升asio短读错误的主要内容,如果未能解决你的问题,请参考以下文章

如何在多个线程中安全地使用提升截止时间计时器?

STM32怎么用库函数使用滴答定时器?

Boost ASIO TCP计时器客户端错误“解决:找不到指定的类”

提升 asio 绑定:错误的文件描述符

在发布模式下提升 ASIO 段错误

捕获倒计时或滴答事件 - Threading.Timer C#