2
\$\begingroup\$

Here's a proposal implementation of websocket using boost::asio::beast that is thread-safe to parallel writes.

In this example below, the async_write can be triggered in response to server notification (I) or from periodic keepalive calls implemented on a dedicated thread (II). I'd like to here your comments regarding my implementation and especially to the use of strand in order to serialize the async_writes and make them thread-safe.

Thanks !


#pragma once

#include <memory>
#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/http.hpp>
#include <boost/asio/spawn.hpp>

#include <boost/asio/io_service.hpp>

#include <functional>
#include <iostream>
#include <string>
#include <mutex>
#include <thread>

#include <cstdlib>

class ServerCommunication {
public:
    explicit ServerCommunication(const char *host, const char *port);

    void Stop();
    void Run(std::vector<std::thread> &v);
    void Send(std::string data);

private:
    void ConnectAndListen(const boost::asio::yield_context &yield);
    void SenderService(const boost::asio::yield_context &yield);

    bool is_connected_ = false;
    std::string ip_;
    std::string port_;
    boost::asio::io_context io_context_;
    
    void on_write(boost::system::error_code ec, std::size_t bytes_transferred);

    boost::asio::strand<boost::asio::io_context::executor_type> strand_;


    boost::beast::websocket::stream<boost::beast::tcp_stream> ws_;
    boost::beast::error_code ec_;
    
    void fail(char const* op) { std::cerr << op << ": " << ec_.message() << "\n"; }
    
};


ServerCommunication::ServerCommunication(const char *ip, const char *port)
    : ip_(ip), port_(port) , ws_(io_context_) , strand_(boost::asio::make_strand(io_context_)) { }

void ServerCommunication::Stop() {
    boost::asio::spawn(
        io_context_,
        [this](const boost::asio::yield_context &yield) {
        ws_.async_close(boost::beast::websocket::close_code::normal, yield[ec_]);

        if(ec_) {
            return fail("close");
        }
    });
    
    io_context_.stop();
}

void ServerCommunication::ConnectAndListen(const boost::asio::yield_context &yield) {

    boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30));
    
    boost::asio::ip::tcp::resolver resolver(io_context_);

    auto const results = resolver.async_resolve(ip_, port_, yield[ec_]);

    auto ep = boost::beast::get_lowest_layer(ws_).async_connect(results, yield[ec_]);
    if(ec_) return fail("connect");

    // Update the host_ string. This will provide the value of the
    // Host HTTP header during the WebSocket handshake.
    // See https://tools.ietf.org/html/rfc7230#section-5.4
    auto address = ip_ + std::to_string(ep.port());

    // Turn off the timeout on the tcp_stream, because
    // the websocket stream has its own timeout system.
    boost::beast::get_lowest_layer(ws_).expires_never();

    // Set suggested timeout settings for the websocket
    ws_.set_option(
        boost::beast::websocket::stream_base::timeout::suggested(
            boost::beast::role_type::client));

    // Set a decorator to change the User-Agent of the handshake
    ws_.set_option(boost::beast::websocket::stream_base::decorator(
        [](boost::beast::websocket::request_type& req)
        {
            req.set(boost::beast::http::field::user_agent,
                std::string(BOOST_BEAST_VERSION_STRING) +
                    " websocket-client-me");
        }));

    // Perform the websocket handshake
    ws_.async_handshake(address, "/ws", yield[ec_]);
    if(ec_) return fail("handshake");

    is_connected_ = true;
    for (;;) {
        boost::beast::flat_buffer buffer;

        // Read a message into our buffer
        ws_.async_read(buffer, yield[ec_]);
        if(ec_) return fail("read");

        std::cout << boost::beast::make_printable(buffer.data()) << std::endl;

        //### The first async_write is triggered from incoming server data (I).
        Send("Ack");

    }
}

void ServerCommunication::Run(std::vector<std::thread> &v) {
    boost::asio::spawn(io_context_,
                       [this](const boost::asio::yield_context &yield) {
        this->ConnectAndListen(yield);
    });
    
    v.emplace_back([this] {
        this->io_context_.run();
        });
}

void ServerCommunication::on_write(
    boost::system::error_code ec,
    std::size_t bytes_transferred)
{
}

void ServerCommunication::Send(std::string data) {
    ws_.async_write(boost::asio::buffer(data),
                    boost::asio::bind_executor(strand_, [this] (boost::system::error_code ec,
                                                                std::size_t bytes_transferred)
                                    { this->on_write(ec, bytes_transferred);}));
    
    if(ec_) { fail("write"); }
}



int main() {    
    std::vector<std::thread> v;
    v.reserve(1);

    // Connect to local server that listen to arbitrary port 44444
    ServerCommunication serverCommunication_("0.0.0.0" , "44444");
    serverCommunication_.Run(v);
    

    sleep(5);
    std::thread t([&] () {
        for (;;) {
            sleep(1);
            // ### this will simulate the on-demand async_writes (II).
            serverCommunication_.Send("ZZZZZ");
        }
    });
    
    t.join();
    return 0;
}
\$\endgroup\$
0

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.