Overview

This tutorial will show you how to configure a HTTP server, while you get familiar with the event manager and the server API.

We'll first cover the basics on HTTP, so feel free to skip ahead if you already handle that.

HTTP basics

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

HTTP transaction

Both HTTP request and HTTP response have a 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 a 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 the contents of a directory on the server's filesystem. In this case, the server is called a "static server". Or, the data could instead be generated by the 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, a 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 the 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;
}

NOTE: If you use a relative path for your root directory, make sure to avoid double-dots, ... If you need to reference an upper-level directory, use an absolute path.

You can check examples/http-server for a more verbose version of this minimal example, it also follows some advice on signal handling we'll see below

  • If you've not already done so, clone the Mongoose Library repo

    $ git clone https://github.com/cesanta/mongoose
    
  • Build and run the example

    $ cd mongoose/examples/http-server
    $ make clean all
    
  • Go to http://localhost:8000 in your browser

Dynamic RESTful server

A dynamic server would be very similar to a static one - the difference would be in the 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 specific headers and body. This snippet replies with a 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 two numbers on the URI /api/sum, and respond with an error on any other URI. For this, we must get more information about the request. First, we cast the function argument ev_data to a 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 an error. If it matches, we assume that the client has sent us the numbers to add as a JSON array in the POST body, like [ 12.2, 4.7 ]. We fetch them using Mongoose's built-in JSON 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
      struct mg_str params = mg_str("");
      double num1, num2;
      params = mg_str_n(hm->body.ptr, hm->body.len);
      if (mg_json_get_num(params, "$[0]", &num1) &&
          mg_json_get_num(params, "$[1]", &num2)) {
        // Success! create a JSON response
        char *resp = mg_mprintf("{%Q:%g}", "result", num1 + num2);
        mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s\n", resp);
        free (resp);
      } else {
        mg_http_reply(c, 500, NULL, "%s", "Parameters missing");
      }
    } else {
      mg_http_reply(c, 500, NULL, "%s", "Invalid URI");
    }
  }

Check the JSON-RPC tutorial for a more involved example.

Hybrid server

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

hybrid HTTP server

In this case, the event handler function is going to handle certain URIs as dynamic, and for the rest of the URIs, just call mg_http_serve_dir(). Here's an example on how to implement such an event handler function:

You can check examples/http-restful-server for an expanded version of this minimal example, it also makes use of the chunked transfer encoding method we'll introduce below.

  • If you've not already done so, clone the Mongoose Library repo

    $ git clone https://github.com/cesanta/mongoose
    
  • Build and run the example

    $ cd mongoose/examples/http-restful-server
    $ make clean all
    
  • Go to http://localhost:8000 in your browser to see directory contents

  • Go to http://localhost:8000/api/f2/this in your browser to see an example of an API URI

  • Go to http://localhost:8000/api/stats in your browser to see chunk-encoded dynamically generated statistics

How to get request data

As it was mentioned above, when Mongoose calls the user's event handler function with the 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;
    ...

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

HTTP message format

From that diagram it should be clear how to access any part of a HTTP request (or response, in case of a HTTP client). For example, this code snippet 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);
  }
}

How to iterate and send data

The following snippet echoes back all HTTP headers we described in this section above. Since we iterate over headers, we cannot use mg_http_reply(). We must send the HTTP response line and the headers first, and then append to the generated response body several times. In cases like this, it is best to use chunked transfer encoding, and we'll do that using mg_http_printf_chunk():

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;
    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 examples above, we have used the mg_printf() function to create a response. Note that mg_printf() is not HTTP specific; it is a generic function. It simply prints data to the connection's output buffer according to a printf()-like format specification.

Using mg_printf(), it is possible to create any custom response - in fact, the mg_http_reply() function is just a convenient 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'll stop sending data when you'll close the connection - so it will look like your peer receives your data after a huge delay (actually, when the connection times out). Therefore, either:

  • Use the mg_http_reply() function, which sets the Content-Length header automatically, or
  • Use chunked transfer encoding like shown in a previous section, or
  • If you're crafting your response manually, e.g. by using mg_printf(), don't forget to set the 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

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

Add a signal handler function:

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

Then, 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: