Introduction
Today for my coding project I want to do a crossover of my previous two projects. This time I’m going to run up a web server like before, however today I’m going to show you how you can use JSON.h to expose charting data to a web page and draw a chart in the browser.
Stuff I'll be using
Highcharts
The new web is awash with several javascript frameworks for various new and exciting applications, and one of the applications that people have been getting really excited about is charting. This framework is typically easy to download and install on your website (you simply download the file). You can read about the highcharts offering here: Highcharts
JSON.h
JSON.h is a project of mine that I've previously written about, you can have a look at the source here: https://github.com/PhillipVoyle/json_h. It's a serialization framework for C++ and JSON - let me know if you like it? or hate it. I'm going to be using it my example today.
boost.asio
Boost is the leading portable non standard source of portable C++ libraries, their aim is to be ahead of the ball and prove technologies prior to standardisation. Today I'll be using their asynchronous i/o library that has excellent support for asynchronous operations and you can write very high throughput network applications using this system. If you're interested in reading more about boost.asio you can read it here: http://www.boost.org/doc/libs/1_58_0/doc/html/boost_asio.html
Web Page Source
Before I get too far into the project I'm going to show you the source code for the page. For my purposes I'm just going to be calling into a web server hosted in a console app on my workstation, so the json data is going to be fetched from localhost, but for most applications you're going to want to expose your domain name here. Ok, so here it is:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>
Chart
</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="jquery-1.9.1.js" type="text/javascript"></script>
<script src="highcharts.js" type="text/javascript"></script>
<script src="exporting.js" type="text/javascript"></script>
<script type="text/javascript">
$(function() {
$.getJSON('http://localhost:8080/Chart.json',
function(data){
$('#container').highcharts(data);
});
});
</script>
</head>
<body>
<div id="container" style="min-width: 800px; height: 800px; margin: 0 auto"></div>
</body>
</html>
Nice! Ok, it's really just the boiler-plate of a basic web page with a function that says: go get my chart data from over here. You could serve up a static page, but I'm going to be serving up JSON data serialised from an object representing the graph data. You could generate this from the fly but I'm going to be hard coding it for clarity.
The C++ web server
As I said before, I'm going to be using boost.asio to serve up the JSON document required to draw the graph. You could do this potentially any kind of data, but realtime stats of your systems operations or service status and the like might be the kinds of things that would be most useful. A web page is handy place to expose management objects. Anyhow here's the source of the app.
The graph classes and JSON.h Stubs
The classes I've written here are meant to fall into a compact representation of the graph, that will then be rendered to JSON, and collected by the web page above. Like I said earlier, I'm currently hard coding the graph data, but you needn't do this. The data is from the national institute of water and atmospheric research in New Zealand.
class ChartText
{
public:
string text;
int x;
};
class ChartXAxis
{
public:
std::vector<std::string> categories;
};
class PlotLine
{
public:
int value;
int width;
string color;
};
class ChartYAxis
{
public:
ChartText title;
std::vector<PlotLine> plotLines;
};
class Legend
{
public:
string layout;
string align;
string verticalAlign;
int borderWidth;
};
class DataSeries
{
public:
string name;
std::vector<float> data;
};
class ChartData
{
public:
ChartText title;
ChartText subtitle;
ChartXAxis xAxis;
ChartYAxis yAxis;
Legend legend;
std::vector<DataSeries> series;
};
BEGIN_CLASS_DESCRIPTOR(ChartText)
CLASS_DESCRIPTOR_ENTRY(text)
CLASS_DESCRIPTOR_ENTRY(x)
END_CLASS_DESCRIPTOR()
BEGIN_CLASS_DESCRIPTOR(ChartXAxis)
CLASS_DESCRIPTOR_ENTRY(categories)
END_CLASS_DESCRIPTOR()
BEGIN_CLASS_DESCRIPTOR(PlotLine)
CLASS_DESCRIPTOR_ENTRY(value)
CLASS_DESCRIPTOR_ENTRY(width)
CLASS_DESCRIPTOR_ENTRY(color)
END_CLASS_DESCRIPTOR()
BEGIN_CLASS_DESCRIPTOR(ChartYAxis)
CLASS_DESCRIPTOR_ENTRY(title)
CLASS_DESCRIPTOR_ENTRY(plotLines)
END_CLASS_DESCRIPTOR()
BEGIN_CLASS_DESCRIPTOR(Legend)
CLASS_DESCRIPTOR_ENTRY(layout)
CLASS_DESCRIPTOR_ENTRY(align)
CLASS_DESCRIPTOR_ENTRY(verticalAlign)
CLASS_DESCRIPTOR_ENTRY(borderWidth)
END_CLASS_DESCRIPTOR()
BEGIN_CLASS_DESCRIPTOR(DataSeries)
CLASS_DESCRIPTOR_ENTRY(name)
CLASS_DESCRIPTOR_ENTRY(data)
END_CLASS_DESCRIPTOR()
BEGIN_CLASS_DESCRIPTOR(ChartData)
CLASS_DESCRIPTOR_ENTRY(title)
CLASS_DESCRIPTOR_ENTRY(subtitle)
CLASS_DESCRIPTOR_ENTRY(xAxis)
CLASS_DESCRIPTOR_ENTRY(yAxis)
CLASS_DESCRIPTOR_ENTRY(legend)
CLASS_DESCRIPTOR_ENTRY(series)
END_CLASS_DESCRIPTOR()
ChartData g_chartData = {
{"Mean Monthly Temperatures (°C)", -20},
{"Source: www.niwa.co.nz", 0},
{{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}},
{{"Temperature (°C)"}, {{0, 1, "#808080"}}},
{"vertical", "right", "middle", 0},
{
{"Auckland", {19.1,19.7,18.4,16.1,14.0,11.8,10.9,11.3,12.7,14.2,15.7,17.8}},
{"Tauranga", {19.4,19.6,18.0,15.5,13.2,10.8,10.2,10.7,12.3,13.9,15.8,18.0}},
{"Hamilton", {18.4,18.8,17.1,14.5,11.9,9.5,8.9,9.8,11.6,13.2,14.9,16.9}},
{"Taupo", {17.0,17.1,14.9,12.0,9.4,7.4,6.5,7.2,9.2,11.1,13.1,15.6}},
{"Wellington", {16.9,17.2,15.8,13.7,11.7,9.7,8.9,9.4,10.8,12.0,13.5,15.4}},
{"Christchurch", {17.5,17.2,15.5,12.7,9.8,7.1,6.6,7.9,10.3,12.2,14.1,16.1}},
{"Dunedin", {15.3,15.0,13.7,11.7,9.3,7.3,6.6,7.7,9.5,10.9,12.4,13.9}},
{"Antarctica, Scott Base", {-4.5,-11.6,-20.6,-24.2,-25.7,-26.0,-28.7,-30.0,-27.6,-20.8,-11.3,-4.5}}
}
};
Headers and Application Code
Ok, so I'm going to be using a similar web server to the one that I wrote a few weeks ago, but I've refined it to use two handy features, namely boost::bind and std::shared_from_this(). If you'd read my previous post you will have seen me use some lambdas that are no longer required here. For starters I'm just going to show you the headers and response code. As you can see below, there's only one sensible url /Chart.json and that returns the chart data with the text/json mime type. Another point of interest is the keepalive behaviour, which allows the session to continue to be used as long as the browser is still available.
class http_headers
{
std::string method;
std::string url;
std::string version;
int nRequests = 0;
typedef std::map<std::string, std::string, nocase_less> map_t;
map_t headers;
public:
http_headers()
{
}
std::string get_response()
{
std::stringstream ssOut;
if((url == "/Chart.json") && (method == "GET"))
{
g_currentStatus.hitCount ++;
std::string sJSON = ToJSON(g_chartData);
ssOut << "HTTP/1.1 200 OK" << std::endl;
ssOut << "content-type: text/json" << std::endl;
ssOut << "content-length: " << sJSON.length() << std::endl;
ssOut << std::endl;
ssOut << sJSON;
}
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;
}
bool keepAlive()
{
auto connection = headers.find("connection");
if(connection != headers.end())
{
return nocase_eq(connection->second, "keep-alive");
}
return false;
}
void on_read_header(const std::string& line)
{
std::stringstream ssHeader(line);
std::string headerName;
std::getline(ssHeader, headerName, ':');
boost::algorithm::trim_left(headerName);
boost::algorithm::trim_right(headerName);
std::string value;
std::getline(ssHeader, value);
boost::algorithm::trim_left(value);
boost::algorithm::trim_right(value);
headers[headerName] = value;
}
void on_read_request_line(const std::string& line)
{
std::stringstream ssRequestLine(line);
ssRequestLine >> method;
ssRequestLine >> url;
ssRequestLine >> version;
std::cout << "request for resource: " << url << std::endl;
}
void clear()
{
method = "";
url = "";
version = "";
headers.clear();
}
void on_read_line(const std::string& line)
{
if((method == "") || (url == "") || (version == ""))
{
clear();
on_read_request_line(line);
}
else
{
on_read_header(line);
}
}
};
Entry point and session class
Again below you can see that I've improved the behaviour of the class from last week, this week it's much briefer and you will note the use of shared_from_this(). The code below spins up a session and waits for a connection. When a connection arrives, it initiates a new connection, and begins interacting with the connected party line by line.
class session: public enable_shared_from_this<session>
{
public:
http_headers headers;
tcp::socket socket;
boost::asio::streambuf buf;
session():socket(iosrv)
{
std::cout << "new session (idle)" << std::endl;
}
~session()
{
std::cout << "destroy session" << std::endl;
}
void complete_write(
const boost::system::error_code& ec,
std::size_t bytes,
shared_ptr<std::string> buf)
{
if(headers.keepAlive())
{
headers.clear();
begin_read();
}
}
void begin_write(std::string string)
{
auto str = std::make_shared<std::string>(string);
async_write(
socket,
boost::asio::buffer(*str),
bind(
&session::complete_write,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred,
str));
}
void complete_read(const boost::system::error_code& ec, std::size_t bytes)
{
if(bytes > 0)
{
std::string line, ignore;
std::istream stream {&buf};
std::getline(stream, line, '\r');
std::getline(stream, ignore, '\n');
if(line == "")
{
begin_write(headers.get_response());
}
else
{
headers.on_read_line(line);
begin_read();
}
}
}
void begin_read()
{
async_read_until(
socket,
buf,
'\n',
bind(
&session::complete_read,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
static void initiate()
{
auto sesh = make_shared<session>();
sesh->begin_accept(acceptor);
}
void complete_accept(const boost::system::error_code& ec)
{
std::cout << "accept session" << std::endl;
initiate();
begin_read();
}
void begin_accept(tcp::acceptor& acceptor)
{
acceptor.async_accept(socket,
bind(
&session::complete_accept,
shared_from_this(),
boost::asio::placeholders::error));
}
};
int main(int argc, const char * argv[]) {
acceptor.listen();
session::initiate();
iosrv.run();
return 0;
}
The rest at the top
Here's the rest of the file, listed at the top - If you want to code this yourself you will need these things too, but they're not really essential parts. The stuff here is are the includes and some handy things for making my map case preserving, but insensitive.
#include <boost/asio.hpp>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/algorithm/string.hpp>
#include <json/JSON.h>
#include <memory>
#include <vector>
using namespace boost::system;
using namespace boost::asio;
using namespace boost::asio::ip;
using namespace std;
using namespace boost::asio::placeholders;
io_service iosrv;
tcp::endpoint ep(ip::address::from_string("127.0.0.1"), 8080);
tcp::acceptor acceptor(iosrv, ep);
bool nocase_compare_char(const unsigned char& c1, const unsigned char& c2)
{
return tolower (c1) < tolower (c2);
}
bool nocase_eq(const std::string& s1, const std::string& s2)
{
return std::lexicographical_compare
(s1.begin (), s1.end (), s2.begin (), s2.end (), [](unsigned char a , unsigned char b){return tolower(a) == tolower(b);}); }
bool nocase_compare(const std::string& s1, const std::string& s2)
{
bool bResult = std::lexicographical_compare
(s1.begin (), s1.end (), s2.begin (), s2.end (), nocase_compare_char);
return bResult;
}
struct nocase_less : std::binary_function<std::string, std::string, bool>
{
bool operator() (const std::string & s1, const std::string & s2) const
{
return nocase_compare(s1, s2); }
};
The Output
Here's what you will see - I haven't done much customisation of the charts - That's something for you to experiment with :-)
Summary and Parting thoughts
You could use this kind of approach for more than just graphing - My favourite daydream about this kind of thing would be in memory management objects that you could view and edit via a web tool instead of writing a UI. A particular object's status, things like that. I'm keen to know your thoughts. Next time I'm going to take a break from serialisation, and write about something a little different.
Thanks for reading.
This article was originally posted here: https://dabblingseriously.wordpress.com/