引言

我看一个项目的时候,比较喜欢首先看它的架构和设计。因为这样在研读源码的时候,有一个指导作用,不会迷失于具体细节,并能够引导我如何去将点串成线,将线串成面。而且一个软件怎么样,很大程度上取决于它采用的架构。

本文主要介绍Mongoose的工作模型,及根据这个模型将代码大致串起来,找出主线。内容框架如下:

  1. I、线程模型
  2. II、从程序入口着手
  3. III、Mongoose的生命旅程

1、线程模型

Mongoose采用了一个自适应的线程池的模型。有一个主线程(master thread)用于打开配置端口和等待连接的到了。一旦新的连接到来,主线程将衍生一个新的线程去服务该连接。当衍生的线程处理完连接的请求之后,它会保持一段时间的空闲(可以通过配置选项-idle_time <seconds>控制空闲时间),在此期间主线程可能会传递另一个连接给它,让它服务。

因此,每个连接都是在自己的线程中执行,且线程数量随着web服务器的负载而变化。然而,最大的活跃线程数由-max_thread <number>控制。如果一旦总的线程数达到了这个阈值,当新的连接到来时,主线程将等到有线程空闲时在分配线程服务新到来的连接。以此同时,建立了TCP监听队列,即当没有线程空闲时到来的新连接会被置入该队列,当有线程空闲了会从队列中取出连接并服务。如果没有线程变空闲,而TCP队列又满了,web服务器将拒绝新到来的连接请求。

上面所述的过程大致如下所示:

Mongoose源码剖析:mongoose的工作模型_入口

图1、线程模型

2、从程序入口着手

在《Mongoose源码剖析:Introduction and Installation》中,我们简单分析了Makefile文件知道生成的mongoose执行文件的入口肯定在main.c中(如果将Mongoose嵌入到你的应用程序中,就由你来决定入口了!)。在典型的main函数入口中,我们可以看到下面的流程:

main(){
启动mongoose及设置相关参数(或使用默认的);
声明几个信号的处理函数:
#ifndef _WIN32
    (void) signal(SIGCHLD, signal_handler);
#endif /* _WIN32 */
    (void) signal(SIGTERM, signal_handler);
    (void) signal(SIGINT, signal_handler);
ctx=mg_start();
process_command_line_arguments(ctx, argv);
进入死循环直到检测到程序结束标记while(exit_flag==0);
mg_stop();
}

上面即是main函数中的主流程。需要注意的是调用mg_start()之后返回一个mg_context结构体的实例,这个实例将会在整个连接请求中用到,而且如果你在启动mongoose中设置了参数选项,在下面的process_command_line_arguments()函数中还会对ctx进行修改。从这里我们也知道了,mongoose程序的核心入口时mg_start(),最后终结于mg_stop()。

3、Mongoose的生命旅程

通过上面的分析我们知道Mongoose起始于mg_stop(),终结于mg_stop()。下面我们就从生命之初到生命终结之间的“故事”。说明:在这里我们不会去过于追究细节,只是串线式的把Mongoose的生命流程串起来,哪些细节或许后续的文章来解释,或者留给读者你去做了!

在mg_start()主要是做一些初始化的工作,最后才会正式进入工作服务于client。这里的初始化工作就好比一个人的出生需要十月怀胎,为诞生积蓄能量,要从受精卵长成一个完整的人。在准备工作完成之后,mg_start()会启动一个主线程master_thread,它用于监听所有的client连接请求。

启动一个主线程即启动了一个web server,在主线程中首先会将该server监听的地址(socket)加入到监听集合中去。然后一直监听该端口,只要有client的连接请求到来,它会调用accept_new_connection()去处理连接请求。

接下来,我们关注的是accept_new_connection()是如何去处理连接请求的。首先它会进行一些预判工作,决定是否允许该连接。如果允许,则调用put_socket()并将处理工作转交给它,所谓权力下放。

在put_socket()首先也会进行一些预判工作——判断mg_context结构体的成员变量queue队列是否已满,如果满了就等待直到queue有位置容纳请求。还有一点要说明的是:由于有可能多个client请求同时到达,对queue进行操作,所以在put_socket()中一开始就设置(void) pthread_mutex_lock(&ctx->thr_mutex);而且请求是通过调节变量来控制等待queue是否有位置容纳请求(void) pthread_cond_wait(&ctx->full_cond, &ctx->thr_mutex)。说了这么多准备工作,现在该正式进入工作了。至此,如果没有空闲进程且进程数量没有达到最大阈值,就会启动一个新的工作进程worker_thread去处理client的请求。之后就是释放信号量等资源,让其它client请求也能够请求到资源工作,如启动了一个工作进程去处理client请求,这时queue就空出一个位置了,它会调用pthread_cond_signal(&ctx->empty_cond)让等待的client请求知道queue中有位置了。最后就是释放put_socket()中一开始设置的锁,(void) pthread_mutex_unlock(&ctx->thr_mutex)。

到了这里,client的请求已经被分配打一个工作线程中去了。而且不同的client请求处理运行在不同的工作线程中,能够互不干扰。在worker_thread中,首先与client建立连接,只有连接上了才能为client服务。连接建立之后调用process_new_connection()去处理请求。处理完之后返回关闭连接,并通过信号机制告诉主线程,我的做工做完了。

在process_new_connection()中处理工作:首先解析请求parse_http_request(),知道请求的内容;接着就是进入Mongoose处理client请求的真正核心工作了analyze_request()。这里就不详细介绍parse_http_request()、analyze_request()是如何去解析、验证、提供具体服务的,否则就陷入了细节出不来了,这里主要是介绍Mongoose的生命之旅的主线。

下面用图形来形象描述一下Mongoose的生命之旅,说明:该图形并不是一个精确的逻辑关系,图中的箭头方向只是描述了程序的大概流程,并都是上级调用下级的关系,如并不是parse_http_request()调用analyze_request()等,而实际上它们都是在process_new_connection()被调用。

Mongoose源码剖析:mongoose的工作模型_生命旅程_02

图2、Mongoose的大概生命主线

4、总结

至此,算是介绍完了Mongoose的一个完整的工作模型了,你可以安装此主线去进行code review。只有你脑海里有这样一个模型,你就不会在研读代码是迷失了。

当然Mongoose提供的很多api,这里都没有介绍到,因为它不是本文的重点。我希望此能够带后来者步入Mongoose源码研读的大门,给后来者节省徘徊在门外停滞不前的时间。