Skip to content

Instantly share code, notes, and snippets.

@Tosainu
Last active January 27, 2023 09:59
Show Gist options
  • Save Tosainu/77628e22e963aa62e50a to your computer and use it in GitHub Desktop.
Save Tosainu/77628e22e963aa62e50a to your computer and use it in GitHub Desktop.
[WIP] An asynchronous HTTP client using Boost.Asio's stackful coroutines.
#include <iostream>
#include <istream>
#include <ostream>
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
class client {
public:
client(boost::asio::io_service& io_service, const std::string& server, const std::string& path)
: resolver_(io_service), socket_(io_service) {
// Form the request. We specify the "Connection: close" header so that the
// server will close the socket after transmitting the response. This will
// allow us to treat all data up until the EOF as the content.
std::ostream request_stream(&request_buf_);
request_stream << "GET " << path << " HTTP/1.1\r\n";
request_stream << "Host: " << server << "\r\n";
request_stream << "Accept: */*\r\n";
request_stream << "Connection: close\r\n\r\n";
boost::asio::ip::tcp::resolver::query query(server, "80");
// Start a new Stackful Coroutine.
boost::asio::spawn(io_service, [ q = std::move(query), this ](auto yield) { do_resolve(q, yield); });
}
private:
void do_resolve(const boost::asio::ip::tcp::resolver::query& query, boost::asio::yield_context yield) {
boost::system::error_code ec;
// Start an asynchronous resolve to translate the server and service names
// into a list of endpoints.
auto iterator = resolver_.async_resolve(query, yield[ec]);
if (ec) {
std::cerr << "failed to resolve: " << ec.message() << "\n";
return;
}
do_connect(iterator, yield);
}
void do_connect(const boost::asio::ip::tcp::resolver::iterator& iterator, boost::asio::yield_context yield) {
boost::system::error_code ec;
// Attempt a connection to each endpoint in the list until we
// successfully establish a connection.
boost::asio::async_connect(socket_, iterator, yield[ec]);
if (ec) {
std::cerr << "failed to do_connect: " << ec.message() << "\n";
return;
}
do_write_req(yield);
}
void do_write_req(boost::asio::yield_context yield) {
boost::system::error_code ec;
// The connection was successful. Send the request.
boost::asio::async_write(socket_, request_buf_, yield[ec]);
if (ec) {
std::cerr << "failed to do_write_req: " << ec.message() << "\n";
return;
}
do_read_status(yield);
}
void do_read_status(boost::asio::yield_context yield) {
boost::system::error_code ec;
// Read the response status line. The response_buf_ streambuf will
// automatically grow to accommodate the entire line. The growth may be
// limited by passing a maximum size to the streambuf constructor.
boost::asio::async_read_until(socket_, response_buf_, "\r\n", yield[ec]);
if (ec) {
std::cerr << "failed to do_read_status: " << ec.message() << "\n";
return;
}
std::istream response_stream(&response_buf_);
std::string http_version;
unsigned int status_code;
response_stream >> http_version >> status_code >> std::ws;
std::string status_message;
std::getline(response_stream, status_message);
// Check that response is OK.
if (!response_stream || http_version.compare(0, 5, "HTTP/")) {
std::cerr << "Invalid response\n";
return;
}
do_read_header(yield);
}
void do_read_header(boost::asio::yield_context yield) {
boost::system::error_code ec;
// Read the response headers, which are terminated by a blank line.
boost::asio::async_read_until(socket_, response_buf_, "\r\n\r\n", yield[ec]);
if (ec) {
std::cerr << "failed to do_read_header: " << ec.message() << "\n";
return;
}
// Process the response headers.
std::istream response_stream(&response_buf_);
std::string header;
while (std::getline(response_stream, header) && header != "\r") {
std::cout << header << "\n";
}
std::cout << "\n";
// Write whatever content we already have to output.
if (response_buf_.size() > 0) {
std::cout << &response_buf_;
}
do_read_content(yield);
}
void do_read_content(boost::asio::yield_context yield) {
boost::system::error_code ec;
// Continue reading remaining data until EOF.
boost::asio::async_read(socket_, response_buf_, boost::asio::transfer_at_least(1), yield[ec]);
if (ec == boost::asio::error::eof) {
std::cout << "\n";
} else if (ec) {
std::cerr << "failed to do_read_content: " << ec.message() << "\n";
} else {
// Write all of the data that has been read so far.
std::cout << &response_buf_;
do_read_content(yield);
}
}
boost::asio::ip::tcp::resolver resolver_;
boost::asio::ip::tcp::socket socket_;
boost::asio::streambuf request_buf_;
boost::asio::streambuf response_buf_;
};
auto main() -> int {
try {
boost::asio::io_service io_service;
client a(io_service, "example.com", "/");
client c(io_service, "myon.info", "/");
io_service.run();
} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment