Using the following boost::asio code I run a loop of 1M sequential http calls to a Docker node.js simple http service that generates random numbers, but after a few thousand calls I start getting async_connect errors. The node.js part is not producing any errors and I believe it works OK.
To avoid resolving the host in every call and trying to speed-up, I am caching the endpoint, which makes no difference, I have tested both ways.
Can anyone see what is wrong with my code below?
Are there any best practices for a stress-test tool using asio that I am missing?
//------------------------------------------------------------------------------
// https://www.boost.org/doc/libs/1_70_0/libs/beast/doc/html/beast/using_io/timeouts.html
HttpResponse HttpClientAsyncBase::_http(HttpRequest&& req)
{
using namespace boost::beast;
namespace net = boost::asio;
using tcp = net::ip::tcp;
HttpResponse res;
req.prepare_payload();
boost::beast::error_code ec = {};
const HOST_INFO host = resolve(req.host(), req.port, req.resolve);
net::io_context m_io;
boost::asio::spawn(m_io, [&](boost::asio::yield_context yield)
{
size_t retries = 0;
tcp_stream stream(m_io);
if (req.timeout_seconds == 0) get_lowest_layer(stream).expires_never();
else get_lowest_layer(stream).expires_after(std::chrono::seconds(req.timeout_seconds));
get_lowest_layer(stream).async_connect(host, yield[ec]);
if (ec) return;
http::async_write(stream, req, yield[ec]);
if (ec)
{
stream.close();
return;
}
flat_buffer buffer;
http::async_read(stream, buffer, res, yield[ec]);
stream.close();
});
m_io.run();
if (ec)
throw boost::system::system_error(ec);
return std::move(res);
}
I have tried both sync/async implementations of a boost http client and I get the exact same problem.
The error I get is "You were not connected because a duplicate name exists on the network. If joining a domain, go to System in Control Panel to change the computer name and try again. If joining a workgroup, choose another workgroup name [system:52]"
So, I decided to... just try. I made your code into self-contained example:
#include <boost/asio/spawn.hpp>
#include <boost/beast.hpp>
#include <fmt/ranges.h>
#include <iostream>
namespace http = boost::beast::http;
//------------------------------------------------------------------------------
// https://www.boost.org/doc/libs/1_70_0/libs/beast/doc/html/beast/using_io/timeouts.html
struct HttpRequest : http::request<http::string_body> { // SEHE: don't do this
using base_type = http::request<http::string_body>;
using base_type::base_type;
std::string host() const { return "127.0.0.1"; }
uint16_t port = 80;
bool resolve = true;
int timeout_seconds = 0;
};
using HttpResponse = http::response<http::vector_body<uint8_t> >; // Do this or aggregation instead
struct HttpClientAsyncBase {
HttpResponse _http(HttpRequest&& req);
using HOST_INFO = boost::asio::ip::tcp::endpoint;
static HOST_INFO resolve(std::string const& host, uint16_t port, bool resolve) {
namespace net = boost::asio;
using net::ip::tcp;
net::io_context ioc;
tcp::resolver r(ioc);
using flags = tcp::resolver::query::flags;
auto f = resolve ? flags::address_configured
: static_cast<flags>(flags::numeric_host | flags::numeric_host);
tcp::resolver::query q(tcp::v4(), host, std::to_string(port), f);
auto it = r.resolve(q);
assert(it.size());
return HOST_INFO{it->endpoint()};
}
};
HttpResponse HttpClientAsyncBase::_http(HttpRequest&& req) {
using namespace boost::beast;
namespace net = boost::asio;
using net::ip::tcp;
HttpResponse res;
req.prepare_payload();
boost::beast::error_code ec = {};
const HOST_INFO host = resolve(req.host(), req.port, req.resolve);
net::io_context m_io;
spawn(m_io, [&](net::yield_context yield) {
// size_t retries = 0;
tcp_stream stream(m_io);
if (req.timeout_seconds == 0)
get_lowest_layer(stream).expires_never();
else
get_lowest_layer(stream).expires_after(std::chrono::seconds(req.timeout_seconds));
get_lowest_layer(stream).async_connect(host, yield[ec]);
if (ec)
return;
http::async_write(stream, req, yield[ec]);
if (ec) {
stream.close();
return;
}
flat_buffer buffer;
http::async_read(stream, buffer, res, yield[ec]);
stream.close();
});
m_io.run();
if (ec)
throw boost::system::system_error(ec);
return res;
}
int main() {
for (int i = 0; i<100'000; ++i) {
HttpClientAsyncBase hcab;
HttpRequest r(http::verb::get, "/bytes/10", 11);
r.timeout_seconds = 0;
r.port = 80;
r.resolve = false;
auto res = hcab._http(std::move(r));
std::cout << res.base() << "\n";
fmt::print("Data: {::02x}\n", res.body());
}
}
(Side note, this is using docker run -p 80:80 kennethreitz/httpbin to run the server side)
While this is about 10x faster than running curl to do the equivalent requests in a bash loop, none of this is particularly stressing. There's nothing async about it, and it seems resource usage is mild and stable, e.g. memory profiled:
(for completeness I verified identical results with timeout_seconds = 1)
Since what you're doing is literally the opposite of async IO, I'd write it much simpler:
struct HttpClientAsyncBase {
net::io_context m_io;
HttpResponse _http(HttpRequest&& req);
static auto resolve(std::string const& host, uint16_t port, bool resolve);
};
HttpResponse HttpClientAsyncBase::_http(HttpRequest&& req) {
HttpResponse res;
req.requestObject.prepare_payload();
const auto host = resolve(req.host(), req.port, req.resolve);
beast::tcp_stream stream(m_io);
if (req.timeout_seconds == 0)
stream.expires_never();
else
stream.expires_after(std::chrono::seconds(req.timeout_seconds));
stream.connect(host);
write(stream, req.requestObject);
beast::flat_buffer buffer;
read(stream, buffer, res);
stream.close();
return res;
}
That's just simpler, runs faster and does the same, down to the exceptions.
But, you're probably trying to cause stress, perhaps you instead need to reuse some connections and multi-thread?
You can see a very complete example of just that here:
How do I make this HTTPS connection persistent in Beast?
It includes reconnecting dropped connections, connections to different hosts, varied requests etc.
Alan's comments gave me the right pointers and I soon found using netstat -a that it was a ports leakage problem with thousands of ports in TIME_WAIT state after running the code for some brief time.
The root cause was both on the client and the server:
In node.js server I had to make sure that responses close the connection by
adding
response.setHeader("connection", "close");
In boost::asio C++ code I replaced stream.close() with
stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
That seems to make all the difference. Also I made sure to use
req.set(boost::beast::http::field::connection, "close");
in my requests.
I verfied with the tool running for over 5 hours with no problems at all, so I guess the problem is solved!
Implementing 'Abortive TCP/IP Close' with boost::asio to treat EADDRNOTAVAIL and TIME_WAIT for HTTP client stress test tool
I am revisting the issue to offer an alternative that actually worked much better. Reminding that the objective was to develop a stress test tool for hitting a server with 1M requests. Even though my previous solution worked on Windows, when I loaded the executable on Docker/Alpine it started crashing with SEGFAULT errors that I was unable to trace. The root cause seems to be related to boost::asio::spawn(m_io, [&](boost::asio::yield_context yield) but time pressured me to solve the HTTP problem.
I decided to use synch HTTP and treat EADDRNOTAVAIL and TIME_WAIT errors by following suggestions from Disable TIME_WAIT with boost sockets and TIME_WAIT with boost asio and template code from https://www.boost.org/doc/libs/1_80_0/libs/beast/example/http/client/sync/http_client_sync.cpp.
For anyone having EADDRNOTAVAIL and TIME_WAIT with boost::asio, the solution that worked for me and it is actually much faster than before on both Windows, Linux and Dockers is the following:
HttpResponse HttpClientSyncBase::_http(HttpRequest&& req)
{
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = net::ip::tcp;
HttpResponse res;
req.prepare_payload();
const auto host = req.host();
const auto port = req.port;
const auto target = req.target();
const bool abortive_close = boost::iequals(req.header("Connection"), "close");
const bool download_large_file = boost::iequals(req.header("X-LARGE-FILE-HINT"), "YES");
beast::error_code ec;
net::io_context ioc;
// Resolve host:port for IPv4
tcp::resolver resolver(ioc);
const auto endpoints = resolver.resolve(boost::asio::ip::tcp::v4(), host, port);
// Create stream and set timeouts
beast::tcp_stream stream(ioc);
if (req.timeout_seconds == 0) boost::beast::get_lowest_layer(stream).expires_never();
else boost::beast::get_lowest_layer(stream).expires_after(std::chrono::seconds(req.timeout_seconds));
// Caution: we can get address_not_available[EADDRNOTAVAIL] due to TIME_WAIT port exhaustion
stream.connect(endpoints, ec);
if (ec == boost::system::errc::address_not_available)
throw beast::system_error{ ec };
// Write HTTP request
http::write(stream, req);
// Read HTTP response (or download large file >8MB)
beast::flat_buffer buffer;
if (download_large_file)
{
_HttpResponse tmp;
boost::beast::http::response_parser<boost::beast::http::string_body> parser{ std::move(tmp) };
parser.body_limit(boost::none);
boost::beast::http::read(stream, buffer, parser);
res = HttpResponse(std::move(parser.release()));
}
else
{
http::read(stream, buffer, res);
}
// Try to shut down socket gracefully
stream.socket().shutdown(tcp::socket::shutdown_both, ec);
if (abortive_close)
{
// Read until no more data are in socket buffers
// https://stackoverflow.com/questions/58983527/disable-time-wait-with-boost-sockets
try
{
http::response<http::dynamic_body> res;
beast::flat_buffer buffer;
http::read(stream, buffer, res);
}
catch (...)
{
// should get end of stream here, ignore it
}
// Perform "Abortive TCP/IP Close" to minimize TIME_WAIT port exhaustion
// https://stackoverflow.com/questions/35006324/time-wait-with-boost-asio
try
{
// enable linger with timeout 0 to force abortive close
boost::asio::socket_base::linger option(true, 0);
stream.socket().set_option(option);
stream.close();
}
catch (...)
{
}
}
else
{
try { stream.close(); } catch (...) {}
}
// Ignore not_connected and end_of_stream errors, handle the rest
if (ec && ec != beast::errc::not_connected && ec != beast::http::error::end_of_stream)
throw beast::system_error{ ec };
return std::move(res);
}
In the sample above I should add error handling in write but I guess anyone can do it. _HttpResponse is the following and is the base for HttpResponse.
using _HttpRequest = boost::beast::http::message<true, boost::beast::http::string_body, boost::beast::http::fields>;
using _HttpResponse = boost::beast::http::message<false, boost::beast::http::string_body, boost::beast::http::fields>;
using HttpHeaders = boost::beast::http::header<1, boost::beast::http::basic_fields<std::allocator<char>>>;
For what is worth, when I started the estimation for the job was 5-7 days. Using connetion=close in my previous solution it got down to 7-8 hours. Using Abortive TCP/IP Close I got down to 1.5 hours.
Funny thing is, the server, also boost::asio, could handle the stress while the original stress tool didn't. Finally both the server and its stress test tool work just fine! The code also demonstrates how to download a large file (over 8MB) which was another side-problem, as I needed to download the test results from the server.
I am trying to send HTTPS request to a server and receive the page contents by only using Boost.Asio(not Network.Ts or Beast or others) by these code :
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <iostream>
int main() {
boost::system::error_code ec;
using namespace boost::asio;
// what we need
io_service svc;
ssl::context ctx(ssl::context::method::tlsv1);
ssl::stream<ip::tcp::socket> ssock(svc, ctx);
ip::tcp::endpoint endpoint(boost::asio::ip::make_address("157.90.94.153",ec),443);
ssock.lowest_layer().connect(endpoint);
ssock.handshake(ssl::stream_base::handshake_type::client);
// send request
std::string request("GET /index.html HTTP/1.1\r\n\r\n");
boost::asio::write(ssock, buffer(request));
// read response
std::string response;
do {
char buf[1024];
size_t bytes_transferred = ssock.read_some(buffer(buf), ec);
if (!ec) response.append(buf, buf + bytes_transferred);
} while (!ec);
// print and exit
std::cout << "Response received: '" << response << "'\n";
}
But I keep getting 405 Not Allowed on my local PC and 400 Bad Request on Coliru.
What did I do wrong?
... "GET /index.html HTTP/1.1\r\n\r\n"
This is not a valid HTTP/1.1 request. It must at least also contain a Host field and the value of the field must match the servers expectation, i.e.
"GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"
In general, HTTP might look easy but is actually complex and has several pitfalls. If you really need to do HTTP by your own please study the standard.
I would like to periodically http GET some value from a website.
with beast, the code is like:
// Set up an HTTP GET request message
http::request<http::string_body> req{http::verb::get, target, version};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
http::response<http::string_body> res;
while (true) {
// Send the HTTP request to the remote host
http::write(stream, req);
res = {};
// Receive the HTTP response
http::read(stream, buffer, res);
// Write the message to standard out
std::cout << res << std::endl;
}
But in the loop,
res = {}
brought one temporary object creation, one move/copy assignment and the destroy of the temporary object.
I am wondering if there is a better way to avoid these unnecessary costs。
Just remove the offending line and move the declaration of res inside the while loop. res will then be created and destroyed each time round the loop:
// Set up an HTTP GET request message
http::request<http::string_body> req{http::verb::get, target, version};
req.set(http::field::host, host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
while (true) {
// Send the HTTP request to the remote host
http::write(stream, req);
// Receive the HTTP response
http::response<http::string_body> res;
http::read(stream, buffer, res);
// Write the message to standard out
std::cout << res << std::endl;
}
You can replace res = {}; with res.clear(); or drop it altogether.
The read free function instantiates a parser that takes ownership of all response resources (by move)¹. In its constructor, it unconditionally clears the message object anyways:
You can use a debugger to trace through these lines with a simple tester like this, which is exactly what I did.
¹ in the end the message is moved back into the response object passed, so it works out
Here's what I would do
http::read(stream, buffer, res);
// move the response object to the processing function
process(std::move(res)); // res will be as clean as newly constructed after being moved
I am trying to send message with a header using boost http library. I searched for a way to send message with a header but I could not find.
what I want to do is following
auto const results = resolver.resolve(host, port);
beast::get_lowest_layer(stream).connect(results);
stream.handshake(ssl::stream_base::client);
http::request<http::string_body> req(verb, query + data, 11);
req.set(http::field::host, host);
// set http header ("key" = "I am a header")
// I want to add above code.
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
http::write(stream, req);
beast::flat_buffer buffer;
http::response<http::dynamic_body> res;
http::read(stream, buffer, res);
Please let me know proper way to add header to boost-beast http request. Thanks!
Just
req.set("key", "I am a header");
It's pretty much the same as the other - standard HTTP - header, but using a custom name.
See it Live On Coliru
#include <boost/beast/http.hpp>
#include <iostream>
namespace http = boost::beast::http;
int main() {
auto verb = http::verb::get;
std::string query = "/path";
std::string data = "?whatever=more";
std::string host = "example.com";
http::request<http::string_body> req(verb, query + data, 11);
req.set(http::field::host, host);
req.set("key", "I am a header");
req.prepare_payload();
std::cout << req;
}
Prints
GET /path?whatever=more HTTP/1.1
Host: example.com
key: I am a header
Trying to send multiple requests via one connection:
int count = boost::asio::write(socket, request); // this will result in normal count2
int count2 = boost::asio::write(socket, request); // this one gives zero
However cont2 is equal to zero meaning that id does not send data.. Ho to fix it
Adding more code for getting better view:
boost::asio::io_service io_service;
boost::asio::streambuf lastResponse;
// Get a list of endpoints corresponding to the server name.
tcp::resolver resolver(io_service);
char *host = "somewebsie.coom";
char *requestPath = "/somequery";
tcp::resolver::query query(host, "http");
tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
// Try each endpoint until we successfully establish a connection.
tcp::socket socket(io_service);
boost::asio::socket_base::keep_alive option(true);
boost::asio::socket_base::non_blocking_io option2(true);
boost::asio::socket_base::non_blocking_io command(true);
double startTime,endTime;
boost::asio::streambuf request;
std::ostream request_stream(&request);
request_stream << "GET "<< requestPath <<" HTTP/1.1\r\nHost: "<<host <<":80\r\n\r\n";
boost::asio::connect(socket, endpoint_iterator);
socket.set_option(option);
socket.io_control(command);
startTime = getRealTime( );
// 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.
int count2 = boost::asio::write(socket, request);
count2 = boost::asio::write(socket, request);
bool isopen = socket.is_open(); // return true...
Why are you calling write with a stream you just emptied?! Trying to write zero bytes is not a good idea.
You're not trying to send multiple requests. There is only one request in your code, and you've already sent it.