This week I was wanted to have a go at implementing a web service with boost/asio - I've managed to get the boost web server part up and running, and I wanted to let you have a look before I get too far down the track. If you're planning on building the code that I'm going to show you, I'd recommend that first you pull down boost from http://www.boost.org/
Structure of the Sample
The sample code I'm going to present is split up into 4 sections, the first, is the main section, which is responsible for starting the program, bootstrapping the essential parts of boost/asio and listening for each session, the second section that I'm going to present will include the code for handling the broad structure of each session, and finally I'll show you the details of how sessions are handled, and the responses. In order to get the c++ code to compile, certain declarations will have to be in order, so the order that I'll present the sections for you to understand them in will be different to those that you have to present to the compiler for it to understand. Never fear though, I'm going to attach some source code for reference.
boost.asio
boost.asio subscribes to an event driven programming model called the proactor pattern, which broadly speaking is an asynchronous programming model. The idea of the proactor pattern is that you initiate an operation someone else (you don't care who) performs the operation, and in time you will be notified by a callback that indicates that the operation has completed. At that point you can initiate another operation, and pend another callback that will be notified when that operation has completed.
Modern c++ has a mechanism for tersely expressing callbacks, called a lambda function. If you've been programming for a while you've probably seen lambdas in other languages, but they are new for c++. While it's not a new concept, lambdas in c++ have a few idiosyncrasies that allow you to specify which variables to capture, and when you do so, whether it is by value or by reference. This is important in our situation because since we're going to be handling callbacks asynchronously we might have to reference values that were on the stack when the lambda was created, but are no longer in scope. A value of note in the code below is the shared pointer 'sesh'. We want a reference to the object created, but we pass the shared pointer to the function by value. Because the object is a shared pointer, we know that when the last reference goes out of scope the object will be destroyed. In this sample I'll be passing the important variables via sesh and when the last copy of this pointer goes out of scope, the destructor will clean up the connection for me.
Entry point function and session handling
the specific details of the code below are that the entry point 'main' will create an io service, spin up an endpoint and an acceptor, listen for a connection and asyncronously accept it. When the connection is accepted, the accepting lambda will queue up another accept ready for the next connection, and interact with the browser.
void accept_and_run(ip::tcp::acceptor& acceptor, io_service& io_service)
{
std::shared_ptr<session> sesh = std::make_shared<session>(io_service);
acceptor.async_accept(sesh->socket,
[sesh, &acceptor, &io_service](const error_code& accept_error)
{
accept_and_run(acceptor, io_service);
if(!accept_error)
{
session::interact(sesh);
}
});
}
int main(int argc, const char * argv[])
{
io_service io_service;
ip::tcp::endpoint endpoint{ip::tcp::v4(), 8080};
ip::tcp::acceptor acceptor{io_service, endpoint};
acceptor.listen();
accept_and_run(acceptor, io_service);
io_service.run();
return 0;
}
The io_service, endpoint, acceptor and socket are boost data types, whereas session is a class for this sample that keeps track of the state of the http conversation, which I'll describe below.
HTTP Session
I've tried to isolate most of the asynchronous behaviour of the session into it's own class. In this class we're reading each line individually, and then depending on the order of the lines, how long those lines are and what we're expecting on them. Since we're expecting Http traffic, we basically want one line containing the request line, followed by several headers, and finally a content body if a content length was specified. The code below reads each line individually and passes the lines through to the http_header class, which I'll show you in a moment. Finally if there is content, that's read too.
class session
{
asio::streambuf buff;
http_headers headers;
static void read_body(std::shared_ptr<session> pThis)
{
int nbuffer = 1000;
std::shared_ptr<std::vector<char>> bufptr =
std::make_shared<std::vector<char>>(nbuffer);
asio::async_read(pThis->socket, boost::asio::buffer(*bufptr, nbuffer),
[pThis](const error_code& e, std::size_t s)
{
});
}
static void read_next_line(std::shared_ptr<session> pThis)
{
asio::async_read_until(pThis->socket, pThis->buff, '\r',
[pThis](const error_code& e, std::size_t s)
{
std::string line, ignore;
std::istream stream {&pThis->buff};
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
pThis->headers.on_read_header(line);
if(line.length() == 0)
{
if(pThis->headers.content_length() == 0)
{
std::shared_ptr<std::string> str =
std::make_shared<std::string>(pThis->headers.get_response());
asio::async_write(
pThis->socket,
boost::asio::buffer(str->c_str(), str->length()),
[pThis, str](const error_code& e, std::size_t s)
{
std::cout << "done" << std::endl;
});
}
else
{
pThis->read_body(pThis);
}
}
else
{
pThis->read_next_line(pThis);
}
});
}
static void read_first_line(std::shared_ptr<session> pThis)
{
asio::async_read_until(pThis->socket, pThis->buff, '\r',
[pThis](const error_code& e, std::size_t s)
{
std::string line, ignore;
std::istream stream {&pThis->buff};
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
pThis->headers.on_read_request_line(line);
pThis->read_next_line(pThis);
});
}
public:
ip::tcp::socket socket;
session(io_service& io_service)
:socket(io_service)
{
}
static void interact(std::shared_ptr<session> pThis)
{
read_first_line(pThis);
}
};
You'll note some other weird things about this class, firstly all of the methods are static, and I'm passing the this parameter in manually. I'm doing this in order to preserve the reference counts of the sessions. I know I don't want to lose this because when the object goes out of scope, so will the socket, and if that goes away it will be bad for business in our web server. Secondly since all of the methods are asynchronous, each method also has a lambda involved, which is the function that will be executed when the operation completes. Listing the code this way makes the method and it's consequence more readable without destroying it's ability to execute later.
Serving up pages
Ok, here's the final bit of the server - serving up the pages themselves. I've included a minimal service this time, but gone to the effort of serializing each result individually for readability. There's the root, which is basically a hello world page. An icon 'favicon.ico' which is a green and red square, and a 404 status page. You'll note that this section isn't asynchronous, and it doesn't have to be, basically in this case all I'm doing is writing to a string, which the previous section will send away.
unsigned char * get_icon(int* pOutSize);
class session;
class http_headers
{
std::string method;
std::string url;
std::string version;
std::map<std::string, std::string> headers;
public:
std::string get_response()
{
std::stringstream ssOut;
if(url == "/favicon.ico")
{
int nSize = 0;
unsigned char* data = get_icon(&nSize);
ssOut << "HTTP/1.1 200 OK" << std::endl;
ssOut << "content-type: image/vnd.microsoft.icon" << std::endl;
ssOut << "content-length: " << nSize << std::endl;
ssOut << std::endl;
ssOut.write((char*)data, nSize);
}
else if(url == "/")
{
std::string sHTML =
"<html><body><h1>Hello World</h1><p>This is a web server in c++</p></body></html>";
ssOut << "HTTP/1.1 200 OK" << std::endl;
ssOut << "content-type: text/html" << std::endl;
ssOut << "content-length: " << sHTML.length() << std::endl;
ssOut << std::endl;
ssOut << sHTML;
}
else
{
std::string sHTML =
"<html><body><h1>404 Not Found</h1><p>There's nothing here.</p></body></html>";
ssOut << "HTTP/1.1 404 Not Found" << std::endl;
ssOut << "content-type: text/html" << std::endl;
ssOut << "content-length: " << sHTML.length() << std::endl;
ssOut << std::endl;
ssOut << sHTML;
}
return ssOut.str();
}
int content_length()
{
auto request = headers.find("content-length");
if(request != headers.end())
{
std::stringstream ssLength(request->second);
int content_length;
ssLength >> content_length;
return content_length;
}
return 0;
}
void on_read_header(std::string line)
{
std::stringstream ssHeader(line);
std::string headerName;
std::getline(ssHeader, headerName, ':');
std::string value;
std::getline(ssHeader, value);
headers[headerName] = value;
}
void on_read_request_line(std::string line)
{
std::stringstream ssRequestLine(line);
ssRequestLine >> method;
ssRequestLine >> url;
ssRequestLine >> version;
std::cout << "request for resource: " << url << std::endl;
}
};
Summing Up
Ok, well, that's all there is to it. I hadn't used boost.asio before today, but I found it pretty easy to work with. The documentation is a little bit cryptic, but I'm sure most of these things become more clear the more you use them. The thing that I like about this software is how easily you can get the asynchronous stuff to work without handling threads and things and all of that plumbing behind.
Thanks for reading, and if you have any comments I'd love to hear from you.
This article was originally posted here: https://dabblingseriously.wordpress.com/2015/07/06/a-minimal-http-web-server-using-boostasio/
P.S: The icon data
here's the icon code in case you're really interested - it's just a green box with a red border.
unsigned char icon_data[] = {
0x00,0x00,
0x01,0x00,
0x01,0x00,
0x10,0x10,
0x00,
0x00,
0x01,0x00,
0x20,0x00,
0x28,0x04,0x00,0x00,
0x16,0x00,0x00,0x00,
0x28,0x00,0x00,0x00, 0x10,0x00,0x00,0x00, 0x20,0x00,0x00,0x00,
0x01,0x00, 0x20,0x00,
0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF ,0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF,
0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF,
0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF,
0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF,
0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF,
0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF,
0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0x00,0xFF,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF,
0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF, 0x00,0xFF,0x00,0xFF
};
unsigned char* get_icon(int* pOut)
{
*pOut = sizeof(icon_data);
return icon_data;
}