0

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.

  • 1
    What version of Boost are you using? I remember SSL buffer handling being vastly improved a while back. Until that time Beast was using their own helpers to avoid the slow-down that you seem to describe https://github.com/boostorg/beast/issues/1108 – sehe Mar 21 '23 at 10:43
  • We are using version 1.81.0, which is the current release. – Dave Edwards Mar 21 '23 at 13:16
  • Interesting. Have you considered the scatter gather approach? See [docs](https://www.boost.org/doc/libs/1_81_0/doc/html/boost_asio/reference/buffer.html#boost_asio.reference.buffer.buffers_and_scatter_gather_i_o) or e.g. this example https://stackoverflow.com/a/65705168/85371 – sehe Mar 21 '23 at 13:19

1 Answers1

0

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.

Like the other stream adaptors (like ssl_stream itself), instead the lower layer stream object is owned by the adaptor.

  • the constructor can forward initializers to it
  • you can access the layer using next_layer() (or lowest_layer() - which might be different in the case of multiple adaptors).

There is an example of how it is used in the tests (test/buffered_stream.hpp)

sehe
  • 374,641
  • 47
  • 450
  • 633
  • I might be missing something, but it looks like the buffered_stream constructor can only forward a single argument. The constructors for ssl::stream require at least two arguments (except for the move constructor). The tests don't have this issue because they wrap a buffered_stream around a tcp::socket, which has a single-argument constructor. – Dave Edwards Mar 21 '23 at 13:21
  • That seems to be the exact analysis. Perhaps you could open a feature request, or request for clarification in case there's some reason why buffering on ssl streams is a bad idea. I'd love to see an actual use case where slowness is an issue to see whether I can see a way to solve it - without `buffered_stream` perhaps – sehe Mar 21 '23 at 15:36
  • 1
    I opened an [issue](https://github.com/chriskohlhoff/asio/issues/1265) in the Asio repository to ask about this. I'll follow up here once I've heard back. – Dave Edwards Mar 22 '23 at 18:53