3

How do I implement asyncConnectAndHandshake function that connects to a TCP server and than does SSL handshake and returns boost::asio::experimental::promise?

#pragma once

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/cobalt.hpp>
#include <boost/asio/experimental/promise.hpp>
#include <boost/asio/experimental/use_promise.hpp>

class SocketWrapper
{
private:

    using SslStream = boost::asio::ssl::stream<boost::asio::ip::tcp::socket>;

public:

boost::asio::experimental::promise<void(boost::system::error_code), boost::asio::any_io_executor, std::allocator<void>>
    asyncConnectAndHandshake(const boost::asio::ip::basic_resolver_results<boost::asio::ip::tcp>& resolver_results)
{
    namespace asio = boost::asio;

    // pseudo code:
    auto promise1 =  asio::async_connect(underlyingSocket(), resolver_results, asio::experimental::use_promise);

    // This line of code should be executed when promise1 is completed.
    if (isSSL()) {
        auto promise2 = stream().async_handshake(asio::ssl::stream_base::client, asio::experimental::use_promise);
        return promise2;
    }

    return promise1;
}

private:

    bool isSSL() const;

    const boost::asio::ip::tcp::socket& underlyingSocket() const;
    boost::asio::ip::tcp::socket& underlyingSocket();

    const SslStream& stream() const;
    SslStream& stream();
};

The return type of asyncConnectAndHandshake function should be a promise that can be either used in a coroutine with co_await or in a regular function with operator().

Should I use async_compose?

1 Answer 1

2

You're trying to make a composed operation. There's no logical need for the promise1, and whether or not the composed operation would be consumed using a promise should be up to the caller.

You can use async_compose to create the composed operation (as @LucasL's answer shows, albeit un-idiomatically passing it a hardcoded completion token), but I'd prefer a more lightweight approach. Consider the co_composed initiator or asio::deferred:

template <typename Token = asio::deferred_t>
auto asyncConnect(tcp::resolver::results_type eps, Token&& token = {}) {
    auto op = asio::async_connect( //
        underlyingSocket(), std::move(eps),
        asio::deferred([this](error_code ec, tcp::endpoint) {
            return asio::deferred
                .when(ec.failed() || !isSSL())
                .then(asio::deferred.values(ec))
                .otherwise(stream().async_handshake(SslStream::client));
        }));

    return std::move(op)(std::forward<Token>(token));
}

This initiation function can be used with any completion token, so e.g.

w.asyncConnect(eps, [](auto ec) { std::cout << "Connect: " << ec.message() << std::endl; });

Or, indeed in a coro:

 co_await w.asyncConnect(eps);

Or, if you wanted a promise:

 auto p = w.asyncConnect(eps, asio::experimental::use_promise);

Or, basically anything you wanted.

Live On Coliru

#include <boost/asio.hpp>
#include <boost/asio/experimental/promise.hpp>
#include <boost/asio/experimental/use_promise.hpp>
#include <boost/asio/ssl.hpp>
#include <iostream>
#include <random>
using error_code = boost::system::error_code;
using namespace std::chrono_literals;
namespace asio = boost::asio;
using asio::ip::tcp;

class SocketWrapper {
  private:
    using SslStream  = asio::ssl::stream<tcp::socket>;

  public:
    SocketWrapper(asio::any_io_executor ex) : s_{ex, ctx_} {}

    template <typename Token = asio::deferred_t>
    auto asyncConnect(tcp::resolver::results_type eps, Token&& token = {}) {
        auto op = asio::async_connect( //
            underlyingSocket(), std::move(eps), asio::deferred([this](error_code ec, tcp::endpoint) {
                return asio::deferred
                    .when(ec.failed() || !isSSL())
                    .then(asio::deferred.values(ec))
                    .otherwise(stream().async_handshake(SslStream::client));
            }));

        return std::move(op)(std::forward<Token>(token));
    }

  private:
    bool isSSL() const { return true; }

    tcp::socket const& underlyingSocket() const { return s_.next_layer(); } 
    tcp::socket&       underlyingSocket()       { return s_.next_layer(); } 
    SslStream const&   stream() const           { return s_;              } 
    SslStream&         stream()                 { return s_;              } 

    asio::ssl::context ctx_{asio::ssl::context::tlsv13};
    SslStream          s_;
};


int main(int argc, char** argv) {
    asio::thread_pool ioc(1);

    SocketWrapper w(ioc.get_executor());
    auto eps = tcp::resolver{ioc}.resolve("localhost", "8989");

    auto select = argc > 1 ? atoi(argv[1]) : std::random_device{}();
    switch (select % 4) {
        case 0:
            std::cout << "Straight callback" << std::endl;
            w.asyncConnect(eps, [](error_code ec) { std::cout << "Callback: " << ec.message() << std::endl; });
            break;
        case 1:
            std::cout << "Coro await" << std::endl;
            co_spawn(ioc, [&] -> asio::awaitable<void> {
                    co_await w.asyncConnect(eps);
                    std::cout << "Coro connected" << std::endl;
                    co_return;
                },
                asio::detached);
            break;
        case 2:
            std::cout << "Coro await with promise" << std::endl;
            co_spawn(ioc, [&] -> asio::awaitable<void> {
                    auto p = w.asyncConnect(eps, asio::experimental::use_promise);
                    std::cout << "Doing some other time consuming stuff as well" << std::endl;

                    co_await p(asio::deferred);
                    std::cout << "Coro with promise connected" << std::endl;
                    co_return;
                },
                asio::detached);
            break;
        case 3: {
            std::cout << "Custom completion token with adaptors" << std::endl;
            auto f = w.asyncConnect(eps, asio::as_tuple(asio::use_future));

            if (f.wait_for(20ms) == std::future_status::ready) {
                auto [ec] = f.get();
                std::cout << "Future resolved within 20ms: " << ec.message() << std::endl;
            }
            break;
        }
    };

    ioc.join();
}

Printing e.g.:

+ ./build/sotest 0
Straight callback
Callback: Success

+ ./build/sotest 1
Coro await
Coro connected

+ ./build/sotest 2
Coro await with promise
Doing some other time consuming stuff as well
Coro with promise connected

+ ./build/sotest 3
Custom completion token with adaptors
Future resolved within 20ms: Success
Sign up to request clarification or add additional context in comments.

3 Comments

why do I write this when/then/otherwise, but not simply if/else in the implementation of asyncConnect?
I think it's a style issue only, perhaps it helps in deducing a common return type (but I don't know whether I ran into this). My choice to present it in this style is to drive home the point that asio::deferred_t was designed as s composition primitive, and encourage more exploration.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.