3
votes

I am trying to create an HTTP server using boost::beast but I noticed a delay of one second in response time. I used the advanced server example and synchronous client example to benchmark this behavior.

Also tried to disable Nagle's algorithm but had no effect and doesn't seem to be an optimisation issue.

Requests were made manually one every few seconds, so a high load on the sever side is not reasonable.

It seems to be related to boost::asio sockets because I also created an HTTP server some time ago (with boost::asio - Boost v1.64 I think, when Beast wasn't around) and I expericend the same high response time - this is the reason I switched to Beast.

My questions:

Is this a known behavior for boost::asio sockets?

Can this delay be addressed?

Is there a reason why setting no_delay on the socket might not work?

Benchmark result:

The average response time from example server was between 1005 ms and 1020 ms as seen in the screenshot bellow. In contrast, the same client gets a response from google.com under 120 ms.

Here is a comparison between requests to my local server versus requests to www.google.com

Hence the question again: from where this 900 ms delay can come from? This is not an acceptable response time for any HTTP server.

And also tried the fast example server and the synchronous server example with the same result: over 1000 ms response time.

Benchmark setup

Test System:

  • Windows 10 Pro x64
  • Processor Intel i7-7700, 16GB RAM
  • Boost 1.66
  • Visual Studio 2015

Tested in x86 and x64 with Debug and Release builds. The only difference was, as expected, a 20-30ms additional delay on Debug builds.

Server setup

  • I took the advanced server example from Beast documentation
  • Created a new empty console project in Visual Studio 2015
  • Added the example code to a new cpp source file
  • In Project's properties > VC++ Directories > Include Directories set boost "...boost\include" include folder
  • In Project's properties > VC++ Directories > Library Directories set boost library folder
  • In C/C++ > Preprocessor added "_WIN32_WINNT=0x0601"
  • In Debugging > Command Arguments I added "0.0.0.0 8080 . 3" which means "bind server to all local addresses with port 8080, using . (current directory) as root folder and run server on 3 threads".

Client setup

  • I used synchronous client from Beast documentation
  • Created a similar Visual Studio project

In order to measure the request time I added timers in client code and commented out the output of the response to cout:

auto start = std::chrono::steady_clock::now();
/* client code */

// printing the response to cout can slow the client
//std::cout << res << std::endl;

auto finish = std::chrono::steady_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
std::cout << "request time: " << ms.count() << "\n";

This is the client code that disables TCP delay:

boost::asio::connect(socket, results.begin(), results.end());
socket.set_option(tcp::no_delay(true));

This is the only modification of the server example code. And the server code to disable TCP delay:

void
on_accept(boost::system::error_code ec)
{
    if(ec)
    {
        fail(ec, "accept");
    }
    else
    {
        // disable TCP delay
        socket_.set_option(tcp::no_delay(true));

        // Create the http_session and run it
        std::make_shared<http_session>(
                std::move(socket_),
                doc_root_)->run();
        }

        // Accept another connection
        do_accept();
}

Here is the modified client:

//
// Copyright (c) 2016-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//

//------------------------------------------------------------------------------
//
// Example: HTTP client, synchronous
//
//------------------------------------------------------------------------------

//[example_http_client

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <cstdlib>
#include <iostream>
#include <string>
#include <chrono>

using tcp = boost::asio::ip::tcp;       // from <boost/asio/ip/tcp.hpp>
namespace http = boost::beast::http;    // from <boost/beast/http.hpp>

// Performs an HTTP GET and prints the response
int main(int argc, char** argv)
{
    try
    {
        // Check command line arguments.
        if(argc != 4 && argc != 5)
        {
            std::cerr <<
                "Usage: http-client-sync <host> <port> <target> [<HTTP version: 1.0 or 1.1(default)>]\n" <<
                "Example:\n" <<
                "    http-client-sync www.example.com 80 /\n" <<
                "    http-client-sync www.example.com 80 / 1.0\n";
            return EXIT_FAILURE;
        }
        auto const host = argv[1];
        auto const port = argv[2];
        auto const target = argv[3];
        int version = argc == 5 && !std::strcmp("1.0", argv[4]) ? 10 : 11;

        auto start = std::chrono::steady_clock::now();

        // The io_context is required for all I/O
        boost::asio::io_context ioc;

        // These objects perform our I/O
        tcp::resolver resolver{ioc};
        tcp::socket socket{ioc};

        // Look up the domain name
        auto const results = resolver.resolve(host, port);

        // Make the connection on the IP address we get from a lookup
        boost::asio::connect(socket, results.begin(), results.end());
        //socket.set_option(tcp::no_delay(true));

        // 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);

        // Send the HTTP request to the remote host
        http::write(socket, req);

        // This buffer is used for reading and must be persisted
        boost::beast::flat_buffer buffer;

        // Declare a container to hold the response
        http::response<http::dynamic_body> res;

        // Receive the HTTP response
        http::read(socket, buffer, res);

        // Write the message to standard out
        //std::cout << res << std::endl;

        // Gracefully close the socket
        boost::system::error_code ec;
        socket.shutdown(tcp::socket::shutdown_both, ec);

        // not_connected happens sometimes
        // so don't bother reporting it.
        //
        if(ec && ec != boost::system::errc::not_connected)
            throw boost::system::system_error{ec};

        // If we get here then the connection is closed gracefully
        auto finish = std::chrono::steady_clock::now();
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(finish - start);
        std::cout << "request time: " << ms.count() << "\n";

    }
    catch(std::exception const& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

//]
2
Is the machine's DNS caching set up correctly? I've seen really bad latency for things like this because the DNS name resolver was configured to lookup remotely before using the cache -- effectively making the cache worthless unless the DNS server failed to resolve.Eljay
I didn't setup any DNS caching myself, it's the default setting that Windows cames with. The problem is not the client, but the server. The client gets resonable response time from google.com, but has big delay with my local server.Laur P.
Actualy it was the DNS lookup for "localhost" string. Typing 127.0.0.1 works like a charm.Laur P.

2 Answers

4
votes

I tried running the debug version of your program against the debug version of the advanced-server example, on Visual Studio 2017 and Windows 10. This was the output:

request time: 5

I think 5 milliseconds is pretty reasonable :)

Try initializing the start time just before the call to http::read:

// Receive the HTTP response
auto start = std::chrono::steady_clock::now();
http::read(socket, buffer, res);

This way we can find out if the DNS lookup is performing slowly (a possibility).

I don't think TCP_NODELAY is going to help at all, the latency it adds is minimal and on Windows I believe the loopback device doesn't implement it. The latency you are seeing is far greater than anything Nagle's algorithm might impose.

These servers should perform with minimal latency and pretty fast results on all correctly configured platforms, I'm not sure what is going on in your particular situation but I feel confident it is an issue related to the operating system or environment.

4
votes

This is silly now thinking how easy is the fix :)

Posting here for anyone wondering what is the solution: replace "localhost" with the IP e.g. "127.0.0.1" when sending the request from the client.

Here are the new response times using 127.0.0.1 instead of "localhost".

It seems to be a Windows configuration issue. C:\Windows\System32\drivers\etc\hosts file contains

# localhost name resolution is handled within DNS itself.
#   127.0.0.1       localhost

but the line resolving "localhost" is commented out and the DNS resolution is slow.