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;
}