I've been developing an audio fingerprint query server on Ubuntu 14.04 and have chosen
cpp-netlib as the HTTP library used to receive and respond to queries. I chose cpp-netlib because it seemed like it had a very simple programming interface, a lot like the
tornado framework and
requests library I favor in Python. You basically instantiate a class from a server template, implement a special operator() method, and read and write from the parameters to that operator() method.
In the course of doing that, I ran into some bumps while getting my code to work, and the
official documentation for cpp-netlib was lacking for my needs. This was disappointing, and the fact that there seems to have been little recent activity on its
github page, makes me question my choice of library.
However, after some digging online, I eventually resolved all my problems, so I thought I would share my findings here so others might benefit. Note that the code below is
using namespace std and #includes <string> and <array>.
Declarations
Here's a portion of the header file for my query daemon class:
#include <boost/network/protocol/http/server.hpp>
class QueryDaemon;
#define ASYNC_SERVER
#ifdef ASYNC_SERVER
typedef boost::network::http::async_server<QueryDaemon> HTTPServer;
#else
typedef boost::network::http::server<QueryDaemon> HTTPServer;
#endif
class QueryDaemon
{
public:
#ifdef ASYNC_SERVER
void operator() (
HTTPServer::request const& request,
HTTPServer::connection_ptr connection);
#else
void operator() (
HTTPServer::request const& request,
HTTPServer::response& response);
#endif
// . . .
};
The key aspect of this is the typedef for HTTPServer, which is used extensively in the implementation.
Accessors
The cpp-netlib headers make extensive use of template metaprogramming, which can be confusing to the novice. The declarations are often very complicated, though usage is meant to be simple. To get useful information from the request, cpp-netlib provides some template functions that can be used as accessors (
wrappers, in their terminology):
string ip_addr = source(request);
string uri = destination(request);
string payload = body(request);
From what I can tell, the advantage of using these accessors rather than adding simple getters to the request interface is that they provide the same encapsulation of the details for extracting information from a type, but do not require modification to the interface for that type. This assumes that the existing interface for the type is sufficient to extract the needed data.
Synchronous Servers
The response object used in the synchronous server has a stock_reply method which makes it easy to return status codes to the client. The codes themselves are in the scope of the response object, so you would use code like HTTPServer::response::ok or HTTPServer::response::internal_server_error to use them.
It's not well documented, but the response object also has a headers container into which you can add individual key/value header pairs, using an STL-standard method like push_back. The following code demonstrates all of this:
void QueryDaemon::operator() (
HTTPServer::request const& request,
HTTPServer::response& response)
{
// extract useful information from the request
string ip_addr = source(request);
string uri = destination(request);
string payload = body(request);
string result;
bool success = processQuery(payload, uri, ip_addr, result);
if (success)
{
response = HTTPServer::response::stock_reply(
HTTPServer::response::ok,
result);
HTTPServer::response_header content_type;
content_type.name = "Content-Type";
content_type.value = "application/json";
response.headers.push_back(content_type);
}
else
{
response = HTTPServer::response::stock_reply(
HTTPServer::response::internal_server_error);
}
}
Note that we are assigning to the response object itself, rather than invoking a method on it, or returning a new response object from the handler function.
Asynchronous Servers
Unlike synchronous servers, asynchronous servers do not have a response object. Instead, there is a connection object that you use to respond to the client. As far as I can tell, this connection object is not well documented, but is critical to the operation of asynchronous servers.
To return a status code, invoke the set_status method on the connection object, passing a result code in the scope of the connection object: connection->set_status(HTTPServer::connection::ok);
To set a key/value pair to the response headers, invoke the set_headers method on the connection object. Note that this method takes an object supporting the boost Single Pass Range concept, which means something with begin, end, and increment methods, like an iterator. A C++11 std::array can be used here. See the example below for some code.
Lastly, in asynchronous servers the order in which you set the result code, headers, and response body is critical. The result code must be set first, followed by the headers, followed by the response body. This implies that you must fully compute the response body before setting the result code. This is a little confusing, because you can write the response body in many chunks with the write method of the connection object. It is not clear how one should handle errors when writing multiple chunks after the result code has been set.
Here's some code for an asynchronous handler that demonstrates all this:
void QueryDaemon::operator() (
HTTPServer::request const& request,
HTTPServer::connection_ptr connection)
{
// extract useful information from the request
string ip_addr = source(request);
string uri = destination(request);
string payload = readBody(request, connection); // defined later
string result;
bool success = processQuery(payload, uri, ip_addr, result);
if (success)
{
connection->set_status(HTTPServer::connection::ok);
array<HTTPServer::response_header, 1> headers =
{
{ "Content-Type", "application/json" }
};
connection->set_headers(headers);
connection->write(result);
}
else
{
connection->set_status(HTTPServer::connection::internal_server_error);
}
}
Note that the technique for reading the body of the request (readBody) needs to be done asynchronously, and will be defined in a later edit or post.
Linking
Even though cpp-netlib is mostly a header-only library, you still need to link with it, especially if you are making an asynchronous server. It also uses boost heavily internally, so you'll need to link with that, too. Here are the relevant parts of my CMakeLists.txt file showing what I had to do to get my code to compile and link:
cmake_minimum_required(VERSION 2.8)
project(MY_PROJECT)
find_package(Boost REQUIRED system thread)
set(EXTRA_CXX_FLAGS "-std=c++0x")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${EXTRA_CXX_FLAGS}")
add_executable(query_daemon QueryDaemon.cpp)
target_link_libraries(query_daemon ${Boost_LIBRARIES} cppnetlib-server-parsers)
Performance
Once I had the basics down, I did some small load tests on both the synchronous and asynchronous versions of my server. The server merely returned a "no match" result, and did not perform any actual audio fingerprint lookups, so it was really a test of the overhead of cpp-netlib in both synchronous and asynchronous modes.
My test code was a C++ program that launched 32 query threads at once. Each query thread composed a query, fired it at the server (running on localhost) using libcurl, and reported how long it waited for the response. The test machine was an 8-core Intel Xeon E3-1270 v3 running at 3.5 GHz with 16 GB RAM.
With the synchronous server, most of the queries were handled in 1.01 seconds, with some of them taking more like 2.01 seconds. The results were bimodal: either 1.01 seconds or 2.01 seconds. With the asynchronous server, all the queries were handled in about 0.02 seconds or less.
I'm not sure how to take these results, since the bimodality of the synchronous server is weird and unexpected. I noticed this earlier when a different C++ test program used popen to launch a curl command to issue the query. If I used popen and curl, there was a 1 second delay before my handler code was executed. If I invoked curl directly from the command line, there was no one second delay and the response was more or less instant.
It is interesting, however, that if you look just at the amount of time spent in the handler function itself, the synchronous server is much faster: around 80 microseconds versus 280 for the asynchronous handler function. This agrees with the official documentation, which states:
If your application does not need to write out information asynchronously or perform potentially long computations, then the synchronous server gives a generally better performance profile than the asynchronous server.
Ultimately, I plan to use the asynchronous model because the audio fingerprint queries can often take hundreds of milliseconds of pure computation, and I want to gracefully handle lots of concurrent connections.
Labels: c++, cpp-netlib, linux