Writing Hello World in FCGI with C++

March 23, 2012

Prerequisites

This post will assume some familiarity with C++. The setup instructions also assume the server to deploy to is Ubuntu. This tutorial can be applied to other operating systems but I do not provide instructions on installing libraries on other operating systems.

Why FCGI?

FCGI (FastCGI) is a protocol in which web applications can talk to a web server to serve web requests. Long story short FCGI was developed to solve the scalability shortcomings that CGI. Typical CGI applications will fork a new process to deal with the request. While this is convenient, it is terribly inefficient due to the overhead of creating and terminating a process for each web request. The idea with FCGI is that you can spawn a static number of processes to handle web requests. This allows for the overhead of creating and terminating processes to be eliminated as resources for request handling can be reused within one process handling multiple requests.

FCGI easily allows for our application to accept web requests by interacting with stdio. There are alternatives such as implementing your own http server using tools like boost. Simple interface and robust are two of the key points of using fcgi for developing web applications in C++.

There are more reasons to use fcgi but you can check out the official site and the wikipedia page for more info.

Installation

We will need to install the libfcgi++ library, a web server (nignx in our case) and spawn-fcgi to run the fcgi app. We’ll also install curl to help test later on.

sudo apt-get install libfcgi-dev
sudo apt-get install spawn-fcgi
sudo apt-get install nginx
sudo apt-get install curl

The Code

Most of this code is basically a copy paste re-interpretation of the example echo-cpp.cpp from fastcgi.com

#include <iostream>
#include "fcgio.h"

using namespace std;

int main(void) {
    // Backup the stdio streambufs
    streambuf * cin_streambuf  = cin.rdbuf();
    streambuf * cout_streambuf = cout.rdbuf();
    streambuf * cerr_streambuf = cerr.rdbuf();

    FCGX_Request request;

    FCGX_Init();
    FCGX_InitRequest(&request, 0, 0);

    while (FCGX_Accept_r(&request) == 0) {
        fcgi_streambuf cin_fcgi_streambuf(request.in);
        fcgi_streambuf cout_fcgi_streambuf(request.out);
        fcgi_streambuf cerr_fcgi_streambuf(request.err);

        cin.rdbuf(&cin_fcgi_streambuf);
        cout.rdbuf(&cout_fcgi_streambuf);
        cerr.rdbuf(&cerr_fcgi_streambuf);

        cout << "Content-type: text/html\r\n"
             << "\r\n"
             << "<html>\n"
             << "  <head>\n"
             << "    <title>Hello, World!</title>\n"
             << "  </head>\n"
             << "  <body>\n"
             << "    <h1>Hello, World!</h1>\n"
             << "  </body>\n"
             << "</html>\n";

        // Note: the fcgi_streambuf destructor will auto flush
    }

    // restore stdio streambufs
    cin.rdbuf(cin_streambuf);
    cout.rdbuf(cout_streambuf);
    cerr.rdbuf(cerr_streambuf);

    return 0;
}

Breaking code dump down we see that the de-facto interface to stdio (cout/cin/cerr) are hijacked to serve the purposes of request handling. It’s possible to retrieve the iostreams back since we save the stdio stream buffers.

We then initialize the FCGX library with FCGX_Init() and initialize the request object that we share across requests for the lifetime of this process.

We then have a blocking loop accepting fcgi requests with our call to FCGX_Accept_r. The _r version calls the multi-thread safe version of the function, although this is not necessary in our single threaded program. This will cleanup the old request that was passed into FCXG_Accept_r and initialize the new request when it comes in.

We construct the fcgi stream buffers inside the loop, using RAII pattern to ensure the buffers are flushed at the end of the request processing (end of the loop)

Nginx Config

Next up we’ll setup nginx to listen on port 80 for http requests and forward those along the the fcgi process which will listen on port 8000.

The key directive is fastcgi_pass 127.0.0.1:8000 indicates that nginx should forward the fcgi request to port 8000 at localhost. You can replace the address with an upstream directive if you want to want to load balance across many processes. The rest of the fastcgi_param directives are optional and just set appropriate environment variables which get forwarded to the fcgi application.

events {
  worker_connections 1024;
}

http {
  server {
    listen 80;
    server_name localhost;

    location / {
      fastcgi_pass   127.0.0.1:8000;

      fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
      fastcgi_param  SERVER_SOFTWARE    nginx;
      fastcgi_param  QUERY_STRING       $query_string;
      fastcgi_param  REQUEST_METHOD     $request_method;
      fastcgi_param  CONTENT_TYPE       $content_type;
      fastcgi_param  CONTENT_LENGTH     $content_length;
      fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
      fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
      fastcgi_param  REQUEST_URI        $request_uri;
      fastcgi_param  DOCUMENT_URI       $document_uri;
      fastcgi_param  DOCUMENT_ROOT      $document_root;
      fastcgi_param  SERVER_PROTOCOL    $server_protocol;
      fastcgi_param  REMOTE_ADDR        $remote_addr;
      fastcgi_param  REMOTE_PORT        $remote_port;
      fastcgi_param  SERVER_ADDR        $server_addr;
      fastcgi_param  SERVER_PORT        $server_port;
      fastcgi_param  SERVER_NAME        $server_name;
    }
  }
}

Running the code

The system now comprises of three parts. One will be the executable fcgi c++ app. In order to run this we need spawn-fcgi and to listen to a port. Nginx will then forward web requests to this port, translating the http proctocol into the fast cgi protocol.

# run nginx using the provided configuration
sudo nginx -c <path to nginx.conf>

# compile hello_world
g++ main_v1.cpp -lfcgi++ -lfcgi -o hello_world

# spawn the fcgi app on port 8000 with no fork
spawn-fcgi -p 8000 -n hello_world

Then just head to the browser and enter http://localhost/ into the location bar and voilà! Hello, World! See the next part in this tutorial to see how to incorporate the request uri and the request content into this simple app.


Click to load comments