HTTP basics

HTTP protocol is a simple request/response protocol. HTTP client sends a request to the HTTP server, and HTTP server replies. Each request/response transaction is stateless, i.e. it is completely self-contained and does not depend on the previous request/responses. Usually, HTTP client is a browser, and HTTP server is a piece of software that runs on a workstation, cloud server - or on a embedded device with Mongoose library!

HTTP transaction

Both HTTP request and HTTP response have simple format:

  • a request line (for requests) or a status line (for responses)
    • GET / HTTP/1.0\r\n - a request line
    • HTTP/1.0 200 OK\r\n - a status line
  • zero or more HTTP headers. Each header has NAME: VALUE\r\n format. Headers are terminated by an empty line, \r\n
  • HTTP body, which could be empty

Example HTTP transaction:

HTTP message format

Data that is returned by a server could be just a contents of a directory on server's filesystem. In this case, a server is called a "static server". Or, data could be generated by server "on the fly" - in which case the content is called "dynamic", and such a server is often architectured as a RESTful API server that returns data in JSON format. A hybrid approach is also common - for example, device Web UI could have a static server with HTML, CSS, JavaScript and media files, as well as a dynamic RESTful API server that returns internal device state.

A minimal static server

Below is a minimal HTTP server that serves files from a given directory, taken straight from the user guide. It is pretty straightforward: we initialise an event manager, create one listening connection that listens on localhost:8000, and start an infinite event loop. An event handler function, fn, simply calls mg_http_serve_dir() function which serves static content from a given directory:

#include "mongoose.h"

static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  struct mg_http_serve_opts opts = {.root_dir = "."};   // Serve local dir
  if (ev == MG_EV_HTTP_MSG) mg_http_serve_dir(c, ev_data, &opts);
}

int main(int argc, char *argv[]) {
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);                                        // Init manager
  mg_http_listen(&mgr, "http://localhost:8000", fn, &mgr);  // Setup listener
  for (;;) mg_mgr_poll(&mgr, 1000);                         // Event loop
  mg_mgr_free(&mgr);                                        // Cleanup
  return 0;
}

Dynamic RESTful server

A dynamic server would be very similar to a static one - the difference would be in a function that outputs data. Instead of mg_http_serve_dir(), we're going to use mg_http_reply(). Using mg_http_reply(), we can generate responses with specified headers and body. This snippet replies with plain-text "Hello, world" on any URI:

static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  if (ev == MG_EV_HTTP_MSG) {
    mg_http_reply(c, 200, "Content-Type: text/plain\r\n", "Hello, %s\n", "world");
  }
}

That is not a very useful server. Let's modify it to become a RESTful API server that can add numbers on URI /api/sum, and respond with an error on any other URI. For this, we must get more information about the request. First, cast ev_data to struct mg_http_message, which contains a parsed HTTP request. Then we check the URI by calling mg_http_match_uri(). If no match, we return error. If math, we assume that the client sent us numbers to add as JSON array in the POST body, like [ 12.2, 4.7 ]. We fetch them using mjson library API, calculate the sum, and respond with a result:

static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    if (mg_http_match_uri(hm, "/api/sum")) {
      // Attempt to fetch parameters from the body, hm->body
      double num1, num2;
      if (mjson_get_number(hm->body.ptr, hm->body.len, "$[0]", &num1) &&
          mjson_get_number(hm->body.ptr, hm->body.len, "$[1]", &num2)) {
        // Success! create a JSON response
        char resp[100];
        mjson_snprintf(resp, sizeof(resp), "{%Q:%g}", "result", num1 + num2);
        mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", resp);
      } else {
        mg_http_reply(c, 500, NULL, "%s", "Parameters missing");
      }
    } else {
      mg_http_reply(c, 500, NULL, "%s", "Invalid URI");
    }
  }

Hybrid server

The best way to implement Web UI for an application or device is to integrate a hybrid web server in it. Customer's browser downloads client part, which in turn makes RESTful requests and renders the UI on a browser:

