一、 前言
简介:Lighttpd是一个轻量级的Web 服务器,支持FastCGI, CGI, Auth, 输出压缩(output compress), URL重写, Alias等重要功能。它具有非常低的内存开销,cpu占用率低,效能好,以及丰富的模块等特点。其静态文件的响应能力远高于Apache,可谓Web服务器的后期之秀。
功能:接收、响应用户请求,网元间消息通信,资源传递等。
我将lighttpd理解为一个平台,提供了一整套开发流程以及各种高效的工具。使得我们在服务器功能开发时就像使用VC在现有工程中添加模块一样简单:
VC |
Lighttpd |
打开已有工程并添加新模块的源文件 |
添加空的插件,自定义插件名,将其加入到makefile中 |
编码,调用各种库函数完成对字符串、数组等数据的处理。 |
编码,调用各种库函数(array、buffer、 chunk等等)完成对字符串、数组等数据的处理。 |
编译、连接(点图标或按快捷键) |
编译、连接(执行makefile或封装好的脚本) |
运行、调试 |
运行、调试 |
Lighttpd架构的设计初衷,所有具体的业务功能都应由插件实现。插件之间通常是完全独立(无内联)的,我们可以根据需要选择性的加载所需插件。
因此我们在开发时主要也是针对业务插件的编写和修改以完成我们所需的功能。
二、 架构
1. 状态机
谈到架构就必须从状态机说起,Lighttpd启动时完成了Server实例初始化、默认配置读取(lighttpd.conf)、插件加载等初始化操作,之后就进入了一个包含11个状态的有限状态机中。每个连接(事件)都是一个connection实例(con)。状态的切换完全取决于con->state。
※ 我们在插件中对事件的处理就是针对这个con实例;lighttpd经过初步处理后将con的基本信息初始化;插件拿到con后按照业务需要进行相应处理,然后再交还给lighttpd;lighttpd根据con中的信息完成响应。至此一次请求(事件)结束。
typedef enum {
CON_STATE_CONNECT,
CON_STATE_REQUEST_START,
CON_STATE_READ,
CON_STATE_REQUEST_END,
CON_STATE_READ_POST,
CON_STATE_HANDLE_REQUEST,
CON_STATE_RESPONSE_START,
CON_STATE_WRITE,
CON_STATE_RESPONSE_END,
CON_STATE_ERROR,
CON_STATE_CLOSE
} connection_state_t;
Lighttpd状态转换图
2. 插件
我们编写的插件会注册15(或其中的几个)回调接口(1.4.20)。lighttpd在初始化阶段、状态机执行阶段、退出阶段会分别调用这些回调函数,完成插件的实例化,初始化,连接重置,事件处理,插件释放等功能。它们分别是:
名称 |
handle_uri_raw |
handle_uri_clean |
handle_subrequest_start |
handle_subrequest |
handle_request_done |
handle_docroot |
handle_physical |
handle_connection_close |
handle_joblist |
connection_reset |
handle_trigger |
handle_sighup |
init |
set_defaults |
cleanup |
前面提到过,我们的开发工作主要集中在编写业务插件上,而插件被调用的方式就是通过上述这些接口;我们的插件与lighttpd都在针对本次事件con进行相关操作。因此我们需要关注以下几点:
这些函数的执行顺序、执行位置、执行次数和执行条件;
接口调用时我们有哪些数据可用;(con实例下的哪些数据有效)
接口内我们应该完成的工作;(填写con实例下的哪些数据,返回什么值)
3. 状态机与插件
二者联系:
事件所处状态机中的各个状态,lighttpd都会对事件进行相应处理。有些是lighttpd自身做的,有些是通过调用插件完成的。插件中那些负责事件处理的接口分布在某几个状态中。
我们只需在插件的各个阶段完成指定工作并返回相应的返回值,就可以促使状态机完成状态切换,完成事件的整套处理流程,并最终由lighttpd完成事件的响应。
例如:
在CON_STATE_REQUEST_END状态,lighttpd对请求进行了解析,并获得了con->request.http_method,con->request.uri,con->request.http_host等重要数据。——lighttpd自身对con的处理。
在CON_STATE_HANDLE_REQUEST状态,lighttpd除了自身对con中数据处理外还调用了插件的接口:handle_uri_raw;handle_uri_clean;handle_docroot;handle_physical;handle_subrequest_start;handle_subrequest。——插件对con的处理。
插件处理:
在CON_STATE_HANDLE_REQUEST状态,lighttpd调用了插件的handle_uri_raw接口。但是我们有几十个插件,每个插件都注册了handle_uri_raw这个接口。如何让我们指定的插件来完成对事件数据的处理而不受其他插件的影响呢?
lighttpd对插件的调用方式:在lighttpd中每次需要调用插件某一个阶段的接口函数时,会对所有插件注册在该位置接口顺序调用,顺序与插件加载顺序相同。例如:调用uri_raw接口,会先调用A插件的mod_A_uri_raw,然后调用B插件的mod_B_uri_raw,直到将所有已加载插件这个位置的接口全部调用完成。但实际处理这次事件通常只有一个插件,这里引出一个重要概念——事件接管。每个事件都有一个mode字段(con->mode)。该字段的定义:
typedef enum { DIRECT, EXTERNAL } connection_type;
连接对象有一个字段mode用来标识该连接是最初由服务器accept产生的客户端连接还是插件产生的其他辅助连接,当mode=DIRECT时表示对应连接由lighttpd服务器accept产生,mode!=DIRECT时表示对应连接是由插件产生的。——网络释义
通过搜索代码得到以下信息:
事件(con)初始化时mode是DIRECT;connection_reset(srv,con);
lighttpd在大部分流程中还会在入口判到mode != DIRECT时就直接返回GO_ON。可以理解为:此事件由用户插件接管,lighttpd不参与。
用户编写的插件应通过将mode置为插件自身的ID达到接管的作用。插件ID是在插件加载时由插件的加载顺序确定的,是插件的唯一标识。
用户编写插件在每个接口的一开始应该判断mode是否等于自身的ID,若相等才能继续执行,否则直接退出,返回GO_ON。
※ 我曾遇这样一种情况,在自己编写的插件中将响应码设置为所需值后返回FINISH,本期望lighttpd可以直接构造所需响应并把响应码返回给用户。可实际在用户侧抓包看到的返回值却是其他值,肯定是由lighttpd或其他插件改写过。经专家指点,把mode赋值为DIRECT后结果才得以正确返回。因此在某些特定阶段,如果插件想要直接停止事件的处理并返回(通常是出错返回),还需要将mode置回为DIRECT,令lighttpd及时接管并处理。(原因在之后的源码学习中会补充上)
4. 各状态工作简述
Lighttpd事件处理的返回值定义:
typedef enum {
HANDLER_UNSET,
HANDLER_GO_ON,
HANDLER_FINISHED,
HANDLER_COMEBACK,
HANDLER_WAIT_FOR_EVENT,
HANDLER_ERROR,
HANDLER_WAIT_FOR_FD
} handler_t;
这里针对各状态的主要内容作一简单描述:
CON_STATE_CONNECT
清除待读取队列中的数据chunkqueue_reset(con->read_queue);
置con->request_count = 0。(本次连接还未处理过请求)
CON_STATE_REQUEST_START /*transient */
记录事件起始时间;
con->request_count++(一次长连接最多可以处理的请求数量是有限制的);
转移到CON_STATE_READ状态。
CON_STATE_READ和CON_STATE_READ_POST
connection_handle_read_state(srv,con);
CON_STATE_REQUEST_END /*transient */
http_request_parse(srv, con);解析请求,若是POST请求则转移到CON_STATE_READ_POST状态,否则转移到CON_STATE_HANDLE_REQUEST状态。
CON_STATE_HANDLE_REQUEST
http_response_prepare(srv, con);函数中调用了handle_uri_raw;handle_uri_clean;handle_docroot;handle_physical;handle_subrequest_start;handle_subrequest。
如果函数返回了HANDLER_FINISHED,且con->mode!=DIRECT(事件已经被我们的业务插件接管),则直接进入CON_STATE_RESPONSE_START。否则lighttpd会做一些处理后再进入CON_STATE_RESPONSE_START状态。
如果函数返回了HANDLER_WAIT_FOR_FD或HANDLER_WAIT_FOR_EVENT,状态依旧会停留在CON_STATE_HANDLE_REQUEST,等待事件或数据。
如果函数返回了HANDLER_ERROR,进入到CON_STATE_ERROR状态。
CON_STATE_RESPONSE_START
connection_handle_write_prepare(srv,con);
CON_STATE_WRITE
connection_handle_write(srv,con);
CON_STATE_RESPONSE_END
调用插件的handle_request_done接口。
如果是长连接,重新回到CON_STATE_REQUEST_START;否则调用插件的handle_connection_close接口。
执行connection_close(srv, con);和connection_reset(srv, con);将连接关闭。
CON_STATE_ERROR /* transient */
调用插件handle_request_done;
调用插件handle_connection_close;
执行connection_close将连接关闭。
CON_STATE_CLOSE
connection_close(srv, con);将连接关闭。
三、 源码学习:(待续)
这里向大家推荐一个博客http://www.cnblogs.com/kernel_hcy/category/218768.html,对lighttpd的入门很有帮助。
http://www.lighttpd.net/这是lighttpd的官方网站。需要权威的、官方的说明就去看看。可以下到各个版本的lighttpd源码,周末跟家可以搭个服务器学习学习。
1. 概述
Lighttpd (1.4.20)中包含.c和.h文件供134个,共计代码量3.8+w。其余还有makefile,conf,doc,可执行文件等。
比较独立的有:
以mod_开头的是插件,共45个;
基础数据结构和配套方法有Array,Buffer,Chunk,Bitset,Etag等;
数据校验有MD5和CRC;
Log.c/h,日志相关;
以data_开头的数据操作方法6个;
非常重要的有:
2. 代码
这里只对两三个重要的函数进行了研读,等以后有机会慢慢补充。
Request.c
inthttp_request_parse(server *srv, connection *con)
只在CON_STATE_REQUEST_END状态调用,此时插件处理流程还未开始。在此函数中完成请求的合理性判断,完成用户请求的最基本解析:获取con->request.http_method,con->request.uri,con->request.http_host。
解析请求的第一行,应该是<method> <uri> <protocol>\r\n格式
请求行不符合规定格式回复400;
请求方法不可识别回复501,方法识别完成保存在con->request.http_method中;
协议版本不是1.1或1.0则回复505;
将http://xxx.xx.xx.xx/中的x区域忽略掉,后面的部分保存在con->request.uri中;
……
是否为长连接,置con->keep_alive的值;
有些方法需要配合相应头域,若没有配套头域则返对应错误;GET和HEAD不能有content-length;POST则必须有content-length;
完成con->request.http_host 的赋值,由上面解析获得。
Response.c
handler_thttp_response_prepare(server *srv, connection *con)
只在CON_STATE_HANDLE_REQUEST状态调用,此时插件处理流程还未开始。
l /* looks like someone hasalready done a decision */
if (con->mode == DIRECT &&(con->http_status != 0 && con->http_status != 200));满足上述3个条件,return HANDLER_FINISHED;可以理解为连接是由lighttpd内部产生的,并且经过之前的处理已经得到了最终结果(非0——被处理过,非200)。
其中这段需要学习con->file_finished标志
/* remove apackets in the queue */
if(con->file_finished == 0) {
chunkqueue_reset(con->write_queue);
}
con->uri.scheme赋值;若采用SSL设置为https,否则设置为http。
con->uri.authority赋值;直接使用了con->request.http_host。
进行了一系列的config_patch_connection()操作,具体还需再深入看。
con->uri.path_raw和con->uri.query赋值;将请求中con->request.uri的'#'后面部分去除。将con->request.uri的'?'(如果存在)后面的部分保存在con->uri.query中;然后将剩下的字符串保存在con->uri.path_raw中。
if (con->request_count >con->conf.max_keep_alive_requests);判断如果当前长连接处理的请求数量超过了lighttpd.conf中配置的最大数,在本次处理结束后将连接断开。con->keep_alive = 0;
con->uri.path赋值;如果是OPTIONS方法并且只是一个'*',则不需要解码,直接使用con->uri.path_raw对con->uri.path赋值;如果不是前面所描述的方法和格式,则需要进行解码处理buffer_urldecode_path,将con->uri.path_raw中的%20转换为空格符' '。然后将转换后的字符串赋值给con->uri.path。
※ 至此,经过lighttpd对请求中uri的最基本处理,我们得到了完整的con->uri,可以在插件中通过对con->uri的分析和处理完成所需功能;uri是request_uri结构体实例。具体定义见下:
typedef struct {
buffer*scheme;
buffer*authority;
buffer*path;
buffer*path_raw;
buffer*query;
} request_uri;
调用插件的uri_raw;plugins_call_handle_uri_raw(srv, con);如果返回GO_ON,则继续向下执行;否则直接将结果return,退出本函数。
调用插件的uri_clean;plugins_call_handle_uri_clean(srv, con); 如果返回GO_ON,则继续向下执行;否则直接将结果return,退出本函数。
※ 从代码中可见uri_raw到uri_clean之间lighttpd本身并没有在插件外做额外操作。我们应该在uri_raw中根据uri的特征决定该交由哪个业务插件进行处理;在uri_clean中对uri按我们的需要进行处理,以满足后面通过uri进一步获取资源在Server中的物理路径。
如果是OPTIONS方法并且con->uri.path_raw只是'*',则调用response_header_insert(),设置返回状态200(con->http_status = 200;),并置con->file_finished=1,return FINISH。
typedef struct {
buffer*path;
buffer*basedir; /* path ="(basedir)(.*)" */
buffer*doc_root; /* path = doc_root + rel_path */
buffer*rel_path;
buffer*etag;
} physical;
con->physical.doc_root和con->physical.rel_path赋初值。最终值应该在下面的docroot插件中设置。将con->physical.doc_root设置为conf中的con->conf.document_root;将con->physical.rel_path设置为前面获得的con->uri.path;
调用插件的docroot;plugins_call_handle_docroot(srv, con);在docroot中完成con->physical.doc_root的设置;如果返回GO_ON,则继续向下执行;否则直接将结果return,退出本函数。
※ 关于docroot的含义可参见http://www.karelia.com/sandvox/help/z/Document_Root.html。
判断在之前的docroot插件中是否设置了con->server_name,若没有设置则使用con->uri.authority作为con->server_name。
con->physical.basedir赋值;使用con->physical.doc_root对其赋值并通过处理保证以字符'/'结尾。
con->physical.path赋值;path =doc_root + rel_path。
※ 上面步骤完成的工作:设置请求资源的根目录,可用默认值也可在插件中重新设置;再根据基准目录+用户请求中的uri完成资源的物理路径定位,保存在con->physical.path。
调用插件的physical;plugins_call_handle_physical(srv, con);如果返回GO_ON,则继续向下执行;否则直接将结果return,退出本函数。
如果在physical阶段还是返回了GO_ON,lighttpd认为No one catched away the file fromnormal path of execution yet (like mod_access)。
※ 根据这里的注释反推,在插件的physical阶段,业务插件应该根据文件的物理路径获取文件,构建响应并返回FINISH。
lighttpd判断此事件是否已被用户的业务插件接管(con->mode==DIRECT)。如果没被接管,lighttpd自己要亲自去访问这个文件,如果没有权限返回403;如果不存在返回404等等。这里还会调到插件的plugins_call_handle_subrequest_start(srv, con);具体lighttpd做了什么目前没深入看,涉及到了Etag。
如果已被用户的业务插件接管,lighttpd不会做上一步的处理,而是直接调用插件plugins_call_handle_subrequest(srv, con);此处返回GO_ON和FINISH都会向上返回FINISH结果。其他返回值则如实返回。
函数结束
Connections.c
connection_state_machine()
前面所说的状态机。函数中主要是一个大循环while(done == 0)。
两个控制循环走向的标志:
int done=0;
size_t ostate = con->state;
循环最后有一个对标志的改写:
if (done == -1)
{
done = 0;
}
else if (ostate == con->state)
{
done = 1;
}
综上,循环达成的条件是:done没有被改为-1并且con->state没有改变(连接状态没有改变)。
done和con->state是理解这个函数需要时刻关注的变量,同时con->state是状态机的控制变量。switch (con->state){包含了11个connection_state_t的元素和default}
done在循环中只会被赋值为-1,且只在连接状态为CON_STATE_HANDLE_REQUEST时才会发生。待将上面的标志控制与状态中的返回值对应
static intconnection_handle_write_prepare(server *srv, connection *con)
此时如果con->http_status == 0,将其改为403。
如果con->http_status是204或205或304,此时没有内容需要返回,构建的响应中只有响应头即可,进行一些清理工作后将con->file_finished置为 1;如果不是以上三种返回值则需再判con->mode是否是DIRECT。如果不是DIRECT证明业务插件已做好相应处理,直接进入下一步;是DIRECT的情况下,如果不是4xx或5xx响应,也直接进入下一步。
…………………………
……………………
………………
…………
……