Mongoose3.3嵌入式web服务器源码剖析

Mongoose3.3嵌入式web服务器源码剖析

一、基本原理

     Mongoose是一个跨平台的嵌入式web服务器,使用生产者和消费者的多线程模型来处理网络连接请求,吞吐量和效率较高。其中主要的代码是mongoose.c。Mongoose中有一个全局上下文context,保存基本的配置信息,包括端口号、访问控制列表等,还有实际创建的工作线程个数,socket缓冲队列,终止标志,互斥锁,条件变量,用户回调函数等。连接connection包括请求头信息,上下文,产生时间,已经发送的字节数,已经消耗的字节数,存放请求头数据的缓冲区,客户端socket,响应状态码,传送速度限制等。程序启动时,首先根据用户的设置初始化全局上下文,接下来的每一个连接使用的都是一个相同的上下文。默认情况下,模型为一个服务线程和20个工作线程。服务线程主要是遍历服务端的一系列监听端口,然后将到来的socket先进行包装,设置好本地地址和远程地址,如果客户端的IP地址能够通过访问控制列表的检测,则放入到等待队列缓冲区中,如果队列已经满了,则等待。由于工作线程有多个,当前工作线程首先要得到全局互斥锁,从等待队列中取走一个socket,然后释放互斥锁,如果没有则等待。然后初始化并建立一个connection,接着读取客户端的请求,解析请求头到connection,并作相应处理,合法的前提下交给handle_request来处理,这个方法能处理各种请求,包括GET,POST,PUT,DELETE,OPTIONS,PROPFIND,HEAD。涉及到对CGI的处理,主要是初始化环境变量,创建子进程,执行CGI解释程序,使用匿名管道进行通信,最后发送信号将子进程杀死。或者对请求目录下的所有目录项进行枚举,对文件内容进行获取,对文件或目录进行建立或者删除等。服务线程和每个工作线程不断地重复上述工作,直到context终止标志置位,程序终止,释放资源。

二、程序入口

1、概述

     启动模型的入口是

struct mg_context *mg_start(mg_callback_t user_callback, void*user_data, const char **options)

其中user_callback是新的连接,连接完成等事件发生时所要调用的处理函数句柄。options是用来初始化ctx->config[]数组的上下文全局配置信息。这个函数的主要功能是新建一个上下文context,并对config[]进行初始化,同时对初始化信息的正确性进行检查,建立socket监听链表。最后创建出一个服务线程和20个工作线程,返回ctx。

const char **options的格式如下,要求以NULL结尾。


2、流程图

 

注:框图中的箭头表示可能调用的函数,不一定存在先后关系,下同。

三、主线程

1、概述

主线程的执行体是maset_thread(struct mg_context *ctx)

主要的思想是当ctx->stop_flag == 0,也就是程序没有终止时,不断地遍历监听链表ctx->listening_sockets(没有空表头),如果该监听端口有新的连接来临,则使用accept_new_connection(listener,ctx)处理新的连接。该方法首先使用accept()初始化socket的远程地址和使用listener的地址作为本地地址,地址包括IP和端口。然后使用check_acl检查访问控制链表,对该IP的访问权限进行检查,若允许,则调用produce_socket,将新的连接请求放置到ctx->queue[]中(缓冲队列满时要等待),这段代码是临界代码。否则拒绝访问。主线程主要是等待连接,产生客户端socket。

2、流程图

 

四、工作线程

1、概述

工作线程的执行体是worker_thread(ctx)

首先为conn动态分配内存,其中conn->buf在conn后面,占MAX_REQUEST_SIZE个字节。然后不断调用consume_socket(ctx, &conn->client),从ctx->queue中取走一个socket,直到ctx->stop_flag == 1为止。获取到连接后,利用conn->client对连接conn的请求信息的远程端口和远程IP进行初始化,然后调用process_new_connection,最后调用close_connection()关闭连接。继续处理新的连接。process_new_connection()首先读取请求内容,获取请求头,然后对其进行解析,得到请求信息,并对请求URI的合法性进行验证,对请求的HTTP版本进行验证。全部合法后,根据是否有Content-Length这个请求头对ctx->content_len进行初始化。最后初始化连接的产生时间,调用handle_request(conn)对整个请求进行处理,完毕后调用call_user(conn, MG_REQUEST_COMPLETE)方法,表示请求结束,并调用log_access(conn)记录访问信息。如果允许keep_alive,则循环上述流程。

2、流程图

 

五、处理各种连接请求

1、概述

     对应的函数是handle_request(conn)

首先从conn->request_info.uri中获取到查询字符串,并初始化conn->request_info.query_string,不包含?。然后调用uri_decode(),对URI进行解码,最后调用convert_uri_to_file_name利用全局根目录将URI转化为服务端的绝对路径名,获取到该路径的状态st。调用set_throttle(),根据config[THROTTLE]中的键值对,对给客户端主机传送响应数据的速度进行限制,设置conn->throttle。然后利用check_authorization(conn, path)对path进行验证,首先根据请求的URI利用config[PROTECT_URI]中的键值对URI=password_file,找到对应的密码文件password_file。如果PROTECT_URI没有配置,或者没有找到对应的键值对,则利用config[GLOBAL_PASSWORD_FILE]作为验证的密码文件。若config[GLOBAL_PASSWORD_FILE]也为NULL,如果该请求是目录,则返回该目录下的.htpasswd作为密码文件。如果该请求是文件,则利用该文件的所在目录下的.htpasswd作为验证文件。默认情况下密码文件不存在,所以所有路径下的验证都能通过。否则,找到了密码文件,利用authorize(conn, fp)借助md5进行验证。然后,调用call_user(conn, MG_NEW_REQUEST)。

对于不同的请求,调用不同的方法,将依次往下执行,直到某个条件成立为止,方法体中对响应状态进行设置:

Ø OPTIONS: send_options(conn),把选项发送给对方。

Ø 不存在根目录,即config[DOCUMENT_ROOT] == NULL,发送404 Not Found

Ø 如果是PUT或DELETE请求,则检查conn->ctx->config[PUT_DELETE_PASSWORDS_FILE],若该项是NULL,则发送验证请求,否则还是利用该密码文件authorize()进行验证。验证通过,对于PUT请求,使用put_file(conn, path)在服务端建立文件或文件夹,对于DELETE请求,则调用mg_remove(path)删除对应的文件或目录。

Ø 如果path是目录,但URI不以/结尾,则发送301,表示目录永久移动。

Ø 如果是PROPFIND请求,则调用handle_propfind(conn, path, &st),若URI是目录且允许目录枚举,则发送请求目录下的所有目录项。否则只单独列举该请求目录。

Ø 如果path是目录,且该目录下没有默认的主文件(index.html等),若允许枚举目录则对目录进行枚举handle_directory_request(conn, path),否则发送403状态码,表示拒绝枚举目录。

Ø 如果目录下有主文件,则将该path代替为path/主文件。

Ø 如果定义了CGI且匹配,且是POST或者GET请求则调用handle_cgi_request(conn, path)。CGI先初始化环境变量(char* envp[],最后一项是NULL),包括请求头信息和查询子串,以name=value的形式,使用pipe()创建两个匿名管道,再利用环境变量和管道作为标准输入和标准输出流,fork出一个子进程,执行解释程序:

 execle(con->config[CGI_INTERPRETER]),与父进程进行通信。如果是POST  请求,父进程(写者)通过第一个管道将剩余的信息(body)发送给CGI,然后关闭写端。并用第二个管道从子进程读取处理后的数据,并对请求头进行 解析,发送数据给客户端。最后父进程调用kill(pid, SIGKILL),将子进程杀死。

Ø 如果请求头有If-Modified-Since或If-None-Match的字段,且文件未修改,则发送304,未修改。

Ø 如果上述都不满足,作为文件请求处理:handle_file_request(conn, path, &st),可能是GET,POST,HEAD请求,因为CGI可能不匹配,或者没有定义。

2、流程图

 

六、底层数据传输的实现机制

1、概述

      网络连接都是基于socket编程的,可以对应用层的数据进行自由设置。工作线程在处理请求时,使用recv(sock)从网络读取请求数据,使用send(sock)向网络发送响应数据。对于一般请求,如GET,只需解析请求头即可,如果是POST请求,则body一般是有内容的。如果是PUT请求,通常body会有建立文件的数据,也需要读取。对于发送数据,HTTP协议也有固定格式,我们更希望能够格式化输出,Mongoose对于IO操作做了不同层次的封装。

2、流程图

 

 

 

 

 Mongoose源码

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值