Last active
January 27, 2023 09:59
-
-
Save Tosainu/77628e22e963aa62e50a to your computer and use it in GitHub Desktop.
[WIP] An asynchronous HTTP client using Boost.Asio's stackful coroutines.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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