Mongoose的初始化
mongoose是一个非常适合嵌入式平台的webserver选择,其模式是单线程,异步非阻塞的网络框架,体量非常的小,一般编译完成的可执行文件最小只需要40k,占用的虚拟内存也相当的小。接下来,我们就一起深入mongoose的源码当中来看看他具体是如何实现的。
1、从hello world开始
我们就从一个最简单的mongoose的http服务器来入手,看看mongoose在初始的过程中到底了做了什么。
#include "mongoose.h"
static const char *s_http_port = "8000";
static void ev_handler(struct mg_connection *c, int ev, void *p) {
if (ev == MG_EV_HTTP_REQUEST) {
struct http_message *hm = (struct http_message *) p;
mg_send_head(c, 200, hm->message.len, "Content-Type: text/plain");
mg_printf(c, "%.*s", (int)hm->message.len, hm->message.p);
}
}
int main(void) {
struct mg_mgr mgr;
struct mg_connection *c;
mg_mgr_init(&mgr, NULL);
c = mg_bind(&mgr, s_http_port, ev_handler);
mg_set_protocol_http_websocket(c);
for (;;) {
mg_mgr_poll(&mgr, 1000);
}
mg_mgr_free(&mgr);
return 0;
}
我们接下来将从这个程序下手,来详细看一下mongoose的内部实现
2、结构体mg_mgr 以及 初始化
2-1、mg_mgr结构体
可以看到,我们的main函数的第一条语句就是mg_mgr_init(&mgr, NULL);
那么我们首先来看一下mg_mgr
,官方文档对这个结构体的定义是这样的,原文:mg_mgr
:is an event manager that holds all active connections,大意就是承载着所有的活跃的连接(可以是TCP或者UDP的sockets连接),当然官方的定义会省略一些那些不需要用户去关心的内部实现。那么这个结构体长什么样子,它里面承载了哪一些数据,我们通过mongoose头文件中的声明来看一下吧~
struct mg_mgr {
struct mg_connection *active_connections;
#if MG_ENABLE_HEXDUMP
const char *hexdump_file; /* Debug hexdump file path */
#endif
#if MG_ENABLE_BROADCAST
sock_t ctl[2]; /* Socketpair for mg_broadcast() */
#endif
void *user_data; /* User data */
int num_ifaces;
int num_calls;
struct mg_iface **ifaces; /* network interfaces */
const char *nameserver; /* DNS server to use */
};
里面有一些跨平台相关的特性我们就先不考虑了,重点关注这两个属性:struct mg_connection *active_connections;
和struct mg_iface **ifaces;
struct mg_connection *active_connections;
:这个目前的活跃连接,是一个双向链表,mg_mgr
保存的其中的第一个。像我们上述的最简单的一个http服务器就只创建了一个连接
struct mg_iface **ifaces;
这个结构体承载着mg_mgr
的接口,里面是一系列的函数,用来控制mg_mgr
的处理请求的逻辑。
2-2、mg_mgr的初始化
大致介绍了mg_mgr
结构体的承载内容,接下来我们来看mg_mgr
的初始化的过程,内部会稍微带一点平台相关的内容,但不着重介绍,跨平台并非我们关注的重点,但是也需要稍作了解(看一看人家的产品怎么做到跨平台的特性的总不是坏事)。接下来看一下源码中mg_mgr_init(struct* mg_mgr, user_data)
的相关实现:
void mg_mgr_init(struct mg_mgr *m, void *user_data) {
struct mg_mgr_init_opts opts;
memset(&opts, 0, sizeof(opts));
mg_mgr_init_opt(m, user_data, opts);
}
2-2-1、结构体mg_mgr_init_opts
该函数内部申请了一个空的mg_mgr_init_opts
结构体,然后调用了mg_mgr_init_opt()
函数,来进行实际的初始化,我们先看一下这个选项结构体到底定义了哪一些选项:
//in src/mg_net.c
struct mg_mgr_init_opts {
const struct mg_iface_vtable *main_iface;
int num_ifaces;
const struct mg_iface_vtable **ifaces;
const char *nameserver;
};
```可以看到定义了一个主接口结构体一个接口数组,还有接口数量等参数,这样看来我们也可以通过```mg_mgr_init_opts()```来实现额外参数的传入。
### 2-3、具体初始化实现:mg_mgr_init_opts();
接下来看一下```mg_mgr_init_opts();```的内部逻辑;
```cpp
void mg_mgr_init_opt(struct mg_mgr *m, void *user_data,
struct mg_mgr_init_opts opts) {
memset(m, 0, sizeof(*m));
//省略部分跨平台相关兼容设置
{
int i;
if (opts.num_ifaces == 0) {
opts.num_ifaces = mg_num_ifaces;
opts.ifaces = mg_ifaces;
}
if (opts.main_iface != NULL) {
opts.ifaces[MG_MAIN_IFACE] = opts.main_iface;
}
m->num_ifaces = opts.num_ifaces;
m->ifaces =
(struct mg_iface **) MG_MALLOC(sizeof(*m->ifaces) * opts.num_ifaces);
for (i = 0; i < opts.num_ifaces; i++) {
m->ifaces[i] = mg_if_create_iface(opts.ifaces[i], m);
m->ifaces[i]->vtable->init(m->ifaces[i]);
}
}
if (opts.nameserver != NULL) {
m->nameserver = strdup(opts.nameserver);
}
DBG(("=================================="));
DBG(("init mgr=%p", m));
#if MG_ENABLE_SSL
{
static int init_done;
if (!init_done) {
mg_ssl_if_init();
init_done++;
}
}
#endif
}
2-3-1、opts选项的初始化以及其跨平台等特性。
首先我们可以看到对opts
的接口数量判断,如果为零的话,就会去对他进行默认赋值(通过mg_mgr_init();
肯定就是走的这个逻辑)---给opts
的iface
赋值了mg_iface
的值,这个值是一个全局的变量,在我们编译可执行文件的时候确定来的值。我们可以看一下这个变量定义在/src/mg_net_if.c
文件中,是一个和平台相关的值:
//in /src/mg_net_if.c
extern const struct mg_iface_vtable mg_default_iface_vtable;
const struct mg_iface_vtable *mg_ifaces[] = {
&mg_default_iface_vtable,
};
int mg_num_ifaces = (int) (sizeof(mg_ifaces) / sizeof(mg_ifaces[0]));
可以看到我们extern进来一个变量struct mg_iface_vtable mg_default_iface_vtable;
这个变量在整个源码中有四处定义(根据不同的嵌入式环境有不同的定义),根据编译时的环境来决定使用哪一个,我们这边如果是linux操作系统的话一般是使用定义在src/mg_net_if_socket.c
中定义的。这个vtable
结构体(这个结构体内部承载了很多的函数指针)定义了基本上所有的我们之后初始化相关的函数逻辑,例如创建套接字等等。后面接触到了还会展开的。
2-3-2、mg_mgr的接口初始化
实际上,mg_mgr_init_opts()
做的主要事情也就是把mg_mgr
的接口iface
给确定下来。
赋值完接口之后就可以将这个iface数组赋值给mg_mgr
结构体了,通过调用mg_if_create_iface()
函数来赋值,iface数组的第一个元素就是mg_mgr
的主要接口main_iface
。
接下来就是我们第一次使用这个接口了:
m->ifaces[i]->vtable->init(m->ifaces[i]);
通过自身的接口的init()
函数来对自身的iface接口来进行初始化操作。神奇的操作,而且找到这个函数发现这个函数啥也没做直接就结束了。
2-4、总结
到这里基本mg_mgr
的初始化也就结束了,总结一下一共就做了三件事:
1、初始化一个mg_mgr_init_opts
结构体。
2、对这个结构体根据其平台相关,环境变量相关进行初始化赋值。
3、通过opts选项对mg_mgr
进行初始化。主要是iface接口数组的赋值和初始化。
3、mg_bind():创建、初始化连接
第三部分主要是了解mongoose如何创建,和初始化struct mg_connections
连接的,这个部分涉及到socket相关的内容,可以先行了解一下socket套接字创建的相关内容再来阅读,会有更好的体验。
3-1、什么是mg_connections
mg_connection
结构体可谓是mongoose中最重要的组成部分了,它创建socket套接字,并且定义了套接字如何去处理接收到的信息等相关操作,以及用户回调函数的定义和调用逻辑都是在这里初始化的。首先,照例先看一下这个结构体定义了哪些内容:
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 */
union {
void *v;
/*
* the C standard is fussy about fitting function pointers into
* void pointers, since some archs might have fat pointers for functions.
*/
mg_event_handler_t f;
} priv_1;
void *priv_2;
void *mgr_data; /* Implementation-specific event manager's data. */
struct mg_iface *iface;
unsigned long flags;
};
同样,去掉了部分环境变量相关逻辑,但是仍然是以一个非常庞大的结构体,具体每一个内容是干什么的可以看源码的注释,解释的也比较清楚了,希望都能先有个印象,因为后面的具体逻辑还会遇到各个部分。
3-2、connection的初始化
从helloword的例子中mg_connection
连接是通过mg_bind()
函数来创建的,我们接下来看一下mg_bind();
的内部逻辑:
//in /src/mg_net.c
struct mg_connection *mg_bind(struct mg_mgr *srv, const char *address,
MG_CB(mg_event_handler_t event_handler,
void *user_data)) {
struct mg_bind_opts opts;
memset(&opts, 0, sizeof(opts));
return mg_bind_opt(srv, address, MG_CB(event_handler, user_data), opts);
}
和mg_mgr_init();
一样也是创建一个选项结构体,然后实际调用mg_bind_opt()
来实现connection的创建。接下来看一下它的内部逻辑吧,源码不贴了,比较长,讲到哪儿贴一部分逻辑,源码在/src/mg_net.c
下。
3-2-1、create_connection
首先我们通过mg_parse_address()
函数来解析传入的地址字符串,将它输出成标准的可以被socket套接字认识的地址结构体(这部分可以去参开socket套接字的相关内容)。然后接下来就是调用mg_create_connection(mgr, callback, add_sock_opts);
来创建连接。而mg_create_connection();
是通过调用mg_reate_connection_base();
来创建connection的,我们来看一下函数的内部实现逻辑:
MG_INTERNAL struct mg_connection *mg_create_connection_base(
struct mg_mgr *mgr, mg_event_handler_t callback,
struct mg_add_sock_opts opts) {
struct mg_connection *conn;
if ((conn = (struct mg_connection *) MG_CALLOC(1, sizeof(*conn))) != NULL) {
conn->sock = INVALID_SOCKET;
conn->handler = callback;
conn->mgr = mgr;
conn->last_io_time = (time_t) mg_time();
conn->iface =
(opts.iface != NULL ? opts.iface : mgr->ifaces[MG_MAIN_IFACE]);
conn->flags = opts.flags & _MG_ALLOWED_CONNECT_FLAGS_MASK;
conn->user_data = opts.user_data;
/*
* SIZE_MAX is defined as a long long constant in
* system headers on some platforms and so it
* doesn't compile with pedantic ansi flags.
*/
conn->recv_mbuf_limit = ~0;
} else {
MG_SET_PTRPTR(opts.error_string, "failed to create connection");
}
return conn;
}
内部逻辑不复杂,就是申请connection的内存空间,然后通过传入的mgr管理结构对其进行赋值。这里需要特别注意mg_connection
的handler
和proto_handler
属性,两者完全不一样,前者是用户自定义的回调函数,后者是源码中编写好的预设回调函数后续也会讲到这两者的调用顺序处理逻辑等等。
完成创建后,再把之前解析好的地址结构体赋值给mg_connection->sa
,然后将mg_connection->flags
赋值为MG_F_LISTENING
表示监听模式。
3-2-2、套接字的创建
接下来就是创建套接字(socket)了。我们来看一下,调用的是刚刚初始化,从mg_mgr
赋值给connection的iface
接口结构体的listen_tcp(nc, &nc->sa);
来创建套接字,我们来看一下内部实现:
//in /src/mg_net_if_socket.c
int mg_socket_if_listen_tcp(struct mg_connection *nc,
union socket_address *sa) {
int proto = 0;
sock_t sock = mg_open_listening_socket(sa, SOCK_STREAM, proto);
if (sock == INVALID_SOCKET) {
return (mg_get_errno() ? mg_get_errno() : 1);
}
mg_sock_set(nc, sock);
return 0;
}
mg_open_listening_socket();
的内部逻辑我们就不细看了,涉及到套接字创建的逻辑和一些知识,有兴趣的童鞋可以了解一下,包括套接字的模式,地址的模式,以及套接字句柄的范围等等。
创建完套接字就是对套接字进行初始化mg_sock_set();
函数内部把套接字设为非阻塞模式。到这里就把sock_t socket
创建以及初始化完成了,我们也可以看到非阻塞模式也是在初始化connection的时候完成的。
3-2-3、connection链表的连接
最后一步,就是把创建好的连接添加到connection的链表上去,这里调用的是mg_add_conn()
函数,我们来看一下内部实现:
//in src/mg_net.c
MG_INTERNAL void mg_add_conn(struct mg_mgr *mgr, struct mg_connection *c) {
DBG(("%p %p", mgr, c));
c->mgr = mgr;
c->next = mgr->active_connections;
mgr->active_connections = c;
c->prev = NULL;
if (c->next != NULL) c->next->prev = c;
if (c->sock != INVALID_SOCKET) {
c->iface->vtable->add_conn(c);
}
}
从代码中可以看出链表是从尾部开始连接,最新创建connection是添加到头部的。最后一步的调用iface的add_conn()
函数逻辑在默认模式下是不参与任何逻辑的。
3-3、mg_connection初始化总结
总结一下mg_connection
的创建以及初始化的过程吧!
1、把传入的地址解析为socket可以识别的地址结构体。
2、调用mg_create_connection()
创建connection把mg_mgr
的iface赋值给connection。
3、将地址结构体赋值给connection,设置connection的flags为MG_F_LISTENING
。
4、创建套接字,设置为非阻塞模式,并将connection的sock指针指向它。
5、将connection添加到链表结构内,从表头开始添加。
4、mg_mgr_poll()轮询机制
最后一步(设置connection的应用层协议的模式会在后面单独展开,本章细讲)就是调用mg_mgr_poll()
来轮询每一个连接,但实际虽然函数叫poll但是内部实现是用select()
来实现句柄的监听的。我们接下来仔细看一下实现的方式。
4-1、socket套接字的分类处理
mg_mgr_poll()
内部实际是调用mg_mgr->iface->vtable->poll()
来实现的句柄监听的,接下来我们先来看一下iface接口的poll的实现吧:
4-1-1、初始化文件句柄集合
由于代码较长,所以我们会摘一部分,一部分一部分来看逻辑。
首先第一步就是要申请一些文件句柄集合数组,这里使用的是fd_set
结构体来承载的。mongoose一共定义了三个这样的集合,fd_set read_set
,fd write_set
和err_set
,分别对应不同的套接字状态:读状态,写状态,和错误状态。
//in /src/mg_net_if_socket.c --->time_t mg_socket_if_poll(struct mg_iface *iface, int timeout_ms)
fd_set read_set, write_set, err_set;
sock_t max_fd = INVALID_SOCKET;
int num_fds, num_ev, num_timers = 0;
#ifdef __unix__
int try_dup = 1;
#endif
FD_ZERO(&read_set);
FD_ZERO(&write_set);
FD_ZERO(&err_set);
4-1-2、套接字句柄分类
接下来就是遍历connection链表(helloworld就一个。。。一般服务端不会创建多个的)将内部的套接字进行分类然后分别放入对应的文件句柄set中,依据则是通过connection的flags和recv_mbuf
和send_mbuf
的状态共同决定的:
//in /src/mg_net_if_socket.c --->time_t mg_socket_if_poll(struct mg_iface *iface, int timeout_ms)
for (nc = mgr->active_connections, num_fds = 0; nc != NULL; nc = tmp) {
tmp = nc->next;
if (nc->sock != INVALID_SOCKET) {
num_fds++;
if (nc->recv_mbuf.len < nc->recv_mbuf_limit &&
(!(nc->flags & MG_F_UDP) || nc->listener == NULL)) {
mg_add_to_set(nc->sock, &read_set, &max_fd);
}
if (((nc->flags & MG_F_CONNECTING) && !(nc->flags & MG_F_WANT_READ)) ||
(nc->send_mbuf.len > 0 && !(nc->flags & MG_F_CONNECTING))) {
mg_add_to_set(nc->sock, &write_set, &max_fd);
mg_add_to_set(nc->sock, &err_set, &max_fd);
}
}
//...后续展开
}
4-1-3、timeOut的确定
在轮询的过程中,因为接下来马上要进入select了,所以要确定超时的时间,默认的超时时间是我们传入mg_mgr_poll(mgr, timeout)
的第二个参数,但是如果connection的ev_timer_time
(时间戳)不为0的话,那么就需要调整time_out的时间为所有connection中最小的那个ev_timer_time
也就是最大的等待时间。
//in /src/mg_net_if_socket.c --->time_t mg_socket_if_poll(struct mg_iface *iface, int timeout_ms)
for (nc = mgr->active_connections, num_fds = 0; nc != NULL; nc = tmp) {
if (nc->ev_timer_time > 0) {
//省略上半部分逻辑。。。
if (num_timers == 0 || nc->ev_timer_time < min_timer) {
min_timer = nc->ev_timer_time;
}
num_timers++;
}
}
if (num_timers > 0) {
double timer_timeout_ms = (min_timer - mg_time()) * 1000 + 1 /* rounding */;
if (timer_timeout_ms < timeout_ms) {
timeout_ms = (int) timer_timeout_ms;
}
}
if (timeout_ms < 0) timeout_ms = 0;
tv.tv_sec = timeout_ms / 1000;//秒级部分
tv.tv_usec = (timeout_ms % 1000) * 1000;//毫秒级部分
4-2、select监听句柄
mongoose是通过select来监听句柄的变化(标准输入输出)的,select函数接收四个入参:
fd_set readfd
:一个专用来接收的句柄,当句柄收到输入的时候,select就会把这个句柄留在readfd
当中。
fd_set writefd
:一个输出句柄,当select监听到可以输出的时候就会将这个句柄继续保留在的writefd
当中。
fd_set err
:出现错误的句柄会放在这个句柄集合当中;
time_t time_out
:超时时间,select监听的时长,返回超时是的各句柄状态。
mongoose就是将上述分好类的句柄传入select中进行监听。
4-2-1、fd_flags的作用
当select结束之后,我们就会对句柄集合再次进行遍历,看留下了哪些就绪的可操作的句柄。 看下面的这段代码:
for (nc = mgr->active_connections; nc != NULL; nc = tmp) {
int fd_flags = 0;
if (nc->sock != INVALID_SOCKET) {
if (num_ev > 0) {
fd_flags = (FD_ISSET(nc->sock, &read_set) &&
(!(nc->flags & MG_F_UDP) || nc->listener == NULL)
? _MG_F_FD_CAN_READ
: 0) |
(FD_ISSET(nc->sock, &write_set) ? _MG_F_FD_CAN_WRITE : 0) |
(FD_ISSET(nc->sock, &err_set) ? _MG_F_FD_ERROR : 0);
}
#if MG_LWIP
/* With LWIP socket emulation layer, we don't get write events for UDP */
if ((nc->flags & MG_F_UDP) && nc->listener == NULL) {
fd_flags |= _MG_F_FD_CAN_WRITE;
}
#endif
}
tmp = nc->next;
mg_mgr_handle_conn(nc, fd_flags, now);
}
这里对每一个连接的句柄建立了一个fd_flags
然后根据是否还在对应的集合当中,添加fd_flags
的标志位。然后调用mg_mgr_handle_conn()
来对就绪的连接进行处理。
5、mg_mgr_handle_conn()数据的接收和发送
5-1、mg_call()处理回调函数
接下来就是调用mg_mgr_handle_conn()
来进行对连接进行处理。
handle_connn
首先会进行一次mg_if_poll()
这个函数主要是用来处理用户设置的连接的flags的,他会先判断用户设置的flags然后做出相应的响应,一般是进行连接的关闭,然后在走一轮空的mg_call()
。mg_call()
函数是实际处理接收请求回调函数的拉起函数,首先,他会去判断是否有回调函数传入,如果没有的话,就会拉起连接绑定的协议的默认回调函数,如果也没有,就会去找用户通过mg_bind()
绑定的自定义回调函数进行处理。看一下他的源码:
MG_INTERNAL void mg_call(struct mg_connection *nc,
mg_event_handler_t ev_handler, void *user_data, int ev,
void *ev_data) {
if (ev_handler == NULL) {
/*
* If protocol handler is specified, call it. Otherwise, call user-specified
* event handler.
*/
ev_handler = nc->proto_handler ? nc->proto_handler : nc->handler;
}
if (ev != MG_EV_POLL) {
DBG(("%p %s ev=%d ev_data=%p flags=0x%lx rmbl=%d smbl=%d", nc,
ev_handler == nc->handler ? "user" : "proto", ev, ev_data, nc->flags,
(int) nc->recv_mbuf.len, (int) nc->send_mbuf.len));
}
if (ev_handler != NULL) {
unsigned long flags_before = nc->flags;
ev_handler(nc, ev, ev_data MG_UD_ARG(user_data));
/* Prevent user handler from fiddling with system flags. */
if (ev_handler == nc->handler && nc->flags != flags_before) {
nc->flags = (flags_before & ~_MG_CALLBACK_MODIFIABLE_FLAGS_MASK) |
(nc->flags & _MG_CALLBACK_MODIFIABLE_FLAGS_MASK);
}
}
if (ev != MG_EV_POLL) nc->mgr->num_calls++;
if (ev != MG_EV_POLL) {
DBG(("%p after %s flags=0x%lx rmbl=%d smbl=%d", nc,
ev_handler == nc->handler ? "user" : "proto", nc->flags,
(int) nc->recv_mbuf.len, (int) nc->send_mbuf.len));
}
}
看到函数在调用回调函数钱会进行flags状态的储存,防止用户篡改默认的flags导致程序的崩溃。
5-2、处理监听connection
回到我们的mg_mgr_handle_conn()
函数,我们可以看到第一部分是处理客户端的逻辑,这部分我们就先跳过,暂不做了解。接着往下看,接下来的逻辑是处理fd_flags
了,看到首先进行套接字读状态的处理。我们来看一下内部实现。抛开UDP部分,首先判断connection的flags
是不是MG_F_LISTENING
回顾上述我们在mg_bind()
中创建的时候处理了这个标志,标志为这个这是一个监听connection。而监听connection的作用就是用来监听请求的,一旦监听到请求,就会通过accept()
再创建一个套接字专门用来处理请求。mongoose是通过mg_accept_conn()
来创建新的连接和套接字的,内部的逻辑不展开,有兴趣的童鞋可以自己看。无非就是把监听连接的一些处理机制同步给新的连接。拉起用户定义的callback进行处理。
if (nc->flags & MG_F_LISTENING) {
/*
* We're not looping here, and accepting just one connection at
* a time. The reason is that eCos does not respect non-blocking
* flag on a listening socket and hangs in a loop.
*/
mg_accept_conn(nc);
} else {
mg_if_can_recv_cb(nc);
}
5-3、处理接收connection
我们在上一步通过监听套接字accept了一个新的套接字,在下一轮poll的手就会实际开始接收字节了,这个时候由于新的接收用的connection是没有MG_F_LISTENING
标志的于是,逻辑会走入mg_if_can_recv_cb(nc)
逻辑进行数据的接收。我们重点看一下这个函数的内部实现:
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);
}
} while (res > 0 && !(nc->flags & (MG_F_CLOSE_IMMEDIATELY | MG_F_UDP)));
return res;
}
void mg_if_can_recv_cb(struct mg_connection *nc) {
mg_do_recv(nc);
}
可以看到接收是循环接收的,首先先将每次接收的大小设定为MG_TCP_IO_SIZE
这个大小为1460个字节。然后判断可接收的recv_mbuf的大小,取两者中较小的一个作为作为最大扩容长度。
接下来判断recv_mbuf
的总大小是否支持本轮的接收后的长度,不够的话,就扩容。接下来就是将临时指针指向缓冲区的末尾,然后将len设置为剩余缓冲区大小进行读取(我的理解前面的设置len长度是作为超过上限时的限制,后面是初始化时给缓冲区的大小分配,然后如果缓冲区满了每次扩大给他一个限制1460字节大小。这里要注意nc-recv_mbuf.size
和nc->recv_mbuf_limit
的区别)。
接下来就是调用原生套接字的read函数进行数据的读取,然后拉起mg_call()
一样的,不传入回调函数,调用绑定协议自带默认回调,这个时候传入的ev变成了MG_EV_RECV
让默认回调知道这个是处理接收请求的。例如http协议的默认handler这个时候就会开始解析接收到的数据,直到接收完整整个http请求后,再拉起用户的自定义回调函数进行逻辑处理。
5-4、处理发送逻辑
我们在回调函数中处理类似mg_serve_http()
等函数的时候就会将响应数据写入send_mbuf
缓冲区,然后在mg_mgr_poll()
的时候就会被分入发送套接字句柄,然后调用mg_mgr_handle_conn()
中的发送逻辑:
if (fd_flags & _MG_F_FD_CAN_WRITE) mg_if_can_send_cb(nc);
我们看到逻辑很简单,就是一句话,通过判断fd_flags
,该函数内部先设置每次写入的字节为1460,然后调用socket套接字的write将数据写入socket的缓冲区里面,然后从send_mbuf
中删除这部分数据:
void mg_if_can_send_cb(struct mg_connection *nc) {
int n = 0;
const char *buf = nc->send_mbuf.buf;
size_t len = nc->send_mbuf.len;
if (nc->flags & (MG_F_CLOSE_IMMEDIATELY | MG_F_CONNECTING)) {
return;
}
if (!(nc->flags & MG_F_UDP)) {
if (nc->flags & MG_F_LISTENING) return;
if (len > MG_TCP_IO_SIZE) len = MG_TCP_IO_SIZE;
}
if (len > 0) {
if (nc->flags & MG_F_UDP) {
n = nc->iface->vtable->udp_send(nc, buf, len);
} else {
n = nc->iface->vtable->tcp_send(nc, buf, len);
}
DBG(("%p -> %d bytes", nc, n));
}
if (n < 0) {
nc->flags |= MG_F_CLOSE_IMMEDIATELY;
} else if (n > 0) {
nc->last_io_time = (time_t) mg_time();
mbuf_remove(&nc->send_mbuf, n);
mbuf_trim(&nc->send_mbuf);
}
if (n != 0) mg_call(nc, NULL, nc->user_data, MG_EV_SEND, &n);
}
从上述代码中,可以看到我们的数据并非一次性发送完成的,而是通过多次轮询分批进行写入缓冲区发送的。
总结
到这里,mongoose的整个初始化的过程我们都有了一定的了解,从管理对象mg_mgr
的创建到mg_connection
监听连接的绑定和初始胡,再到mg_mgr_poll()
轮询机制的流程详解,通过这个过程,希望能够帮助小伙伴理解,作为一个业内成熟的webserver他们是如何对Sockets进行封装的,包括新增承载连接对象,创建二级缓冲recv_mbuf
和send_mbuf
等等,以及对接收数据的处理。
这样我们以后在使用的过程中,遇到问题的时候,如何去定位问题,可能出错的环节,都会比不了解整个机制的情况下,更加的熟稔于心,得心应手。