Embedded HandbookApps
All sections › XVII. Systems programming in C
· level intermediate · 12 min read
Open in the interactive handbook

142. Web services in C: HTTP, REST, CGI, FastCGI, sendfile

Section 78 showed sockets and a first web server; section 141 showed how to hold thousands of connections. Here is the protocol itself and the ways to plug a program in C into the web: with your own server, through CGI under someone else's server, through FastCGI behind nginx. And the thing nginx is loved for — sending a file without copying it through user space, sendfile. All the responses below were obtained with curl from the programs in _verify/sys142/ in stm-sys:1; the CGI program was run by a real CGI server.

Terms used in the documentation

Term Meaning
request line, status line the first line of a request, of a response
header, body metadata lines, then the payload
method (GET, POST, PUT, DELETE) what to do with the resource
status code the result of a request (200, 404, 405…)
Content-Type, Content-Length the type and length of the body
MIME type the content type
REST, resource, endpoint REST, a thing with an address, an address that serves requests
CGI (Common Gateway Interface) a gateway: one program run per request
FastCGI a persistent process instead of a launch per request
reverse proxy a server in front of the application (nginx)
zero-copy, sendfile no copying through the program's memory
WebSocket a two-way channel on top of HTTP

142.1 HTTP is text

HTTP/1.1 is lines of text separated by \r\n: the request line, the headers, an empty line, the body. The response is the same. curl -v shows both sides (> is the request, < is the response):

> GET /hello HTTP/1.1
> Host: 127.0.0.1:8080
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain
< Content-Length: 13
< Connection: close
< 
Hello from C

The server must either state the length of the body (Content-Length), or close the connection (Connection: close), or send it in chunks (Transfer-Encoding: chunked) — otherwise the client cannot tell where the end is. Your own server builds the response with one function:

static void reply(int c, int code, const char *text, const char *type, const char *body, long len)
{
    char hdr[256];
    int n = snprintf(hdr, sizeof hdr, "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %ld\r\nConnection: close\r\n\r\n",
                     code, text, type, len);
    write(c, hdr, n);
    if (body) write(c, body, len);
}

Parsing the request is sscanf("%7s %255s") for the method and path and a search for \r\n\r\n before the body. Methods: GET reads, POST creates or executes, PUT replaces, PATCH changes a part, DELETE deletes, HEAD returns only the headers, OPTIONS tells what is allowed. Codes: 2xx success, 3xx redirection, 4xx client error (400 bad request, 404 not found, 405 method not allowed), 5xx server error.

Three ways to plug C into the web: your own HTTP server, a CGI process per request, FastCGI behind nginx


142.2 REST: resource, method, representation

REST is not a library but a convention: every thing has an address (a resource), standard methods are applied to it, and the state travels in the body (usually JSON); the server remembers nothing between requests (no session). An LED as the resource /api/led:

    } else if (!strcmp(path, "/api/led")) {                   /* REST: GET читает, PUT меняет */
        if (!strcmp(method, "PUT")) {
            static const char err[] = "{\"error\":\"expected {\\\"on\\\":0|1}\"}\n";   /* длину считает sizeof */
            int v; if (sscanf(body, "{\"on\":%d}", &v) == 1) led = v;
            else { reply(c, 400, "Bad Request", "application/json", err, sizeof err - 1); return; }
        } else if (strcmp(method, "GET")) { reply(c, 405, "Method Not Allowed", "text/plain", "", 0); return; }
curl http://…/api/led                          → {"on":0}
curl -X PUT -d '{"on":1}' http://…/api/led     → {"on":1}
curl http://…/api/led                          → {"on":1}
POST /api/led -> 405
PUT 'garbage' -> 400 {"error":"expected {\"on\":0|1}"}

The body length is computed by sizeof, not by hand: the error string is 34 bytes including the newline, and a length of 32 written as a number would cut off the closing brace — the client would get incomplete JSON.

This is what a device API looks like: GET for reading sensors, PUT for setpoints, errors as a code plus JSON with an explanation. JSON is parsed by hand only in a toy case like this one; real code uses cJSON or jansson, and on a microcontroller cJSON too, or jsmn. Full-featured REST servers in C are libmicrohttpd, ulfius, civetweb.

On a network beyond the developer's desk such an API is protected. Without TLS, setpoints and passwords go in plain text; without authentication, anyone who sees the device on the network can change a setpoint. On Linux, TLS is usually handled by nginx in front of the application (142.4); on a microcontroller, by mbedTLS or the communication module (107.7); for authentication use at least a key in the Authorization header, by which the server tells its own clients apart (tokens — 88.7). And request parsing in your own server is a place for overflows: check the length before copying (129.6).


142.3 CGI: a program per request

The oldest way to connect C to the web: the server starts a program for each request, passes the parameters in environment variables and the body in stdin, and sends the program's stdout to the client:

    const char *method = getenv("REQUEST_METHOD"), *query = getenv("QUERY_STRING"), *len = getenv("CONTENT_LENGTH");
    printf("Content-Type: text/plain\r\n\r\n");                /* заголовки, пустая строка, тело */

Under a real CGI server (python3 -m http.server --cgi):

curl "http://…/cgi-bin/env.cgi?x=1&y=2"
method=GET query=x=1&y=2
GATEWAY_INTERFACE=CGI/1.1
SERVER_PROTOCOL=HTTP/1.0
SCRIPT_NAME=/cgi-bin/env.cgi
REMOTE_ADDR=127.0.0.1
HTTP_USER_AGENT=curl/7.88.1

curl -d 'hello=world' http://…/cgi-bin/env.cgi
method=POST query=
body=hello=world

Request headers arrive as HTTP_*, the URL parameters in QUERY_STRING, the body as CONTENT_LENGTH bytes from stdin. Simple and reliable: if the program crashes, only this request fails. Expensive: fork + exec for every request (section 136), no state between requests, the database connection is opened anew every time.

The path of a CGI request: the server starts the program, the request arrives in the environment and stdin, the response leaves through stdout


142.4 FastCGI: a persistent process behind nginx

FastCGI keeps the CGI contract — the same variables and streams — but the process lives permanently and accepts requests over a socket from the web server:

    FCGX_Init();
    int sock = FCGX_OpenSocket(":9000", 16);                     /* nginx: fastcgi_pass app:9000 */
    if (sock < 0) return 1;
    FCGX_Request r;
    FCGX_InitRequest(&r, sock, 0);
    long requests = 0;
    while (FCGX_Accept_r(&r) == 0) {                             /* каждый запрос — без нового процесса */
        const char *uri = FCGX_GetParam("REQUEST_URI", r.envp);

nginx stands in front of the application: it accepts HTTP, serves static files itself, and passes requests to the application over FastCGI:

location /app { fastcgi_pass app:9000; include fastcgi_params; }

The test runs in two containers on one Docker network — nginx and the application: three requests in a row answer запрос №1, №2, №3 ("request no.") from one pid — the state lives between requests, there is one process. php-fpm does the same for PHP and uWSGI for Python; fcgiwrap turns an ordinary CGI program into FastCGI.


142.5 sendfile: a file into a socket without copying

Sending a file over the network "head-on" is a loop of read into a buffer and write from it: every byte goes kernel → program → kernel. sendfile(out, in, &off, n) asks the kernel to move the data directly; for a socket the kernel does not even need to copy — it hands the file's pages to the network stack.

    while ((n = read(f, buf, sizeof buf)) > 0) if (write(fd, buf, n) != n) break;   /* ядро → буфер → ядро */
    while (off < SIZE && sendfile(fd, f, &off, SIZE - off) > 0) ;                   /* без выхода в пользователя */

256 MB through a pair of sockets on one machine:

read+write: 256 МБ за 114 мс, 2.4 ГБ/с, получено всё: да
sendfile:   256 МБ за 43 мс, 6.2 ГБ/с, получено всё: да

Faster — and with almost no CPU load. The server from 142.1 serves /file/… this way: a megabyte of random bytes reached curl intact. sendfile may send less than requested, so it runs in a loop on off. Relatives: splice — between any descriptors through a pipe, tee — duplication; TCP_CORK glues the header and the file into one packet. nginx is built on this: static files through sendfile, thousands of connections on epoll, 2.5 MB of memory per 10000 idle connections.


142.6 WebSocket and what comes next

HTTP is "request — response"; a device often needs the opposite, to send data on its own initiative. WebSocket starts as an HTTP request with the Upgrade: websocket header; after the 101 Switching Protocols response the connection becomes a two-way channel of frames. In C it is provided by libwebsockets and ulfius; for microcontrollers MQTT (section 87) is more common — it is lighter. HTTP/2 and HTTP/3 are binary, with multiplexing; for a device with one request per second they bring no gain, and HTTP/1.1 remains the norm.


142.7 Pitfalls

Symptom Cause
the client "hangs" after the response there is no Content-Length, no Connection: close and no chunked
the browser shows garbage Content-Type is missing or wrong
the CGI response is 500 "malformed header" no empty line after the headers, or \n instead of \r\n\r\n
CGI does not see the parameters look at QUERY_STRING and CONTENT_LENGTH, not at argv
FastCGI answers "502 Bad Gateway" the application is not listening on the address from fastcgi_pass, or is on another network
sendfile did not send everything it returns less than requested — loop on off
sendfile returns EINVAL in_fd is a socket or a pipe; the input must be a file that supports mmap
PUT with JSON returned 400 the body was parsed by the wrong parser — use a JSON library
SIGPIPE killed the server the client closed the connection; signal(SIGPIPE, SIG_IGN)
one slow client stops everyone the server from 142.1 is single-threaded; for many clients see section 141

142.8 Summary

Full sources

http_min.c — a minimal HTTP/1.1 server with sendfile and the REST resource /api/led.

/* Раздел 142: минимальный сервер HTTP/1.1 — разбор строки запроса и заголовков, ответ с
   Content-Length, статический файл через sendfile, REST-эндпоинт /api/led с GET и PUT.
   Однопоточный, одно соединение за раз: цель — показать протокол, а не производительность. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <netinet/in.h>

static int led;                                              /* «ресурс» для REST */

static void reply(int c, int code, const char *text, const char *type, const char *body, long len)
{
    char hdr[256];
    int n = snprintf(hdr, sizeof hdr, "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %ld\r\nConnection: close\r\n\r\n",
                     code, text, type, len);
    write(c, hdr, n);
    if (body) write(c, body, len);
}

static void handle(int c)
{
    char req[4096]; ssize_t n = read(c, req, sizeof req - 1);
    if (n <= 0) return;
    req[n] = 0;
    char method[8], path[256];
    if (sscanf(req, "%7s %255s", method, path) != 2) { reply(c, 400, "Bad Request", "text/plain", "bad\n", 4); return; }
    char *body = strstr(req, "\r\n\r\n"); body = body ? body + 4 : "";
    if (!strcmp(path, "/hello")) {
        const char *s = "Hello from C\n";
        reply(c, 200, "OK", "text/plain", s, strlen(s));
    } else if (!strcmp(path, "/api/led")) {                   /* REST: GET читает, PUT меняет */
        if (!strcmp(method, "PUT")) {
            static const char err[] = "{\"error\":\"expected {\\\"on\\\":0|1}\"}\n";   /* длину считает sizeof */
            int v; if (sscanf(body, "{\"on\":%d}", &v) == 1) led = v;
            else { reply(c, 400, "Bad Request", "application/json", err, sizeof err - 1); return; }
        } else if (strcmp(method, "GET")) { reply(c, 405, "Method Not Allowed", "text/plain", "", 0); return; }
        char js[32]; int l = snprintf(js, sizeof js, "{\"on\":%d}\n", led);
        reply(c, 200, "OK", "application/json", js, l);
    } else if (!strncmp(path, "/file/", 6) && !strstr(path, "..")) {  /* статический файл через sendfile */
        int f = open(path + 6, O_RDONLY);
        struct stat st;
        if (f < 0 || fstat(f, &st) < 0) { reply(c, 404, "Not Found", "text/plain", "no such file\n", 13); return; }
        reply(c, 200, "OK", "application/octet-stream", NULL, st.st_size);
        off_t off = 0;
        while (off < st.st_size && sendfile(c, f, &off, st.st_size - off) > 0) ;   /* ядро копирует само */
        close(f);
    } else reply(c, 404, "Not Found", "text/plain", "not found\n", 10);
}

int main(int argc, char **argv)
{
    signal(SIGPIPE, SIG_IGN);
    int port = argc > 1 ? atoi(argv[1]) : 8080;
    int s = socket(AF_INET, SOCK_STREAM, 0), one = 1;
    setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
    struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = htonl(INADDR_ANY) };
    if (bind(s, (struct sockaddr *)&a, sizeof a) < 0 || listen(s, 16) < 0) { perror("bind"); return 1; }
    for (;;) { int c = accept(s, NULL, NULL); if (c < 0) continue; handle(c); close(c); }
}

fcgi_app.c — a FastCGI application behind nginx with a counter kept between requests.

/* Раздел 142: приложение FastCGI — один процесс живёт постоянно и принимает запросы от nginx
   по TCP; между запросами хранит состояние (счётчик). Сборка: -lfcgi. */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcgiapp.h>

int main(void)
{
    FCGX_Init();
    int sock = FCGX_OpenSocket(":9000", 16);                     /* nginx: fastcgi_pass app:9000 */
    if (sock < 0) return 1;
    FCGX_Request r;
    FCGX_InitRequest(&r, sock, 0);
    long requests = 0;
    while (FCGX_Accept_r(&r) == 0) {                             /* каждый запрос — без нового процесса */
        const char *uri = FCGX_GetParam("REQUEST_URI", r.envp);
        const char *method = FCGX_GetParam("REQUEST_METHOD", r.envp);
        requests++;
        FCGX_FPrintF(r.out, "Content-Type: text/plain\r\n\r\n");
        FCGX_FPrintF(r.out, "FastCGI: %s %s, запрос №%ld в этом процессе, pid %d\n", method, uri ? uri : "?", requests, getpid());
        FCGX_Finish_r(&r);
    }
    return 0;
}

Next: section 143 — static and dynamic libraries.

Open in the interactive handbook