I have one active async operation, it is http::async_read
. At the moment when I want to close the ssl connection, I cancel this async operation on the socket by calling ip::tcp::socket::cancel
.
Then I terminate the ssl connection like this
socket_wrapper.cancel(); // cancel http::async_read
boost::asio::post(socket_wrapper.get_stream().get_executor(), [this, self]
{
socket_wrapper.get_stream().async_shutdown([this, self](const boost::system::error_code& ec)
{
socket_wrapper.close();
});
});
I call boost::asio::post
to make sure the completion handler for the cancelled http::async_read
was called. After that I call async_shutdown
. The async_shutdown
operation completes successfully in most cases.
However, sometimes I get an exception: "Exception thrown: read access violation. s was nullptr." and the debugger points to the following code from the boost::asio
sources:
int engine::do_shutdown(void*, std::size_t)
{
int result = ::SSL_shutdown(ssl_);
if (result == 0)
result = ::SSL_shutdown(ssl_);
return result;
}
What could be the reason?
I call boost::asio::post to make sure the completion handler for the cancelled http::async_read was called
Sadly, that doesn't work. Post doesn't ensure completion happens in a specific order. It may or may not (if you very diligently control threads and poll()
on the context you might be able to get it right, but I wouldn't risk it and your code doesn't mention it, so I'm assuming you didn't do anything to orchestrate order of handlers).
You need specific synchronization before you even post the shutdown.
Here's completely deconstructed example, knowing that you do not want to rely on thread synchronization primitives (good idea, see below):
#define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <iostream>
using namespace std::literals;
namespace asio = boost::asio;
namespace ssl = asio::ssl;
using asio::ip::tcp;
using boost::system::error_code;
static std::atomic_bool s_write_completed{false};
static std::atomic_bool s_read_completed{false};
static void read_handler(error_code ec, size_t) {
s_read_completed = true;
std::cout << "Read completion: " << ec.message() << std::endl;
}
static void write_handler(error_code ec, size_t) {
s_write_completed = true;
std::cout << "Write completion: " << ec.message() << std::endl;
}
static void shutdown_handler(error_code ec) { //
std::cout << "Shutdown completion: " << ec.message() << std::endl;
}
int main() {
asio::io_context ioc(1); // single thread
ssl::context ctx(ssl::context::sslv23);
ssl::stream<tcp::socket> s(ioc, ctx);
s.lowest_layer().connect({{}, 8989});
s.handshake(ssl::stream_base::client); // synchronous for simplicity
// deferred ops
asio::cancellation_signal signal;
s.async_write_some(asio::buffer("Hello, world!\n"sv),
bind_cancellation_slot(signal.slot(), write_handler));
std::array<char, 1024> buffer;
s.async_read_some(asio::buffer(buffer),
bind_cancellation_slot(signal.slot(), read_handler));
// std::thread io(io_worker, std::ref(ioc));
asio::steady_timer tim2(ioc, 50ms);
tim2.async_wait([&](error_code) { //
signal.emit(asio::cancellation_type::all);
});
// process all pending operations
while (ioc.run_one())
std::cout << "Progress: " << s_read_completed << ", " << s_write_completed << std::endl;
assert(s_read_completed); // read operation should have completed
assert(s_write_completed); // write operation should have completed
s.async_shutdown(shutdown_handler);
ioc.restart();
ioc.run(); // run to completion
}
Which will print as standard output:
Write completion: Success
Progress: 0, 1
Progress: 0, 1
Progress: 0, 1
Progress: 0, 1
Read completion: Operation canceled
Progress: 1, 1
Shutdown completion: Success
The handler vizualization:
Legend: some receives succeed as part of SSL negotiation. The
ec125
indicates cancellation. The "right most column" represents the shutdown which is initiated AFTER the read was completed withasio::error::operation_aborted
, and consists of a read & receive, as per SSL protocol.
Normally, you don't bother with all the detail. You would have async operations manage the lifetime of your session object, and implicitly know when all your operations completed because the destructor is invoked. There you safely deal with shutdown, keeping in mind not to throw exceptions:
#define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <deque>
#include <iostream>
using namespace std::literals;
namespace asio = boost::asio;
namespace ssl = asio::ssl;
using asio::ip::tcp;
using boost::system::error_code;
struct ClientSession : std::enable_shared_from_this<ClientSession> {
ClientSession(asio::any_io_executor ex) : s{std::move(ex), ctx_} {
s.lowest_layer().connect({{}, 8989});
s.handshake(ssl::stream_base::client); // synchronous for simplicity
}
void start() { do_read_loop(); }
void send(std::string msg) {
post(s.get_executor(), [this, self = shared_from_this(), m = std::move(msg)]() mutable {
outbox_.push_back(std::move(m));
if (outbox_.size() == 1)
do_write_loop();
});
}
void shutdown() {
post(s.get_executor(), [this, self = shared_from_this()]() {
s.lowest_layer().cancel();
});
}
~ClientSession() {
error_code ec;
s.shutdown(ec); // synchronous shutdown
std::cout << "Shutdown: " << ec.message() << std::endl;
}
private:
ssl::context ctx_{ssl::context::sslv23};
ssl::stream<tcp::socket> s;
std::array<char, 1024> buffer_;
std::deque<std::string> outbox_;
void do_read_loop() {
s.async_read_some(asio::buffer(buffer_), [this, self = shared_from_this()](error_code ec, size_t n) {
if (ec)
std::cout << "Read error: " << ec.message() << std::endl;
else {
std::cout << "Read " << n << " bytes: " << std::string(buffer_.data(), n) << std::endl;
do_read_loop(); // continue reading
}
});
}
void do_write_loop() {
if (outbox_.empty())
return;
asio::async_write( //
s, asio::buffer(outbox_.front()), [this, self = shared_from_this()](error_code ec, size_t n) {
if (ec) {
std::cout << "Write error: " << ec.message() << std::endl;
} else {
outbox_.pop_front();
std::cout << "Wrote " << n << " bytes." << std::endl;
}
do_write_loop(); // continue writing
});
}
};
int main() {
asio::io_context ioc(1); // single thread
{
auto sess = make_shared<ClientSession>(ioc.get_executor()); //
sess->start();
ioc.run_for(1s);
sess->send("Hello, world!\n"); // send a message
ioc.run_for(3s);
sess->send("Bye, world!\n"); // send a message
sess->shutdown(); // cancellation
} // sess goes out of scope here, so destructor will run once cancellation(s) complete
// run to completion
ioc.run();
}
A local demo against openssl s_server -port 8989 ...
:
In text, e.g.:
Read 18 bytes: Hello from server
Wrote 14 bytes.
Read 18 bytes: Hello from server
Read 18 bytes: Hello from server
Read 18 bytes: Hello from server
Wrote 12 bytes.
Read error: Operation canceled
Shutdown: Success