Updated Dec 7, 2022

Web App With C & FastCGI - Request Loop (#1)

How to initialize FastCGI and accept requests.
Watch video on YouTube

Introduction

This is how a typical web request / response cycle works:

  • A client (e.g. browser) makes a request to a web server.
  • The web server passes the request to a web app.
  • The app generates a response based on that request and sends it to the web server.
  • The web server sends the response to the client.

In this example the communication between the app and the web server happens using a protocal called FastCGI.

Setup

Install libfcgi-dev and nginx:

sudo apt-get update
sudo apt-get install libfcgi-dev nginx
  • libfcgi-dev is a library that implements the FastCGI protocol.
  • Nginx is a web server that we use as a reverse proxy. Reverse proxy is a program that forwards client requests to another program and sends responses from the program back to the client. Reverse proxy can add security, performance and scalability to the system (with TLS, caching and load balancing).

Create main.c and add these lines to it:

#include <stdio.h>

int main() {
    printf("Hello.\n");
	return 0;
}

Create build.sh and add this line to it:

cc main.c

Make it executable and run it:

chmod 700 build.sh
./build.sh

Run the program:

./a.out

You should see this:

Hello.

Initialization

Update main.c:

#include <fcgiapp.h> // include this
#include <stdio.h>

int main() {
	
	printf("Hello.\n");

	// add these lines:
	
	FCGX_Init();
	int Socket = FCGX_OpenSocket("127.0.0.1:2016", 1024);
	FCGX_Request Request;
	FCGX_InitRequest(&Request, Socket, 0);
	
	return 0;
}

  • fcgiapp is the library we are using to create FastCGI applications.
  • Note: fcgi_stdio is a thin layer on top of fcgiapp that we won't be using. It adds support for CGI and defines macros for stdio.h types and functions (e.g. printf becomes FCGI_printf).
  • FCGX_Init() initializes the FCGX library. Must be called when using FCGX_Accept_r() to accept requests.
  • Note: FCGX_Accept() would call FCGX_Init if the library is not initialized and then FCGX_Accept_r().
  • FCGX_OpenSocket() creates a communication channel through which our application talks to the web server. 127.0.0.1:2016 is the socket path. 1024 (backlog length) defines the maximum size for the pending connections queue.
  • FCGX_Request is a struct that contains information about a request.
  • FCGX_InitRequest() initializes the Request struct (clears it and sets some values). Socket file descriptor is stored in Request->listen_sock. The last parameter is for flags. You can set it to FCGI_FAIL_ACCEPT_ON_INTR to prevent FCGX_Accept_r() from restarting when interrupted. That's the only flag available.

Update build.sh:

cc main.c -o app.fcgi -lfcgi
  • app.fcgi is the output name of our application executable.
  • -lfcgi links the fcgi library.

The app should run but it doesn't do anything remarkable yet:

./app.fcgi

Accept Requests

Update main.c:

#include <fcgiapp.h>
#include <stdio.h>

int main() {

	printf("Hello.\n");

	FCGX_Init();
	int Socket = FCGX_OpenSocket("127.0.0.1:2016", 1024);
	FCGX_Request Request;
	FCGX_InitRequest(&Request, Socket, 0);
	
	// add this loop:

	while(FCGX_Accept_r(&Request) >= 0) {
		printf("Request accepted.\n");
		FCGX_FPrintF(
			Request.out,
			"Content-type: text/html\r\n\r\n"
			"<h1>Hello.</h1>"
			);
		FCGX_Finish_r(&Request);
	}
	
	return 0;
}
  • FCGX_Accept_r() accepts a request from the server. It creates input, output, and error streams and assigns them to Request->in, Request->out, and Request->err. It creates a data structure for parameters and assigns it to Request->envp (we can access these parameters with FCGX_GetParam()).
  • FCGX_FPrintF writes to the Request->out output stream.
  • FCGX_Finish_r finishes the current request.

Compile and run the app:

./build.sh
./app.fcgi

Replace /etc/nginx/sites-enabled/default contents with these lines:

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    server_name _;

    location / {
        fastcgi_pass 127.0.0.1:2016;
        include fastcgi_params;
    }
}
  • fastcgi_pass sets the address to a FastCGI server that we want to proxy (our web app).
  • include fastcgi_params defines common parameters that we want to pass to the app (such as CONTENT LENGTH, REQUEST_METHOD and QUERY_STRING).

Open a new terminal and make a request:

sudo service nginx restart
curl localhost

You should see the response body:

<h1>Hello.</h1>
Full project in GitHub