Mongoose Embedded Web Server supports a lot of protocols. We think we’ve added the most popular ones like HTTP, WebSocket, MQTT and more. But, there are many protocols that aren’t supported. And there always will be.

Here’s why:

Protocols_Mongoose.png

The million dollar question

The simple choice, select your networking library and then work with one of the supported protocols. But sometimes, that just won’t work because your chosen networking library doesn’t support your chosen protocol.

It gets especially weird if you are already using Mongoose and you need to find a second library to support your protocol (by the way, you can integrate third party libraries into Mongoose. Use the third party only for parsing the protocol, while the networking part will be handled by Mongoose. The user API will also be provided by Mongoose).

Or another case, you’ve built your own protocol. So there it can’t be available in libraries.

This is where the million dollar question comes up: Is it possible to add a new protocol to Mongoose and how much effort does it take? The answer: Yes, it’s possible. And, the effort really depends on your protocol. The integration with Mongoose itself is a simple process.

The Basics

Let’s start with the basics. We’ll take a look behind the structure and what describes a connection in Mongoose:

struct mg_connection {
struct mg_connection *next, prev; / mg_mgr::active_connections linkage */
struct mg_connection listener; / Set only for accept()-ed connections */
struct mg_mgr mgr; / Pointer to containing manager /
….
sock_t sock; /
Socket to the remote peer /
int err;
...
union socket_address sa; /
Remote peer address /
size_t recv_mbuf_limit; /
Max size of recv buffer /
struct mbuf recv_mbuf; /
Received data /
struct mbuf send_mbuf; /
Data scheduled for sending /
….
time_t last_io_time; /
Timestamp of the last socket IO /
double ev_timer_time; /
Timestamp of the future MG_EV_TIMER /
mg_event_handler_t proto_handler; /
Protocol-specific event handler */
void proto_data; / Protocol-specific data */
void (*proto_data_destructor)(void proto_data);
mg_event_handler_t handler; /
Event handler function */
void user_data; / User-specific data */
void mgr_data; / Implementation-specific event manager's data. */
unsigned long flags;
};

Note, there are two members of mg_event_handler_t type: handler and proto_handler. Handler contains a pointer to the callback functions that you provide to bind/connect functions, while proto_handler is used for protocol implementations.

When Mongoose receives data, it checks if proto_handler is null. Then it calls handler. If proto_handler is not null, it will be invoked instead. To route all Mongoose events to the protocol handler, instead of being user defined, all we have to do is fill proto_handler.

In Mongoose, the usual practice is to provide a special function for this, for example:

int mg_set_protocol_coap(struct mg_connection nc) {
/
supports UDP only */
if ((nc->flags & MG_F_UDP) == 0) {
return -1;
}

nc->proto_handler = coap_handler; /* implemented by coap module */

return 0;
}

The user workflow would be something like:


struct mg_connection c* = mg_connect(.......);
if (c != NULL) {
mg_set_protocol_coap(c);
}

After this, Mongoose stops to call any user defined handler and calls proto_handler ONLY, that means it is the responsibility of proto_handler to call any user defined handler.

Let’s take a look at the coap_handler implementation:

static void coap_handler(struct mg_connection *nc, int ev, void *ev_data) {
struct mbuf *io = &nc->recv_mbuf;
struct mg_coap_message cm;
uint32_t parse_res;

memset(&cm, 0, sizeof(cm));

nc->handler(nc, ev, ev_data);

switch (ev) {
case MG_EV_RECV:
parse_res = mg_coap_parse(io, &cm);
if ((parse_res & MG_COAP_IGNORE) == 0) {
if ((cm.flags & MG_COAP_NOT_ENOUGH_DATA) != 0) {
cm.flags |= MG_COAP_FORMAT_ERROR;
}
nc->handler(nc, MG_COAP_EVENT_BASE + cm.msg_type, &cm);
}

  mg_coap_free_options(&cm);
  mbuf_remove(io, io->len);
  break;

}
}

As you can see, the first thing this handler does is call the user handler:

nc->handler(nc, ev, ev_data);

Calling the user defined handler is required to keep events like MG_RECV, MG_SEND etc. If you want to hide these events from the user, just don’t call handler in this way.

Look at this code:

parse_res = mg_coap_parse(io, &cm);
if ((parse_res & MG_COAP_IGNORE) == 0) {

nc->handler(nc, MG_COAP_EVENT_BASE + cm.msg_type, &cm);
}

This code parses the received data (it is located in case MG_EV_RECV section) and, if parsing is successful, it calls the user handler with the CoAP specific event.

User handles these events in our usual way:

struct mg_connection *c = mg_connect(....., my_handler);
mg_set_protocol_coap(c);
….

static void my_handler(struct mg_connection *nc, int ev, void *p) {
switch (ev) {
…..
case MG_EV_COAP_ACK:
case MG_EV_COAP_RST: {
struct mg_coap_message *cm = (struct mg_coap_message *) p;
printf("ACK/RST for message with msg_id = %d received\n", cm->msg_id);
break;
}
}
}

This alone is enough to integrate a new protocol into Mongoose. Of course, you have to write functions to parse/send messages for you protocol, like mg_coap_parse in our example. And, as I mentioned, you can use a third party library for parsing/composing messages. Just use this library in the MG_EV_RECV handler and utility functions (like mg_coap_send_message)

What about state handling?

If your protocol is like CoAP (one UDP packet = one CoAP message), you don’t need any extras. But, there are other cases. The simplest example is uploading of a big file. Since we are assuming that a file is big and cannot be stored in the internal Mongoose receive buffer, we have to receive it bit by bit. And, this means we need to call the user handler for every received part to give the user the chance to store this file, for example, on flash. In this situation we need to somehow store the progress of the current operation, and storage must be linked to the connection which performs this operation.

Let’s take a look at other members of the mg_connection structure. There are two members, which are supposed to handle these situations:

struct mg_connection {
...
void *proto_data;
void (*proto_data_destructor)(void *proto_data);
….
}

You can use proto_data member in your proto_handler.

For example:

struct my_protocol_state{

int a;
FILE* f;
...
/* A lot of members here */
....
}

mg_set_protocol_my_protocol(struct mg_connection* c) {
struct my_protocol_state *s = malloc(sizeof(my_protocol_state));
s->a = 1;
c->proto_data = s;

}

In this code, we use malloc. So, the next question is: how can I free this memory and release any other resources that I allocated and put them into my_protocol_state?

For this, the proto_data_destructor member is intended. This is a pointer to function, which will be called and then the connection is destroyed.

static void my_protocol_destructor(void *proto_data) {
struct my_protocol_state s = (struct my_protocol_state)proto_data;
fclose(f)
….
free(proto_data);
}

proto_data_destructor might be assigned, for example,  in the mg_set_protocol_my_protocol function.

Note, unlike C++ destructors, you have to free proto_data inside your destructor. Mongoose doesn’t call free for this member.

That’s all

So there you have it. How to integrate a new protocol into Mongoose.

Quick check list:

  1. Implement mg_set_protocol_my_protocol  function, fill proto_handler and (if required) proto_data and proto_data_desctuctor in it.
  2. Implement proto_handler function (at least to handle MG_EV_RECV event)
  3. It is good idea to call the user handler within proto_handler, to deliver events like MG_EV_RECV, MG_EV_SEND, MG_EV_CONNECT etc to user.
  4. Implement destructor for connection, if your protocol needs to store any intermediate data.
  5. Here, the integration ends - and you have to implement the parsing/composing messages of your protocol.

To contact: send us a message or ask on the developer forum.