Our goal here will be to developed a full FastCGI application that provides both HTML and image data to clients. We will utilize caching facilities along with some localization methods. Concepts covered include:
- Outputting non-HTML data.
- Using fastcgi++'s localization functionality to provide content to an international audience.
- Implementing caching to provide dramatic performance increases to clients.
You can build it with
make gnu.fcgi
Walkthrough
First we'll define our request class.
First in our request class we'll need a static vector of strings to define what locales we're going to support. This will become more clear later on.
static const std::vector<std::string> locales;
Next we need a static two-dimensional vector of strings to define the textual data we'll be sending to the client as it relates to the requested locality. This will, as well, become more clear later on.
static const std::vector<std::vector<std::wstring>> catalogues;
This is simply a static declaration of the image data we will be providing.
static const unsigned char gnuPng[];
static const size_t gnuPngSize = 58587;
We'll keep a timestamp of when the FastCGI application was actually started for caching purposes. Unfortunately we need both std::tm and std::time_t objects defined because of the dumbass lack of thread-safety with std::gmtime().
static const std::time_t startTimestamp;
static const std::tm startTime;
Let's define an inline function to handle the outputting of the image.
Here's where we decide if the client has a new enough version of this image. Fastcgipp::Http::Environment::ifModifiedSince is set by the client to tell us when their version of this resource was last modified (which in turn comes from us). We compare to our start time to decide.
if(startTimestamp <= environment().ifModifiedSince)
out << "Status: 304 Not Modified\r\n\r\n";
If their version isn't new enough, however, we must send them one. Notice the extra header line where we specify when the image was last modified.
else
{
out << "Last-Modified: "
<< std::put_time(&startTime, L"%a, %d %b %Y %H:%M:%S GMT\n");
out << L"Content-Length: " << gnuPngSize << '\n';
out << L"Content-Type: image/png\r\n\r\n";
dump(gnuPng, gnuPngSize);
}
}
Here we define an inline function to handle the outputting of the HTML data.
This is where we decide what language/locale the client will get. We use Fastcgipp::Request::pickLocale() for this. It will cross-reference the vector of locales we support with the list of languages/regions that the client wants and selects the best option. What we get back is an index value of the locale in our vector. We use this index to create a two character language code (you'll see why later) and a reference to the message catalogue our client prefers.
const unsigned locale = pickLocale(locales);
const std::wstring language(
locales[locale].cbegin(),
locales[locale].cbegin()+2);
const std::vector<std::wstring>& catalogue(catalogues[locale]);
Now we decide whether or not the client has an up-to-date version of the HTML data. Notice this time around we use more than just the ifModifiedSince value. In this case we use the HTTP Etag value to represent our locale. This ensures that cached versions of the HTML data do not prevent people from getting the localized version they prefer.
if(
locale==environment().etag
&& startTimestamp<=environment().ifModifiedSince)
out << "Status: 304 Not Modified\r\n\r\n";
If the client doesn't have a valid cached version we'll need to send them something. Notice the HTTP header has a few new things in it. We set the Etag to our locale index value and we set the content language.
else
{
out << "Last-Modified: "
<< std::put_time(&startTime, L"%a, %d %b %Y %H:%M:%S GMT\n");
out << L"Etag: " << locale << '\n';
out << L"Content-Type: text/html; charset=utf-8\n";
out << L"Content-Language: " << language << L"\r\n\r\n";
Now we set the locale of the output stream. We do this to ensure things like numbers, dates, times, money, etc. show up the way the client wants them. Make sure not to do this before the HTTP header is outputted as it'll mess stuff up.
setLocale(locales[locale]);
Alright, let's output the actual HTML now. Notice how all language specific text come from the message catalogue. This greatly simplifies the internationalization process.
out <<
L"<!DOCTYPE html>\n"
L"<html lang='" << language << L"'>"
L"<head>"
L"<meta charset='utf-8' />"
L"<title>fastcgi++: " << catalogue[0] << L"</title>"
L"</head>"
L"<body>"
L"<h1>" << catalogue[1] << L"</h1>"
L"<figure>"
L"<img src='" << environment().scriptName << L"/gnu.png' alt='"
<< catalogue[2] << L"'>"
L"<figcaption>" << catalogue[3] << gnuPngSize
<< catalogue[4] << std::put_time(&startTime, L"%c")
<< L". </figcaption>"
L"</figure>"
L"</body>"
L"</html>";
}
}
And now our response() function. We use the path info to decide whether or not to output image or HTML data.
bool response()
{
if(
environment().pathInfo.size() == 1
&& environment().pathInfo[0] == L"gnu.png")
image();
else
html();
return true;
}
};
We, of course, need to actually define all that static stuff we declared in the class. I won't explain any of this but look it over as it should be fairly self-explanatory.
const std::vector<std::string> Gnu::locales
{
"en_CA",
"en_US",
"fr_CA",
"fr_FR",
"zh_CN",
"de_DE"
};
const std::vector<std::vector<std::wstring>> Gnu::catalogues
{
{
L"Showing the colourless GNU",
L"This is a header",
L"The GNU Logo",
L"Figure 1: This GNU logo is ",
L" bytes. It was last modified "
},
{
L"Showing the colorless GNU",
L"This is a header",
L"The GNU Logo",
L"Figure 1: This GNU logo is ",
L" bytes. It was last modified "
},
{
L"Montrant le GNU incolore",
L"Ceci est un en-tête",
L"Le logo GNU",
L"Figure 1: Ce logo GNU est de ",
L" octets. Il a été modifié "
},
{
L"Montrant le GNU incolore",
L"Ceci est un en-tête",
L"Le logo GNU",
L"Figure 1: Ce logo GNU est de ",
L" octets. Il a été modifié "
},
{
L"顯示無色GNU",
L"這是一個標頭",
L"GNU的標誌",
L"圖1 :這GNU標誌是",
L"字節。最後一次修改"
},
{
L"Angezeigt wird die farblose GNU",
L"Dies ist ein Kopf",
L"Das GNU- Logo",
L"Abbildung 1: Das GNU -Logo ist ",
L" Bytes. Es wurde zuletzt geändert "
}
};
const unsigned char Gnu::gnuPng[] =
#include "gnu.png.hpp"
const std::time_t Gnu::startTimestamp = std::time(nullptr);
const std::tm Gnu::startTime = *std::gmtime(&startTimestamp);
And the rest is all familiar
Full Source Code
#include <iomanip>
{
static const std::vector<std::string> locales;
static const std::vector<std::vector<std::wstring>> catalogues;
static const unsigned char gnuPng[];
static const size_t gnuPngSize = 58587;
static const std::time_t startTimestamp;
static const std::tm startTime;
inline void image()
{
out << "Status: 304 Not Modified\r\n\r\n";
else
{
out << "Last-Modified: "
<< std::put_time(&startTime, L"%a, %d %b %Y %H:%M:%S GMT\n");
out << L"Content-Length: " << gnuPngSize << '\n';
out << L"Content-Type: image/png\r\n\r\n";
dump(gnuPng, gnuPngSize);
}
}
inline void html()
{
const std::wstring language(
locales[locale].cbegin(),
locales[locale].cbegin()+2);
const std::vector<std::wstring>& catalogue(catalogues[locale]);
if(
out << "Status: 304 Not Modified\r\n\r\n";
else
{
out << "Last-Modified: "
<< std::put_time(&startTime, L"%a, %d %b %Y %H:%M:%S GMT\n");
out << L"Etag: " << locale << '\n';
out << L"Content-Type: text/html; charset=utf-8\n";
out << L"Content-Language: " << language << L"\r\n\r\n";
out <<
L"<!DOCTYPE html>\n"
L"<html lang='" << language << L"'>"
L"<head>"
L"<meta charset='utf-8' />"
L"<title>fastcgi++: " << catalogue[0] << L"</title>"
L"</head>"
L"<body>"
L"<h1>" << catalogue[1] << L"</h1>"
L"<figure>"
L
"<img src='" <<
environment().scriptName << L
"/gnu.png' alt='" << catalogue[2] << L"'>"
L"<figcaption>" << catalogue[3] << gnuPngSize
<< catalogue[4] << std::put_time(&startTime, L"%c")
<< L". </figcaption>"
L"</figure>"
L"</body>"
L"</html>";
}
}
{
if(
image();
else
html();
return true;
}
};
const std::vector<std::string> Gnu::locales
{
"en_CA",
"en_US",
"fr_CA",
"fr_FR",
"zh_CN",
"de_DE"
};
const std::vector<std::vector<std::wstring>> Gnu::catalogues
{
{
L"Showing the colourless GNU",
L"This is a header",
L"The GNU Logo",
L"Figure 1: This GNU logo is ",
L" bytes. It was last modified "
},
{
L"Showing the colorless GNU",
L"This is a header",
L"The GNU Logo",
L"Figure 1: This GNU logo is ",
L" bytes. It was last modified "
},
{
L"Montrant le GNU incolore",
L"Ceci est un en-tête",
L"Le logo GNU",
L"Figure 1: Ce logo GNU est de ",
L" octets. Il a été modifié "
},
{
L"Montrant le GNU incolore",
L"Ceci est un en-tête",
L"Le logo GNU",
L"Figure 1: Ce logo GNU est de ",
L" octets. Il a été modifié "
},
{
L"顯示無色GNU",
L"這是一個標頭",
L"GNU的標誌",
L"圖1 :這GNU標誌是",
L"字節。最後一次修改"
},
{
L"Angezeigt wird die farblose GNU",
L"Dies ist ein Kopf",
L"Das GNU- Logo",
L"Abbildung 1: Das GNU -Logo ist ",
L" Bytes. Es wurde zuletzt geändert "
}
};
const unsigned char Gnu::gnuPng[] =
#include "gnu.png.hpp"
const std::time_t Gnu::startTimestamp = std::time(nullptr);
const std::tm Gnu::startTime = *std::gmtime(&startTimestamp);
int main()
{
return 0;
}