I am using boost::beast as well as boost::asio to implement a websocket client with SSL support. My WebsocketClient class has the following members:
boost::asio::io_context& io_context;
boost::asio::ssl::context& ssl_context;
std::optional<tcp::resolver> resolver;
std::optional<websocket::stream<beast::ssl_stream<beast::tcp_stream>>> ws;
std::promise<void> promise_;
The io_context and ssl_context are constructed and passed by reference from main. The resolver and ws members are initialized via:
resolver.emplace(boost::asio::make_strand(io_context));
ws.emplace(boost::asio::make_strand(io_context), ssl_context);
After calling a WebsocketClient method "run" which triggers a sequence which calls connect, handshake, etc. we enter an async loop. Also of note, we set the value of promise before entering the "on_read" loop. This promise is returned to the caller of run in main which allows the application to progress once the initial WebsocketClient::run call is made.
void WebsocketClient::on_read(
beast::error_code ec,
std::size_t bytes_transferred) {
boost::ignore_unused(bytes_transferred);
if (ec) {
fail(ec, "read");
reconnect(ec);
} else {
// business logic
ws_->async_read(
buffer_,
beast::bind_front_handler(
&WebsocketClient::on_read,
this)
);
}
}
The WebsocketClient works well for several minutes until I reach an exception:
read: application data after close notify
Upon reaching this error, the WebsocketClient proceeds to:
void WebsocketClient::reconnect(beast::error_code ec) {
ws.emplace(boost::asio::make_strand(io_context), ssl_context);
promise_ = std::promise<void>();
resolver_.emplace(boost::asio::make_strand(io_context));
on_disconnect_cb(ec);
}
The function "on_disconnect_cb" is passed in during WebsocketClient initialization as a lambda function. This function simply calls the WebsocketClient::run function again in attempt to reconnect.
To summarize my questions:
Why am I receiving this "read: application data after close notify" error?
Why does the application fail to reconnect after calling WebsocketClient::reconnect?
In the process of debugging I have concluded that the application makes no additional progress after calling:
resolver->async_resolve(
host,
port,
beast::bind_front_handler(
&WebsocketClient::on_resolve,
this)
);
in the WebsocketClient::run function. This fails only in the case of a reconnect and run works as expected on first call. Therefore I expect the issue to be related to incorrectly resetting some of the components of the WebsocketClient.
Edit:
As requested in the comments, I'd like to provide a sketch of the "main" routine. This is indeed a sketch but should cover how the websocket is being used during the program lifecycle:
class App {
public:
App(boost::asio::io_context& io_context,
boost::asio::ssl::context& ssl_context) : {
ws.emplace(boost::asio::io_context& io_context,
boost::asio::ssl::context& ssl_context,
[this]() {
// on_read callback for ws
logic();
},
[this]() {
// on_disconnect callback for ws
on_disconnect();
}
);
}
void run() {
// call underlying websocket connect
}
void logic() {
// do stuff with inbound message
}
void on_disconnect() {
// destroy existing websocket
// If *this contains a value, destroy that value as if by value().T::~T()
ws.reset();
// Initialize new WebsocketClient
ws.emplace(io_context,
ssl_context,
[this](){
logic();
},
[this](){
on_disconnect();
};
);
// call App::run again to restart
run();
}
private:
boost::asio::io_context& io_context;
boost::asio::ssl::context& ssl_context;
std::optional<WebsocketClient> ws;
};
int main() {
// spinup io_context and detach thread that polls
App app(io_context, ssl_contex);
// triggers WebsocketClient::connect
app.run();
}
Related
I have been researching Boost.Asio and Boost.Beast and have some confusion around when explicit strand wrapping is needed with socket::async_* member function calls.
In Boost.Asio (1.78), there is a make_strand function. The examples provided with Boost.Beast show it being used like this:
server/chat-multi/listener.cpp
void
listener::
run()
{
// The new connection gets its own strand
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
}
//...
// Handle a connection
void
listener::
on_accept(beast::error_code ec, tcp::socket socket)
{
if(ec)
return fail(ec, "accept");
else
// Launch a new session for this connection
boost::make_shared<http_session>(std::move(socket), state_)->run();
// The new connection gets its own strand
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
}
server/chat-multi/http_session.cpp
void
http_session::
run()
{
do_read();
}
//...
void
http_session::
do_read()
{
// Construct a new parser for each message
parser_.emplace();
// Apply a reasonable limit to the allowed size
// of the body in bytes to prevent abuse.
parser_->body_limit(10000);
// Set the timeout.
stream_.expires_after(std::chrono::seconds(30));
// Read a request
http::async_read(
stream_,
buffer_,
parser_->get(),
beast::bind_front_handler(
&http_session::on_read,
shared_from_this()));
}
void
http_session::
on_read(beast::error_code ec, std::size_t)
{
// This means they closed the connection
if(ec == http::error::end_of_stream)
{
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
return;
}
// Handle the error, if any
if(ec)
return fail(ec, "read");
// See if it is a WebSocket Upgrade
if(websocket::is_upgrade(parser_->get()))
{
// Create a websocket session, transferring ownership
// of both the socket and the HTTP request.
boost::make_shared<websocket_session>(
stream_.release_socket(),
state_)->run(parser_->release());
return;
}
//...
}
server/chat-multi/websocket_session.cpp
void
websocket_session::
on_read(beast::error_code ec, std::size_t)
{
// Handle the error, if any
if(ec)
return fail(ec, "read");
// Send to all connections
state_->send(beast::buffers_to_string(buffer_.data()));
// Clear the buffer
buffer_.consume(buffer_.size());
// Read another message
ws_.async_read(
buffer_,
beast::bind_front_handler(
&websocket_session::on_read,
shared_from_this()));
}
In the same Boost.Beast example, subsequent calls on the socket's async_read member function are done without explicitly wrapping the work in a strand, either via post, dispatch (with socket::get_executor) or wrapping the completion handler with strand::wrap.
Based on the answer to this question, it seems that the make_strand function copies the executor into the socket object, and by default the socket object's completion handlers will be invoked on the same strand. Using socket::async_receive as an example, this to me says that there are two bits of work to be done:
A) The socket::async_receive I/O work itself
B) The work involved in calling the completion handler
My questions are:
According to the linked answer, when using make_strand B is guaranteed to be called on the same strand, but not A. Is this correct, or have I misunderstood something?
If 1) is correct, why does the server/chat-multi example provided above not explicitly wrap the async_read work on a strand?
In Michael Caisse's cppcon 2016 talk, "Asynchronous IO with Boost.Asio", he also does not explicitly wrap async_read_until operations in a strand. He explains that write calls should be synchronised with a strand, as they can in theory be called from any thread in the application. But read calls don't, as he is controlling them himself. How does this fit into the picture?
Thanks in advance
If an executor is not specified or bound, the "associated executor" is used.
For member async initiation functions the default executor is the one from the IO object. In your case it would be the socket which has been created "on" (with) the strand executor. In other words, socket.get_executor() already returns the strand<> executor.
Only when posting you would either need to specify the strand executor (or bind the handler to it, so it becomes the implicit default for the handler):
When must you pass io_context to boost::asio::spawn? (C++)
Why is boost::asio::io service designed to be used as a parameter?
Boost's official site socket::close() function, see the description
"This function causes all outstanding asynchronous connect, send and receive operations to finish immediately, and the handlers for cancelled operations will be passed the boost::asio::error::operation_aborted error."
But strangely, when i call chat_session::close(), socket::close() passing ERROR_CONNECTION_ABORTED (1236) error instead of boost::asio::error::operation_aborted (995).
Why is this happens?
here is my chat_session class.
class chat_session
: public boost::enable_shared_from_this<chat_session>
{
public:
chat_session(boost::asio::io_service& io, chat_server* room)
: m_sock(io), m_room(room)
{
}
~chat_session()
{
}
void start()
{
m_room.join(shared_from_this());
m_sock.async_read_some(
boost::asio::buffer(m_recv_data),
boost::bind(&chat_session::handle_read, shared_from_this(),
boost::asio::placeholders::error));
}
void close()
{
// closing socket. chat_session::handle_read will receive
// boost::asio::error::operation_aborted error.
m_sock.close();
}
boost::asio::ip::tcp::socket& socket()
{
return m_sock;
}
private:
void handle_read(const boost::system::error_code& error)
{
if (!error)
{
printf("RECV -> %s.\n", m_recv_data);
m_sock.async_read_some(
boost::asio::buffer(m_recv_data),
boost::bind(&chat_session::handle_read, shared_from_this(),
boost::asio::placeholders::error));
}
else
{
// when i call chat_session::close(),
// ERROR_CONNECTION_ABORTED (1236) error occurred
// instead of boost::asio::error::operation_aborted error over here
...
m_room.leave(shared_from_this());
}
}
boost::asio::ip::tcp::socket m_sock;
chat_room& m_room;
char m_recv_data[50];
};
Try calling shutdown on the socket first before you close it, as the boost basic_stream_socket::close documentation specifies in the Remarks here:
Remarks
For portable behaviour with respect to graceful closure of a connected socket, call shutdown() before closing the socket.
Try something like the following in your close function:
m_sock.shutdown(boost::asio::ip::tcp::socket::shutdown_receive);
m_sock.close();
If you want to shut down both send and receive, use "shutdown_both" instead of "shutdown_receive".
Funny enough, I've seen this error happen on Windows but not Linux when using an implementation without the call to shutdown.
I am implementing something looks like a HTTP server, the design is: for a already established connection, I want to reuse it for several requests, so I start another reading task with async_read on it when a request is finished, and also start a deadline_timer. If there is no input in 60 seconds, the timer will be triggered and the connection will be destructed. The thing that annoys me is that before the invocation of the connection's destructor, the callback we set to async_read will be invoked.
So, my question is, is there any way to cancel the pending reading task, that is, destruct the connection without the callback function invoked?
If the generic description above is not clear, the detail work flow is as below(code is attached at the bottom):
cleanup() is called when a request finished;
start the timer and another reading task in cleanup();
if time is out, HandleTimeout() is called, and it calls stop();
in stop(), do the clean work, and after it, the connection instance will be destructed.
but, after step 4, the callback() function will be called, which is registered in AsyncRead(), so, is there any way to cancel the invocation of callback()?
code:
class Connection : public boost::enable_shared_from_this<Connection>,
private boost::noncopyable {
public:
typedef Connection this_type;
void cleanup() {
timer_.expires_from_now(boost::posix_time::seconds(kDefaultTimeout));
timer_.async_wait(boost::bind(&this_type::HandleTimeout,
shared_from_this(),
boost::asio::placeholders::error));
AsyncRead();
}
void AsyncRead() {
boost::asio::async_read(*socket_, in_, boost::asio::transfer_at_least(1),
boost::bind(&this_type::callback,
shared_from_this(),
boost::asio::placeholders::error));
}
void callback(const boost::system::error_code& e) {
// ...
}
void HandleTimeout(const boost::system::error_code& e) {
if(e == boost::asio::error::operation_aborted)
LDEBUG << "The timeout timer is cancelled.";
else if(e)
LERROR << "Error occurred with the timer, message: " << e.message();
else if(timer_.expires_at()
<= boost::asio::deadline_timer::traits_type::now()) {
LDEBUG << "Connection timed out, close it.";
stop();
}
}
virtual void stop() {
connected_ = false;
socket_->close();
connection_manager_.stop(shared_from_this());
}
private:
// ...
boost::asio::deadline_timer timer_;
};
There is no clean way to accomplish this. The only way to guarantee that ready-to-run handlers, such as Connection::callback(), will not be invoked is to either:
Stop processing the io_service event loop.
Destroy the io_service, as the io_service's destructor will cause all outstanding handlers to be destroyed.
In the example code, consider returning in Connection::callback() if the socket is no longer open:
void callback(const boost::system::error_code& error)
{
if (!socket_.is_open()) return;
// ...
}
Also note that the error_code argument is not enough to deduce whether the timeout has occurred. It is possible that Connection::callback() is queued for invocation with an error_code of boost::system::errc::success when socket::close() is invoked. Hence, there are no operations to cancel.
I am writing a DLL plugin for the Orbiter space simulator, which allows for UDP communication with an external system. I've chosen boost::asio for the task, as it allows me to abstract from the low-level stuff.
The "boundary conditions" are as follows:
I can create any threads or call any API functions from my DLL
I can modify the data inside of the simulation only inside the callback passed to my DLL (each frame), due to lack of other thread safety.
Hence, I chose the following architecture for the NetworkClient class I'm using for communications:
Upon construction, it initializes the UDP socket (boost::socket+boost::io_service) and starts a thread, which calls io_service.run()
Incoming messages are put asyncronously into a queue (thread-safe via CriticalSection)
The callback processing function can pull the messages from queue and process it
However, I have run into some strange exception upon running the implementation:
boost::exception_detail::clone_impl > at memory location 0x01ABFA00.
The exception arises in io_service.run() call.
Can anyone point me, please, am I missing something? The code listings for my classes are below.
NetworkClient declaration:
class NetworkClient {
public:
NetworkClient(udp::endpoint server_endpoint);
~NetworkClient();
void Send(shared_ptr<NetworkMessage> message);
inline bool HasMessages() {return incomingMessages.HasMessages();};
inline shared_ptr<NetworkMessage> GetQueuedMessage() {return incomingMessages.GetQueuedMessage();};
private:
// Network send/receive stuff
boost::asio::io_service io_service;
udp::socket socket;
udp::endpoint server_endpoint;
udp::endpoint remote_endpoint;
boost::array<char, NetworkBufferSize> recv_buffer;
// Queue for incoming messages
NetworkMessageQueue incomingMessages;
void start_receive();
void handle_receive(const boost::system::error_code& error, std::size_t bytes_transferred);
void handle_send(boost::shared_ptr<std::string> /*message*/, const boost::system::error_code& /*error*/, std::size_t /*bytes_transferred*/) {}
void run_service();
NetworkClient(NetworkClient&); // block default copy constructor
};
Methods implementation:
NetworkClient::NetworkClient(udp::endpoint server_endpoint) : socket(io_service, udp::endpoint(udp::v4(), 28465)) {
this->server_endpoint = server_endpoint;
boost::thread* th = new boost::thread(boost::bind(&NetworkClient::run_service,this));
start_receive();
}
void NetworkClient::start_receive()
{
socket.async_receive_from(boost::asio::buffer(recv_buffer), remote_endpoint,
boost::bind(&NetworkClient::handle_receive, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)
);
}
void NetworkClient::run_service()
{
this->io_service.run();
}
There's nothing wrong with your architecture that I can see. You should catch exceptions thrown from io_service::run(), that is likely the source of your problem.
void NetworkClient::run_service()
{
while(1) {
try {
this->io_service.run();
} catch( const std::exception& e ) {
std::cerr << e.what << std::endl;
}
}
}
You'll also want to fix whatever is throwing the exception.
I have some software that I would like to make a TCP client. I don't know if this is the best architecture, but in my software I spawn a thread that will be used for the Network I/O. If there is a better architecture, I'd appreciate some pointers and advice.
Both threads have a refernce to the boost::asio::io_service object and a Session object that encapsulates the socket object. The sesson object is roughly as follows:
class Session
{
public:
Session(
boost::asio::io_service & io_service,
std::string const & ip_address,
std::string const & port)
: io_service_(io_service),
resolver_(io_service),
socket_(io_service),
ip_address_(ip_address),
port_(port),
{}
virtual void start();
virtual ~Session();
virtual void stop();
void write(std::string const & msg);
void handle_resolve(
const boost::system::error_code & error,
boost::asio::ip::tcp::resolver::iterator endpoint_itr);
void handle_connect(
const boost::system::error_code & error,
boost::asio::ip::tcp::resolver::iterator endpoint_itr);
void handle_close();
void handle_write(const boost::system::error_code & error);
private:
boost::asio::io_service & io_service_;
boost::asio::ip::tcp::resolver resolver_;
boost::asio::ip::tcp::socket socket_;
std::string ip_address_;
std::string port_;
};
In the I/O thread run-loop, the start() method of the session object is called which connects to the server. (This works, btw). Then, the thread sits in a loop calling the run() method on the I/O service object [io_service_.run()] to trigger events.
The main thread calls the write() method of the session when it wants to send data, and the session object calls boost::async_write with the data to write and then a callback method that is a member of the session object (handle_write).
While I have the I/O thread connecting to the server, I cannot get the handle_write method to be triggered. I have verified that the main thread is calling into the session object and executing async_write() on the socket. It is just that the callback is never triggered. I also don't see any data on the server side or over the wire with tcpdump.
Any idea where my problem might be? Is there a better way to organize the architecture? Most of all, I don't want to block the main thread doing I/O.
Here is the code that spawns the io thread from the main thread (apologies for the spacing):
boost::asio::io_service io_service;
boost::shared_ptr<Session> session_ptr;
boost::thread io_thread;
....
session_ptr.reset(
new Session::Session(
io_service,
std::string("127.0.0.1"),
std::string("17001")));
// spawn new thread for the network I/O endpoint
io_thread = boost::thread(
boost::bind(
&Session::start,
session_ptr_.get()));
The code for the start() method is as follows:
void Session::start()
{
typedef boost::asio::ip::tcp tcp;
tcp::resolver::query query(
tcp::v4(),
ip_address_,
port_);
resolver_.async_resolve(
query,
boost::bind(
&Session::handle_resolve,
this,
boost::asio::placeholders::error,
boost::asio::placeholders::iterator));
while(1){ // improve this later
io_service_.run();
}
}
The callback for the resolver:
void Session::handle_resolve(
const boost::system::error_code & error,
boost::asio::ip::tcp::resolver::iterator endpoint_itr)
{
if (!error)
{
boost::asio::ip::tcp::endpoint endpoint = *endpoint_itr;
socket_.async_connect(
endpoint,
boost::bind(
&Session::handle_connect,
this,
boost::asio::placeholders::error,
++endpoint_itr));
}
else
{
std::cerr << "Failed to resolve\n";
std::cerr << "Error: " << error.message() << std::endl;
}
}
The callback for connect:
void Session::handle_connect(
const boost::system::error_code & error,
boost::asio::ip::tcp::resolver::iterator endpoint_itr)
{
typedef boost::asio::ip::tcp tcp;
if (!error)
{
std::cerr << "Connected to the server!\n";
}
else if (endpoint_itr != tcp::resolver::iterator())
{
socket_.close();
socket_.async_connect(
*endpoint_itr,
boost::bind(
&Session::handle_connect,
this,
boost::asio::placeholders::error,
++endpoint_itr));
}
else
{
std::cerr << "Failed to connect\n";
}
}
The write() method that the main thread can call to send post an asychronous write.
void Session::write(
std::string const & msg)
{
std::cout << "Write: " << msg << std::endl;
boost::asio::async_write(
socket_,
boost::asio::buffer(
msg.c_str(),
msg.length()),
boost::bind(
&Session::handle_write,
this,
boost::asio::placeholders::error));
}
And finally, the write completion callback:
void Session::handle_write(
const boost::system::error_code & error)
{
if (error)
{
std::cout << "Write complete with errors !!!\n";
}
else
{
std::cout << "Write complete with no errors\n";
}
}
Looks like your io service will run out of work after connect, after which you just call io_service::run again? It looks like run is being called in the while loop, however I can't see a call to reset anywhere. You need to call io::service::reset before you call run on the same io_service again.
Structurally, it would be better to add work to the io_service, then you don't need to call it in the loop and the run will exit once you call io_service::stop.
this portion of your code
boost::asio::io_service io_service;
boost::shared_ptr<Session> session_ptr;
boost::thread io_thread;
....
session_ptr.reset(
new Session::Session(
io_service,
std::string("127.0.0.1"),
std::string("17001")));
// spawn new thread for the network I/O endpoint
io_thread = boost::thread(
boost::bind(
&Session::start,
session_ptr_.get()));
is a red flag to me. Your io_service object is possibly going out of scope and causing strange behavior. An io_service is not copyable, so passing it to your Session as a non-const reference is probably not what you are hoping to achieve.
samm#macmini ~> grep -C 2 noncopyable /usr/include/boost/asio/io_service.hpp
#include <boost/asio/detail/epoll_reactor_fwd.hpp>
#include <boost/asio/detail/kqueue_reactor_fwd.hpp>
#include <boost/asio/detail/noncopyable.hpp>
#include <boost/asio/detail/select_reactor_fwd.hpp>
#include <boost/asio/detail/service_registry_fwd.hpp>
--
*/
class io_service
: private noncopyable
{
private:
--
/// Class used to uniquely identify a service.
class io_service::id
: private noncopyable
{
public:
--
/// Base class for all io_service services.
class io_service::service
: private noncopyable
{
public:
If you're basing your code off the HTTP client example, you should note the io_service is in scope all the time inside of main(). As Ralf pointed out, your io_service is also likely running out of work to do after the connect handler, which is why you've kludged it to invoke run() inside of a loop
while(1){ // improve this later
io_service_.run();
}
again, note that the HTTP client example does not do this. You need to start another async operation inside of the connect handler, either a read or write depending on what your application needs.