I've got simple io_context object which is used to initialize ssl_stream object (using async_resolve, async_connect and async_handshake).
On a different scope, it's used to call async_read and async_write to pass IO in that connection.
the async calls are performed from within coroutine (boost::asio::spawn(io_context_, [&](boost::asio::yield_context yield)). each one of the stages above is executed on a different such coroutine. In order to execute the coroutine, the underlying io_context of the ssl_stream should be in run() method.
However, since those 2 methods are separated, than a single run() stage wouldn't be enough. After the connection initialization will be finished, the first run() will be terminated, so a second run() instance should be called right after the second coroutine (that does the IO ops) is called)
However, I observed that the second run goes out immediately, and doesn't perform the recently inserted spawn. Any idea how to overcome this scenario ? or the only alternative is to run the whole connection lifecycle in single coroutine, or call the run on a separated thread that never quits...
Here's the semi-pseudo code of my scenario :
boost::asio::io_context io_context_;
std::optional<boost::beast::tcp_stream> stream_;
std::optional<boost::asio::ip::tcp::resolver> resolver_;
//STAGE 1
boost::asio::spawn(io_context_, [&](boost::asio::yield_context yield) {
results = resolver_->async_resolve(host_,port_ , yield);
ssl_stream_->next_layer().async_connect(results, yield);
ssl_stream_->async_handshake(ssl::stream_base::client, yield);
}
try {
io_context_.run();
} catch (...) {
}
//STAGE 2
beast::flat_buffer buffer;
http::response<http::dynamic_body> res;
boost::asio::spawn(io_context_, [&](boost::asio::yield_context yield) {
beast::get_lowest_layer(*ssl_stream_).expires_after(kOpTimeout);
auto sent = http::async_write(*ssl_stream_, beast_request, yield);
auto read = http::async_read(*ssl_stream_, buffer, res, yield);
});
try {
io_context_.run();
return res;
} catch (...) {
}
It is possible, but a-typical. As documented, in order to be able to re-run after the service ran out of work (I.e. returning 0 handlers when executed), you need to call restart()
(previously reset()
):
A normal exit from the
run()
function implies that theio_context
object is stopped (thestopped()
function returnstrue
). Subsequent calls torun()
,run_one()
,poll()
orpoll_one()
will return immediately unless there is a prior call torestart()
.
Here is a demo based on your snippet:
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>
#include <iostream>
#include <optional>
namespace net = boost::asio;
namespace ssl = net::ssl;
namespace beast = boost::beast;
namespace http = beast::http;
using net::ip::tcp;
using namespace std::chrono_literals;
struct X {
std::string host_ = "www.example.com";
std::string port_ = "https";
static auto constexpr kOpTimeout = 3s;
net::io_context io_context_;
ssl::context ctx_{ssl::context::sslv23_client};
std::optional<beast::ssl_stream<beast::tcp_stream>> ssl_stream_{std::in_place,
io_context_, ctx_};
std::optional<tcp::resolver> resolver_{std::in_place, io_context_};
X() { ctx_.set_default_verify_paths(); }
void STAGE1() {
net::spawn(io_context_, [&](net::yield_context yield) {
try {
auto results = resolver_->async_resolve(host_, port_, yield);
ssl_stream_->next_layer().async_connect(results, yield);
ssl_stream_->async_handshake(ssl::stream_base::client, yield);
} catch (std::exception const& e) {
std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
throw;
}
});
if (io_context_.stopped())
io_context_.restart();
try {
io_context_.run();
} catch (std::exception const& e) {
std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
throw;
}
}
auto STAGE2() {
beast::flat_buffer buffer;
http::response<http::dynamic_body> res;
http::request<http::empty_body> beast_request(http::verb::get, "/", 11);
beast_request.set(http::field::host, "www.example.com");
net::spawn(io_context_, [&](net::yield_context yield) {
try {
beast::get_lowest_layer(*ssl_stream_).expires_after(kOpTimeout);
/*auto sent =*/ http::async_write(*ssl_stream_, beast_request, yield);
/*auto read =*/ http::async_read(*ssl_stream_, buffer, res, yield);
} catch (std::exception const& e) {
std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
throw;
}
});
if (io_context_.stopped())
io_context_.restart();
try {
io_context_.run();
} catch (std::exception const& e) {
std::cerr << "Whoops in " << __LINE__ << ": " << e.what() << std::endl;
throw;
}
return res;
}
};
int main() {
X x;
x.STAGE1();
std::cout << x.STAGE2();
}
Prints, locally:
HTTP/1.1 200 OK
Age: 533333
Cache-Control: max-age=604800
Content-Type: text/html; charset=UTF-8
Date: Sun, 18 Dec 2022 21:50:21 GMT
Etag: "3147526947+ident"
Expires: Sun, 25 Dec 2022 21:50:21 GMT
Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
Server: ECS (bsa/EB20)
Vary: Accept-Encoding
X-Cache: HIT
Content-Length: 1256
<!doctype html>
<html>
<head>
<title>Example Domain</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style type="text/css">
body {
background-color: #f0f0f2;
margin: 0;
padding: 0;
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
div {
width: 600px;
margin: 5em auto;
padding: 2em;
background-color: #fdfdff;
border-radius: 0.5em;
box-shadow: 2px 3px 7px 2px rgba(0,0,0,0.02);
}
a:link, a:visited {
color: #38488f;
text-decoration: none;
}
@media (max-width: 700px) {
div {
margin: 0 auto;
width: auto;
}
}
</style>
</head>
<body>
<div>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples in documents. You may use this
domain in literature without prior coordination or asking for permission.</p>
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
Again, make buffer
a member, because its context will matter for any subsequent traffic on the connection.
Also, don't use run()
to find out when things are "done". You can use futures, or any other way of synchronization:
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast.hpp>
#include <boost/beast/ssl.hpp>
#include <iostream>
#include <optional>
namespace net = boost::asio;
namespace ssl = net::ssl;
namespace beast = boost::beast;
namespace http = beast::http;
using net::ip::tcp;
using namespace std::chrono_literals;
using duration = std::chrono::steady_clock::duration;
struct Client {
Client(net::any_io_executor ex) : ssl_stream_{ex, ctx_} {
ctx_.set_default_verify_paths();
}
void connect(std::string host, std::string port = "https") {
host_ = host;
std::packaged_task<void(std::string, std::string, net::yield_context)> task{
[this](std::string host, std::string port, net::yield_context yield) {
tcp::resolver resolver_{ssl_stream_.get_executor()};
auto results = resolver_.async_resolve(host, port, yield);
ssl_stream_.next_layer().async_connect(results, yield);
ssl_stream_.async_handshake(ssl::stream_base::client, yield);
}};
auto fut = task.get_future();
net::spawn(ssl_stream_.get_executor(),
std::bind(std::move(task), std::move(host), std::move(port),
std::placeholders::_1));
return fut.get();
}
using Response = http::response<http::dynamic_body>;
Response makeRequest(std::string target, duration kOpTimeout = 300ms) {
http::request<http::empty_body> beast_request(http::verb::get, target, 11);
beast_request.set(http::field::host, host_);
std::packaged_task<Response(net::yield_context)> task{
[kOpTimeout, req = std::move(beast_request), this](net::yield_context yield) {
Response res;
beast::get_lowest_layer(ssl_stream_).expires_after(kOpTimeout);
/*auto sent =*/http::async_write(ssl_stream_, req, yield);
/*auto read =*/http::async_read(ssl_stream_, buffer_, res, yield);
return res;
}};
auto fut = task.get_future();
net::spawn(ssl_stream_.get_executor(), std::move(task));
return fut.get();
}
private:
ssl::context ctx_{ssl::context::sslv23_client};
beast::ssl_stream<beast::tcp_stream> ssl_stream_;
beast::flat_buffer buffer_;
std::string host_;
};
int main() {
net::thread_pool ioc(1); // 1 thread is enough
Client x(make_strand(ioc)); // strand only required if you have multiple IO threads
x.connect("httpbin.org");
std::cout << x.makeRequest("/delay/2", 3s) << std::endl;
std::cout << "Second request will time out: " << std::endl;
std::cout << x.makeRequest("/delay/5", 1s); // timeout
ioc.join(); // wait for service to complete
}
Locally: