Note!!! The question is for people who are experts in boost::asio library
. Unfortunately, I cannot do the code more compact, It contains a minimum amount to describe the problem. The code is example, artificially created. Places where it crashes known and described in comments, it is designed to illustrate the crashes!!! NO need
any help with debugging of the code...
The question is about how to design the asio server, not about - where it crashes!!!
This example is close to "chat server" design from official boost::asio documentation. But, unlike the official example, where only objects of the connection class are created/destroyed dynamically, in my example, both, server and its connection class entities are created/destroyed dynamically... I am sure that the implementation of such pattern should be well known among the asio lovers and the problem described below should already be solved by somebody...
Please see the code. Here, the entities of CAsioServer and CAsioConnection are created and destroyed on fly.
#include <map>
#include <array>
#include <set>
#include <vector>
#include <deque>
#include <thread>
#include <iostream>
#include <asio.hpp>
#include <iomanip>
class CAsioConnection
: public std::enable_shared_from_this<CAsioConnection>
{
public:
using PtrType = std::shared_ptr<CAsioConnection>;
CAsioConnection(asio::ip::tcp::socket socket, std::set<CAsioConnection::PtrType>& connections)
: socket_(std::move(socket)), connections_(connections)
{
std::cout << "-- CAsioConnection is creating, socket: " << socket_.native_handle() << "\n";
}
virtual ~CAsioConnection()
{
std::cout << "-- CAsioConnection is destroying , socket: " << socket_.native_handle() << "\n";
}
void read() { do_read(); }
private:
void do_read(void)
{
uint8_t buff[3];
asio::async_read(socket_, asio::buffer(buff,3),
[this](std::error_code ec, std::size_t /*length*/) {
if (!ec)
{
do_read();
}
else
{
std::cout << "-- CAsioConnection::do_read() error : " << ec.message() << "\n";
// Here is the crash N2
connections_.erase(shared_from_this());
// Crash may be fixed by the code below
//if (ec.value() != 1236) // (winerror.h) #define ERROR_CONNECTION_ABORTED 1236L
// connections_.erase(shared_from_this());
}
});
}
asio::ip::tcp::socket socket_;
std::set<CAsioConnection::PtrType>& connections_;
};
class CAsioServer
: public std::enable_shared_from_this<CAsioServer>
{
public:
using PtrType = std::shared_ptr<CAsioServer>;
CAsioServer(int port, asio::io_context& io, const asio::ip::tcp::endpoint& endpoint)
: port_(port), acceptor_(io, endpoint)
{
std::cout << "-- CAsioServer is creating, port: " << port_ << "\n";
}
virtual ~CAsioServer()
{
std::cout << "-- CAsioServer is destroying , port: " << port_ << "\n";
}
int port(void) { return port_; }
void accept(void) { do_accept(); }
private:
void do_accept()
{
acceptor_.async_accept([this](std::error_code ec, asio::ip::tcp::socket socket) {
if (!ec)
{
std::cout << "-- CAsioServer::do_accept() connection to socket: " << socket.native_handle() << "\n";
auto c = std::make_shared<CAsioConnection>(std::move(socket), connections_);
connections_.insert(c);
c->read();
}
else
{
// Here is the crash N1
std::cout << "-- CAsioServer::do_accept() error : " << ec.message() << "\n";
// Crash may be fixed by the code below
//if (ec.value() == 995) // (winerror.h) #define ERROR_OPERATION_ABORTED 995L
// return;
}
// Actually here is the crash N1 )), but the fix is above...
do_accept();
});
}
int port_;
asio::ip::tcp::acceptor acceptor_;
std::set<CAsioConnection::PtrType> connections_;
};
//*****************************************************************************
class CTcpBase
{
public:
CTcpBase()
{
// heart beat timer to keep it alive
do_heart_beat();
t_ = std::thread([this] {
std::cout << "-- io context is RUNNING!!!\n";
io_.run();
std::cout << "-- io context has been STOPED!!!\n";
});
}
virtual ~CTcpBase()
{
io_.stop();
if (t_.joinable())
t_.join();
}
void add_server(int port)
{
io_.post([this, port]
{
for (auto s : servers_)
if (port == s->port())
return;
auto endpoint = asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port);
auto s = std::make_shared<CAsioServer>(port, io_, endpoint);
s->accept();
servers_.insert(s);
});
}
void remove_server(int port)
{
io_.post([this, port]
{
for (auto s : servers_)
if (port == s->port())
{ servers_.erase(s); return; }
});
}
private:
void do_heart_beat(void)
{
std::cout << "-- beat\n";
auto timer = std::make_shared<asio::steady_timer>(io_, asio::chrono::milliseconds(3000));
timer->async_wait([timer, this](const asio::error_code& ec) {
do_heart_beat();
});
}
asio::io_context io_;
std::thread t_;
std::set<CAsioServer::PtrType> servers_;
};
//*****************************************************************************
int main(void)
{
CTcpBase tcp_base;
std::cout << "CONNECT the server to port 502\n";
tcp_base.add_server(502);
std::this_thread::sleep_for(std::chrono::seconds(20));
std::cout << "REMOVE the server from port 502\n";
tcp_base.remove_server(502);
std::this_thread::sleep_for(std::chrono::seconds(10));
return 0;
}
It supposed that CTcpBase::add_server()
and CTcpBase::remove_server()
will be called by outer clients from different threads. And asio context handles it in its own thread.
Let’s consider the two scenarios:
- Start application and wait a half of minute.
The crash happens in
CAsioServer::do_accept()
see the output below. Debug Console Output - Start application. Make connection to the port 502 by any outer client and wait less than 20 seconds.
The crash happens in
CAsioConnection::do_read()
see the output below. Debug Console Output
It seems asio framework calls postponed asio::async_read()
and acceptor_.async_accept()
handlers when its class' entities already destroyed.
I have fixed the handlers with error checking, but the solution doesn't seem to be reliable. Who knows what other errors and scenarios there might be… Sometimes, when client disconnects, I need to clean the connection_
set at asio::async_read()
, how can I be sure that server or connection objects are still alive?…
Is any way to ask boost::asio framework to prevent calling the postponed handlers for objects that are already destroyed? Or how to recognize (be 100% sure)
by the error code that the object has already been destroyed? Or my be there are other solutions or design patterns in the scope of asio - how to handle dinamically created/destroyed servers and its connections in one running thread without mutexes and stuff...