hybrid HTTP server

In this case, the event handler function is going to handle certain URIs as dynamic, and for the rest of URIs, call mg_http_serve_dir(). The relevant example is examples/http-restful-server, and that's how it implements an event handler function:

How to get request data

As it was mentioned above, when Mongoose calls user's event handler function with MG_EV_HTTP_MSG event, the void *ev_data pointer points to a parsed HTTP message, struct mg_http_message:

static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    ...

The diagram below shows how different parts of HTTP message map to that structure:

HTTP message format

From that diagram it should be apparent, how to access any part of HTTP request (or response, in case of a HTTP client). For example, this snippet of code echoes back the requested URI: (in case of HTTP client - protocol, status code, and message):

static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    mg_http_reply(c, 200, NULL, "URI: %.*s", (int) hm->uri.len, hm->uri.ptr);
  }
}

And the following snippet echoes back all HTTP headers. Since we iterate over headers, we cannot use mg_http_reply(). We must send HTTP response line and headers first, and then append to the response body several times. In cases like this, it is best to use chunked transfer encoding:

static void fn(struct mg_connection *c, int ev, void *ev_data, void *fn_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    static const char *cte = 
    size_t i, max = sizeof(hm->headers) / sizeof(hm->headers[0]);
    mg_printf(c, "%s%s%s\r\n",
              "HTTP/1.1 200 OK\r\n"               // Output response line
              "Transfer-Encoding: chunked\r\n";   // Chunked header
              "Content-Type: text/plain\r\n");    // and Content-Type header
    mg_printf(c, "Request headers:\n");
    // Iterate over request headers, and print them one by one
    for (i = 0; i < max && hm->headers[i].name.len > 0; i++) {
      struct mg_str *k = &hm->headers[i].name, *v = &hm->headers[i].value;
      mg_http_printf_chunk(c, "%.*s -> %.*s\n", (int) k->len, k->ptr, (int) v->len, v->ptr);
    }
    mg_http_write_chunk(c, "", 0);  // Final empty chunk
  }
}

Using mg_printf()

In the code above, we have used mg_printf() function to create response. Note that mg_printf() is not HTTP specific. It is generic. It simply prints data to the connection's output buffer according to the printf()-like format specification.

Using mg_printf(), it is possible to create any custom response - in fact, mg_http_reply() function is just a convenience wrapper around mg_printf() that adds a correct status message and a Content-Length header.

Things to remember

Set content length

Always set content length! Otherwise, your peer assumes that you stop sending data when you close a connection - so it will look like your peer receives your data after a huge delay (actually, when a connection times out). Therefore, either:

  • Use mg_http_reply() function which sets Content-Length header automatically, or
  • Use chunked transfer encoding like shown in the previous section, or
  • If you're crafting your response manually, e.g. by using mg_printf(), don't forget to set Content-Length header. Here is an illustration:
// Create JSON response using mjson library call
char *json = mjson_aprintf("{%Q: [%d, %g]}", "data", 123, 3.1415);

// Send response to the client
mg_printf(c, "%s", "HTTP/1.0 200 OK\r\n");                  // Response line
mg_printf(c, "%s", "Content-Type: application/json\r\n");   // One header
mg_printf(c, "Content-Length: %d\r\n", (int) strlen(json)); // Another header
mg_printf(c, "%s", "\r\n");                                 // End of headers
mg_printf(c, "%s", json);                                   // Body
free(json);                                                 // Don't forget!

Handle signals

That applies to UNIX or UNIX-like systems like MacOS. Especially, ignore SIGPIPE signal to prevent accidental killing of your server when some client just abrupts a connection, and handle SIGINT to make your server behave well in container environments like Docker.

Add a signal handler function:

In the main(), assign our signal handler function to handle SIGINT and SIGTERM signals:

And make the main event loop exit when a signal arrives:

Command line flags

Use command line flags to tune your server. For that, we declare change-able parameters as static variables:

And add a command line parsing snippet: