C++ CGI und mySQL

Ein CGI-Programm in C++ zu schreiben ist – eigentlich – ziemlich einfach:

#include 

using namespace std;

int main()
{
	cout << "Content-Type: text/html" << endl << endl;
	
	cout << "Hello CGI" << endl;
	cout << "

C++ says Hi to the CGI API

" << endl; cout << "" << endl; return 0; }

Das ganze kann man jetzt durch einen C++ Compiler jagen, in das cgi-bin Verzeichniss des Webservers verschrieben, chmod +x, und dann zB unter http://webserver/cgi-bin/test.cgi ansehen. Nicht sehr spannend, da man diese statische Seite ja auch direkt in HTML schreiben hätte können und sich den Umweg über CGI sparen hätte können.

Interessant wird es also, wenn man mit C++ über CGI dynamische Webseiten (dynamisch nicht im Sinne von JavaScript, sondern eher ähnlich wie PHP) generieren möchte. Ein erster Schritt wäre hier, die GET-Variablen auszulesen. Die GET-Variablen werden von der CGI in die Environment-Variable QUERY_STRING geschrieben, und zwar in folgendem Format:
[key1]=[value1]&[key2]=[value2]&....&[keyN]=[valueN]
Wir müssen uns also eine Funktion zum Parsen dieses Strings schreiben:

void getVariables(std::unordered_map &variables)
{
	// fetch the query string
	char* raw_query_string = getenv("QUERY_STRING");

	// query string empty?
	if (raw_query_string == NULL) 
	{
		variables.clear();
		return variables;
	}
	
	// key and value into a stringstream, init with an empty stream
	// to make sure get-queries such as ?step=1&confirm are parsed 
	// correctly
	std::stringstream key, value;

	key.str(std::string(""));
	value.str(std::string(""));

	// are we currently parsing a key or a value?
	bool isKey = true;

	// loop until we read all raw data
	while (*raw_query_string != '')
	{
		// if we read a &, we'll change or type to key and store 
		// are previous key and value variables into out map
		// and clear them
		if (*raw_query_string == '&') 
		{ 
			variables[key.str()] = value.str();
			isKey = true; 
			key.str(std::string(""));
			value.str(std::string(""));
		}
		// if we read a =, we'll change or type to value
		else if (*raw_query_string == '=') 
		{ 
			isKey = false; 
		}
		// anything else goes into the currently active stream
		else 
		{
			if (isKey) { key << *raw_query_string; }
			else { value << *raw_query_string; }
		}
		// move pointer to next char
		raw_query_string++;
	}
	
	// store last key value pair, because query-strings 
	// normaly don't end on &
	variables[key.str()] = value.str();
	
	return variables;
}

Das Problem ist, dass die keys und values URL-codiert sind, wenn man also Text verarbeiten möchte muss man sie noch entsprechend dekodieren. Das ist aber an dieser Stelle erstmal Aufgabe des Lesers 😉

Natürlich möchte man jetzt nicht nur GET-Variablen lesen, sondern auch Daten empfangen können, die per POST geschickt werden. Diese schickt der WebServer an stdin des CGI-Scripts. Und leider nicht -terminiert, aber das ist kein Problem, da die Umgebungsvariable CONTENT_LENGTH die genaue Länge der POST-Daten enthält. Die POST Daten sind in gleichen Format wie die GET-Daten. Das Parsen funktioniert also entsprechend ähnlich, allerdings muss man erst beispielsweise per fread die Daten aus dem stdin in ein char-buffer lesen und -terminieren bevor man sie durch entsprechende parsing Funktion schicken kann.

Mit diesen zwei Funktionalitäten kann man schon eine Menge anstellen (wie zB ein Datei-basiertes Gästebuch), allerdings muss man beim Ausgeben von Benutzereingaben daran denken diese Entsprechend zu Maskieren, damit man keine XSS-Attacken und Ähnliches erlaubt. Dafür habe ich mir folgende kleine Hilfsfunktion geschrieben:

std::string htmlspecialchars(std::string input)
{
	replaceAll("&", "&", input);

	replaceAll("", ">", input);

	replaceAll("'", "'", input);
	replaceAll(""", """, input);

	return input;
}

void replaceAll(std::string search, std::string replace, std::string &str)
{
	size_t currPos = 0;
	size_t findPos;
	while((findPos = str.find(search, currPos)) != std::string::npos) 
	{
		str.replace(findPos, search.size(), replace);
		currPos = findPos + replace.size();
	}
}

Jetzt wird’s entlich interessant: Wir möchten in unserer C++ CGI-Anwendung natürlich auch (wie gewohnt aus PHP) eine mySQL-Datenbank ansprechen. Ich habe zuerst ziemlich lange mit dem MySQL Connector/C++ rumprobiert, hab das ganze allerdings nicht wirklich vernünftig zum Laufen bekommen (wenn Details gewünscht sind, kann ich gerne noch einen weiteren Blogpost dazu schreiben :-D). Viele Probleme die bei mir aufgetreten sind waren wohl nicht unbekannt und sind (teilweise) sogar ohne Lösung auf StackOverflow zu finden. Dort bin ich dann auch auf die Idee gekommen einfach die MySQL C-API zu nehmen. Gesagt getan, ich hab mir einen kleinen Wrapper dafür geschrieben und ab dort konnte ich ohne Probleme über C++ mit meiner mySQL Datenbank reden. Um die C-API zu benutzen, muss man einfach von der MySQL Homepage den „C-Connector“ runterladen, das include-Directory im Compiler mit einbinden, das lib-Directory dem Linker geben und mit der libmysql linken. Dann kann man die Funktionen per #include „mysql.h“ laden und verwenden. Je nach Projekt kann es nötig sein noch die SOCKET-Header (unter windows winsock2.h, unter linux sys/socket.h und arpa/inet.h) einzubinden.

Kleines Beispiel-Programm:

#include 

#include "mysql.h"

using namespace std;

int main()
{
	cout << "Content-Type: text/html" << endl << endl;
	
	cout << "nmySQL TEST" << endl;
	
	MYSQL *conn = mysql_init(NULL);
	
	if (mysql_real_connect(conn, "localhost", "root", "", "testdatabase", NULL, 0, NULL) == NULL)
	{
		printf("mySQL Error #%u: %s 
n", mysql_errno(conn), mysql_error(conn)); return 0; } if (mysql_real_query(conn, "SELECT name, age FROM clients") != 0) { printf("mySQL Error #%u: %s
n", mysql_errno(conn), mysql_error(conn)); return 0; } cout << "

Client age table:

" << endl; MYSQL_RES *result = mysql_store_result(conn); MYSQL_ROW row; while (row = mysql_fetch_row(result)) { printf("n", row[0], row[1]); } mysql_free_result(result); mysql_close(conn); cout << "
Name age
%s%d
" << endl; return 0; }

Da ich ja am TWLan-Projekt mitarbeite (www.twlan.org) habe ich als Test mal die Startseite und die Registration in C++ implementiert. Dafür habe ich noch eine kleine C++ HTML Template Klasse geschrieben, ist allerdings nichts besonderes. Nun habe ich die PHP-Startseite und die C++ CGI-Startseite mal (ohne Caching) gegeneinander im Punkto Geschwindigkeit gegenübergestellt:

PHP-TWLan: 0.2501s 0.0743s 0.0771s 0.0693s 0.0672s 0.0635s 0.0775s
AVG:	   0.097s

CGI-TWLan: 0.0220s 0.0321s 0.0142s 0.0354s 0.0199s 0.0194s 0.0195s
AVG:	   0.023s

Die CGI-Version ist, selbst bei nur 2-Datenbank-Abfragen, 4x schneller als der PHP-Code. Der sehr hohe Wert bei der ersten PHP-Zeitmessung erklärt sich übrigens dadurch, dass die Zend-Engine beim ersten durchlauf den PHP-Opcode cached. Ein solches Ergebnis war zu erwarten – und wenn man es dann mal selbst erlebt ist es doch sehr motivierend weitere Web-Anwendungen in C++ zu schreiben. Wenn man dann noch FastCGI unterstützt, und zB lighttpd als Webserver verwendet, dann hat man wirklich die ultimativ-schnelle Web-Anwendung 🙂

Das „TWLanCGI“ Projekt gibts im TWLan-Forum übrigens für Windows zum Download (hier).