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!
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 lineHTTP/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:
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;
}
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 absolute path.
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:
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:
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 setsContent-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 setContent-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: