Mongoose7 New features and poll process details (Qingming Small Holiday series 1)

Note:

This is Mongoose Server — embedded server, not Mongoose.js

Tips: I hope I can have a general understanding of mongoose server before reading this article, and it is best to have read the latest documents on the official website

preface

Finally arrived qingming small long holiday (only one day! But it did give the workers a chance to breathe, we all go out for spring outing, travel, relieve the mood. I would like to take this opportunity to summarize the harvest and summary of the first quarter: in the past three months, I spent most of my time dealing with embedded servers, and mongoose, as a popular framework among embedded servers, is naturally the focus of attention. Coincidentally, Mongoose released a new version 7.0 at the end of December. As a result, over the past three months, I have familiarized myself with the new MonGoose7 features and conducted simple debugging, and gained a general understanding of the 7.0 version. Now LET’s share it with you

First, discard the dross and take the essence

Compared to version 6.0, The 7.0 version of Mongoose has a lot of features cut out. Some of them are dross, but some of the features that I think are good are cut out altogether. (I don’t know if it is because Cesanta’s own mongoose-based server solution is not selling. So on purpose!) Let’s take a look at each one:

1-1. Get rid of CGI

Version 7.0 abandoned CGI completely, there is no CGI in the source code. (cesanta: no CGI box you have to write their own integration business, can’t write to buy my framework, haha) guess the main reason is that the use of CGI too memory consumption, compared with direct write business module, can’t, and especially in the current prevalence of smart home, human-computer interaction becomes increasingly frequent business scenarios, CGI is really out of its depth. 1. Set the environment variables to prepare the child process for forking, copying some HTTP request information to the child process. Fork the child process, and then execute the exec family of functions. So in this process, we will set up two child processes to handle the CGI process and set some additional environment variables to increase memory consumption. This can be fatal for low-power embedded devices, so in version 7.0 mongoose completely sacrificed CGI for the greater good.

1-2, socket read logic changes

The big change is that the socket read logic in Mongoose 6 is not non-blocking in the full sense. The reason is that when Mongoose finds a Sokcet readable, it starts a loop that reads the socket until it is finished (each loop reads up to 1460 bytes). This can cause a problem. If I am uploading a large file and the network signal is good, I will read and receive the handle continuously, so that I cannot process other connected handles and block the whole server.

static int mg_do_recv(struct mg_connection *nc) {
  int res = 0;
  char *buf = NULL;
  size_t len = (nc->flags & MG_F_UDP ? MG_UDP_IO_SIZE : MG_TCP_IO_SIZE);
  if((nc->flags & (MG_F_CLOSE_IMMEDIATELY | MG_F_CONNECTING)) || ((nc->flags & MG_F_LISTENING) && ! (nc->flags & MG_F_UDP))) {return - 1;
  }
  do {
    len = recv_avail_size(nc, len);
    if (len == 0) {
      res = 2 -;
      break;
    }
    if (nc->recv_mbuf.size < nc->recv_mbuf.len + len) {
      mbuf_resize(&nc->recv_mbuf, nc->recv_mbuf.len + len);
    }
    buf = nc->recv_mbuf.buf + nc->recv_mbuf.len;
    len = nc->recv_mbuf.size - nc->recv_mbuf.len;
    if (nc->flags & MG_F_UDP) {
      res = mg_recv_udp(nc, buf, len);
    } else {
      res = mg_recv_tcp(nc, buf, len);// The MG_EV_RECV callback is pulled once every 1460 bytes collected
    }
    // Loop as long as you can read
  } while (res > 0 && !(nc->flags & (MG_F_CLOSE_IMMEDIATELY | MG_F_UDP))); 
  return res;
}
Copy the code

In version 7, this loop was eliminated, polling the socket once (1024 bytes Max), and polling the next socket after pulling the callback instead of blocking here.

1-3. Disable multipart-format

Version 7.0 also dropped support for multipart-format in favor of shard upload mode. This is based on a modified to form a complete set to use, because we now single cycle receive only 1024 bytes, if still use formdata way to upload an entire file at a time, we at the time of parsing, will take a long time to parse the content of the body, may be in hundreds of round polling to receive an HTTP request, Parsing for a single HTTP request is too slow (actually, this is better than nothing, if it is uploading a file, it is slow, don’t affect my other requests) let’s take a look at the upload code in Mongoose7:

int mg_http_upload(struct mg_connection *c, struct mg_http_message *hm,
                   const char *dir) {
  char offset[40] = "", name[200] = "", path[256];
  mg_http_get_var(&hm->query, "offset", offset, sizeof(offset));
  mg_http_get_var(&hm->query, "name", name, sizeof(name));
  if (name[0] = ='\ 0') {
    mg_http_reply(c, 400.""."%s"."name required");
    return - 1;
  } else {
    FILE *fp;
    size_t oft = strtoul(offset, NULL.0);
    snprintf(path, sizeof(path), "%s%c%s", dir, MG_DIRSEP, name);
    LOG(LL_DEBUG,
        ("%p %d bytes @ %d [%s]", c->fd, (int) hm->body.len, (int) oft, name));
    if ((fp = mg_fopen(path, oft == 0 ? "wb" : "ab")) = =NULL) {
      mg_http_reply(c, 400.""."fopen(%s): %d", name, errno);
      return 2 -;
    } else {
      fwrite(hm->body.ptr, 1, hm->body.len, fp);
      fclose(fp);
      mg_http_reply(c, 200.""."");
      returnhm->body.len; }}}Copy the code

There is a small problem. Mongoose7 parses HTTP messages all at once before pulling up the user’s registered HTTP callback, so the data is stuck in memory. When the front-end BLOB is transferring file fragments, it is not appropriate to set the fragments too large, which will cause the embedded device to occupy too much memory. Mongoose6, by contrast, releases the HTTP header when receiving files, concentrating on processing each chunk of multipart and deleting each chunk.

1-4. Adjustment of static resource request logic

I have a lot of questions about the static resource request logic of MonGoose7. Let’s talk about the different places first. The biggest difference is that mongoose7 does away with parsing Range headers. Range header is a part that supports breakpoint download and cannot be obtained. Meanwhile, the
tag and
tag of the front end will use Range header when playing control and dragging the progress bar. If Mongoose does not support this header, The video/audio drag-and-drop function of the Web page is broken.

Configuration options no longer support additional HTTP headers

The char * extra_HEADERS member has been removed from mongoose7’s initial configuration options structure. The mg_printf() function is still there, however, so it’s just a little more verbose when writing the request header.

1-6. Add parameters to the callback function

Void *fn_data this is like CTX’s user_data from mongoose6, except mongoose7 explicitly passes it in, and it can be passed in during the listening binding phase, after which each connection inherits this data (note that inheritance is a pointer, Pointing to the same piece of memory! Modify with caution)

This allows you to pass in some default configurations when configuring your own server, which is more of an object oriented approach to Mongoose development. This way I can pass in the configuration as a structure and save it instead of accessing it as a global variable. Not only does it prevent too many global variables, but it also keeps different configurations for each server. (I’ll encapsulate the object-oriented ideas used by Mongoose later.

1-7. Changes in server initialization

The main change in initialization is the way addresses are bound: mg_bind() has been changed

struct mg_connection *mg_http_listen(struct mg_mgr *mgr, const char *url,mg_event_handler_t fn, void *fn_data)
Copy the code

In mongoose6, mg_bind() only needs to bind the port, and mongoose handles the rest for you internally. In Mongoose7, you need to pass in a complete URL, similar to: https://206.123.6.1:8000 so you may need to hand-write the function that resolves the device’s own IP address. After testing, it seems that the IP address is not supported as localhost or 127.0.0.1.

Mongoose7 poll process

In fact, it is not accurate to say that Mongoose is poll. In essence, Mongoose realizes the whole polling process through select. But for convenience, poll is used in all of the following, and it’s not a problem, because both of these things have O(n) time complexity.

Next we will analyze the whole poll process of Mongoose7.

2-1, the start of the server: listening for sockets

Mongoose uses the connection structure, which we will not go into details, but we can see the official documentation. Mongoose 7 still polls the listening socket and ordinary socket in a linked list. Then, when polling for that socket, which is readable and marked as a listening socket, the accept logic is entered, and we won’t go into that.

2-2, select preparation: fd_set

This part is basically the same as MonGoose6. All connected sockets are placed into an fD_set that listens for readability. The socket is then placed into the listener writable fD_set based on whether the send_buf of the connection has data

If the socket in the writable listener set is in normal state, it must be able to be selected

Next is the select, and then the link flag position corresponding to the readable or writable state, complete the select process. In the source code, the process is completed in mg_iotest;

static void mg_iotest(struct mg_mgr *mgr, int ms) {
#if MG_ARCH == MG_ARCH_FREERTOS
  struct mg_connection *c;
  for(c = mgr->conns; c ! =NULL; c = c->next) {
    FreeRTOS_FD_CLR(c->fd, mgr->ss, eSELECT_WRITE);
    if (c->is_connecting || (c->send.len > 0 && c->is_tls_hs == 0))
      FreeRTOS_FD_SET(c->fd, mgr->ss, eSELECT_WRITE);
  }
  FreeRTOS_select(mgr->ss, pdMS_TO_TICKS(ms));
  for(c = mgr->conns; c ! =NULL; c = c->next) {
    EventBits_t bits = FreeRTOS_FD_ISSET(c->fd, mgr->ss);
    c->is_readable = bits & (eSELECT_READ | eSELECT_EXCEPT) ? 1 : 0;
    c->is_writable = bits & eSELECT_WRITE ? 1 : 0;
  }
#else
  struct timeval tv = {ms / 1000, (ms % 1000) * 1000};
  struct mg_connection *c;
  fd_set rset, wset;
  SOCKET maxfd = 0;
  int rc;

  FD_ZERO(&rset);
  FD_ZERO(&wset);

  for(c = mgr->conns; c ! =NULL; c = c->next) {
    // c->is_writable = 0;
    // TLS might have stuff buffered, so dig everything
    // c->is_readable = c->is_tls && c->is_readable ? 1:0;
    if (c->is_closing || c->is_resolving || FD(c) == INVALID_SOCKET) continue;
    FD_SET(FD(c), &rset);
    if (FD(c) > maxfd) maxfd = FD(c);
    if (c->is_connecting || (c->send.len > 0 && c->is_tls_hs == 0))
      FD_SET(FD(c), &wset);
  }

  if ((rc = select(maxfd + 1, &rset, &wset, NULL, &tv)) < 0) {
    LOG(LL_DEBUG, ("select: %d %d", rc, MG_SOCK_ERRNO));
    FD_ZERO(&rset);
    FD_ZERO(&wset);
  }

  for(c = mgr->conns; c ! =NULL; c = c->next) {
    // TLS might have stuff buffered, so dig everything
    c->is_readable = c->is_tls && c->is_readable
                         ? 1: FD(c) ! = INVALID_SOCKET && FD_ISSET(FD(c), &rset); c->is_writable = FD(c) ! = INVALID_SOCKET && FD_ISSET(FD(c), &wset); }#endif
}
Copy the code

2-3 poll done, poll traverses the connection and pulls the callback

After selecting, we completed a poll and marked the flag bits of each connection. Next, we traversed the whole linked list for separate processing. The mg_call() function still pulls the callback, but the logic has changed slightly:

void mg_call(struct mg_connection *c, int ev, void *ev_data) {
  if(c->pfn ! =NULL) c->pfn(c, ev, ev_data, c->pfn_data);
  if(c->fn ! =NULL) c->fn(c, ev, ev_data, c->fn_data);
}
Copy the code

This is a big departure from mongoose6. Mongoose6 does not pull user callbacks if it pulls default callbacks.

In the 2-3-1 s, the default poll

At the beginning of each connection traversal a callback is pulled (again) and the MG_EV_POLL event is passed to indicate the end of the round of polling. This time the pull callback function is actually very key, and it also provides the possibility for mongoose to be extended into multi-threading (see the example of multi-threading on Github source code). Meanwhile, with the new MG_call (), it also provides infinite possibilities for us to extend Mongoose.

2-3-2. Determine the connection status

First, check whether the connection is a listening connection. If it is, and it is readable, then enter the accept process. Then, you need to decrypt the HTTPS message to determine if it is HTTPS. Again, determine whether it is readable or writable, perform the read operation, write the operation, and pull the callback (MG_EV_WRITE and MG_EV_READ)

In the 2-3-3 s, the source code

Take a look at the source code:

void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
  struct mg_connection *c, *tmp;
  unsigned long now;

  mg_iotest(mgr, ms);
  now = mg_millis();
  mg_timer_poll(now);

  for(c = mgr->conns; c ! =NULL; c = tmp) {
    tmp = c->next;
    mg_call(c, MG_EV_POLL, &now);
    LOG(LL_VERBOSE_DEBUG,
        ("%lu %c%c %c%c%c%c%c", c->id, c->is_readable ? 'r' : The '-',
         c->is_writable ? 'w' : The '-', c->is_tls ? 'T' : 't',
         c->is_connecting ? 'C' : 'c', c->is_tls_hs ? 'H' : 'h',
         c->is_resolving ? 'R' : 'r', c->is_closing ? 'C' : 'c'));
    if (c->is_resolving || c->is_closing) {
      // Do nothing
    } else if (c->is_listening && c->is_udp == 0) {
      if (c->is_readable) accept_conn(mgr, c);
    } else if (c->is_connecting) {
      if (c->is_readable || c->is_writable) connect_conn(c);
    } else if (c->is_tls_hs) {
      if ((c->is_readable || c->is_writable)) mg_tls_handshake(c);
    } else {
      if (c->is_readable) read_conn(c, ll_read);
      if (c->is_writable) write_conn(c);
    }

    if (c->is_draining && c->send.len == 0) c->is_closing = 1;
    if(c->is_closing) close_conn(c); }}Copy the code

When we understand the whole process, the most critical thing is actually the timing of the pull back, if we can grasp the timing of each pull back, then the whole poll process is familiar.

2-4. Flow chart

Next is the flow chart, no nonsense directly above! (stay up late and find a better flowchart software to replace him with T_T)

summary

In this article, we will give a brief introduction to the new and deprecated features of MonGoose7.0, as well as the polling process and logic of MonGoose7.0. We will focus on the timing of poll callbacks, which is the core of poll. At present, many projects of our company will also use this framework, which can stand the test of time