I want to close the TLS connection gracefully. So before closing the socket I call ssl::stream::shutdown
. I call ssl::stream::shutdown
in the session destructor and at that moment I have no active async operations.
However, sometimes ssl::stream::shutdown
hangs and does not return. Because of this, the server remains waiting and cannot close the connection with the client.
Why can ssl::stream::shutdown
hang and how to solve this problem? This is how I call shutdown
struct ServerSession : std::enable_shared_from_this<ServerSession>
{
// ...
~ServerSession()
{
error_code ec;
s.shutdown(ec); // synchronous shutdown
std::cout << "Shutdown: " << ec.message() << std::endl;
}
// ...
private:
ssl::context ctx_;
ssl::stream<tcp::socket> s;
};
So, mulling things over, I came to this elegant idea to make the shutdown
async AND time limited without heroics. Well, without manual heroics.
Referring to the example from the other answer where we had:
~ClientSession() {
error_code ec;
s.shutdown(ec); // synchronous shutdown
std::cout << "Shutdown: " << ec.message() << std::endl;
}
You could replace it with the equivalent async timelimited approach:
~ClientSession() {
// auto token = asio::detached;
using keep_t = std::pair<ssl::context, ssl::stream<tcp::socket>>;
auto keep = std::make_unique<keep_t>(std::move(ctx_),std::move(s_));
auto& [_, s] = *keep; // avoid UB by getting the reference before the move
s.async_shutdown(consign(asio::cancel_after(50ms, token), std::move(keep)));
// Note: The stream will be destroyed only after the shutdown completes (keep)
}
There's a bit of subtlety there:
s
member is moved from, so can no longer be used after cnstructing keep
stream
reference MUST be taken before the call to stream.async_shutdown
as x->foo(std::move)
invokes UBkeep
is consigned to the completion token so we don't have to bind or capture anythingkeep
includes the ssl::context
because it might be used in the stream's shutdown implementationpair
need to be in that order (order of destruction)tuple
instead of the pair
async_shutdown
has been cancelled before keep
is released.Of course, to get it actually equivalent to the synchronous original version:
auto token = [](error_code ec) { std::cout << "Shutdown: " << ec.message() << std::endl; };
#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() {
// auto token = asio::detached;
auto token = [](error_code ec) { std::cout << "Shutdown: " << ec.message() << std::endl; };
using keep_t = std::pair<ssl::context, ssl::stream<tcp::socket>>;
auto keep = std::make_unique<keep_t>(std::move(ctx_),std::move(s_));
auto& [_, s] = *keep; // avoid UB by getting the reference before the move
s.async_shutdown(consign(asio::cancel_after(50ms, token), std::move(keep)));
std::cout << "ClientSession destroyed, waiting for shutdown to complete..." << std::endl;
// Note: The stream will be destroyed only after the shutdown completes (keep)
}
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();
}
Printing e.g.
Wrote 14 bytes.
Wrote 12 bytes.
Read error: Operation canceled
ClientSession destroyed, waiting for shutdown to complete...
Shutdown: Success
And the handler-visualization: