Problem Description
We have some C++ networking code in our codebase that uses OpenSSL directly. To make the code easier to maintain, I'm trying to replace it with the equivalent code that uses Boost.Asio. The code sets up a TLS connection between a server and client (using mutual authentication), and allows both sides to send and receive arrays of bytes.
On our benchmarks, the Asio-based code took nearly 5 times longer to run than the OpenSSL-based version. The cause was that the OpenSSL version used buffering for the network communications, and the Boost.Asio version did not. Since our application sends many small messages back and forth, buffering is important for performance (our API has a flush
function that we call to flush the send buffer as necessary).
I added some logic to manage a std::vector
of bytes as a buffer, and updated our application to write to the buffer and only send network traffic when flush
is called. This improved the performance, but even with that change, the Asio-based code is still about 30% slower than the OpenSSL-based code on our benchmarks.
I see that Boost.Asio seems to have support for buffered I/O. For example, the buffered_stream
class seems to do exactly what I'm looking for. However, I can't see how to get the constructor for that class to work when the underlying stream is of type boost::asio::ssl::stream
. Does anyone know how to get buffered_stream
working with SSL on Asio? If not, is there a better way to do buffered I/O with Asio that might give us similar performance to our OpenSSL-based code?
Prior (Unsuccessful) Work
Here's what I've tried so far:
using namespace boost::asio;
using ssl_socket_t = ssl::stream<ip::tcp::socket>;
// this function (defined elsewhere) returns an ssl::context object with the correct configuration for a client
ssl::context tlsClientContext(std::string_view certificate_filename,
std::string_view private_key_filename,
std::string_view trust_store_filename);
// I am trying to implement an API like the following:
buffered_stream<ssl_socket_t> connect(std::string_view hostname,
std::uint16_t port,
std::string_view certificate_filename,
std::string_view private_key_filename,
std::string_view trust_store_filename) {
auto ssl_ctx = tlsClientContext(certificate_filename, private_key_filename, trust_store_filename);
auto ssl_socket = ssl_socket_t(system_executor(), ssl_ctx);
auto resolver = ip::tcp::resolver(ssl_socket.get_executor());
connect(ssl_socket.lowest_layer(), resolver.resolve(hostname, std::to_string(port)));
ssl_socket.handshake(ssl_socket_t::client);
return buffered_stream<ssl_socket_t>(std::move(ssl_socket));
}
void read(buffered_stream<ssl_socket_t>& stream, std::byte* data, std::size_t num_bytes) {
boost::asio::read(stream, buffer(data, num_bytes));
}
void write(buffered_stream<ssl_socket_t>& stream, const std::byte* data, std::size_t num_bytes) {
boost::asio::write(stream, buffer(data, num_bytes));
}
void flush(buffered_stream<ssl_socket_t>& stream) {
stream.flush();
}
The connect
function above doesn't compile because the buffered_stream
constructor requires an lvalue as its first argument. However, if I remove the call to std::move
, it also fails to compile because ssl::stream
is not copyable. I expected buffered_stream
to allow me to pass a rvalue reference to the stream to be buffered, but I don't see how that's possible.