lighttpd-1.4.39 : mod_staticfile
2016-02-25 20:28
549 查看
http://www.cnblogs.com/kernel_hcy/archive/2010/04/07/1706587.html
前面大概的介绍了一下lighttpd的状态机。在这篇中,将通过状态机,看看lighttpd到底是怎样处理连接请求的。
在本篇中,我们只介绍lighttpd的最基本功能──处理静态页面。lighttpd处理静态页面要使用mod_staticfile.c插件。从名字中也可以看出是用来处理静态文件的。另外这个插件在配置文件中没有配置,是lighttpd默认会加载的。
首先,连接建立以后,连接进入CON_STATE_REQUEST_START状态。在这个状态中,服务器仅仅是记录了做一些标记,如记录连接开始的时间,读操作发呆的时间等。接着,连接就进入了CON_STATE_READ状态。
在READ状态中,服务器从连接读取HTTP头并存放在con->requeset.request。在读取过程中,可能一次调用没有读取全部的数据,连接的状态继续停留在READ,然后,继续等待剩下的数据可读。由于这个连接同时也在作业队列中,在对作业队列进行处理的时候,依然会调用connecion_handle_read_state函数进行处理,不过,函数中通过con->is_readable来判断是否有数据可读,如果没有,则只是处理一下以前已经读取的数据。数据读取完毕之后,连接进入CON_STATE_REQUEST_END状态。
在REQUEST_END状态中,主要就是调用了
HTTP Request Message的格式:
| methods |sp| URL |sp| Version |\r|\n| —–> Request line (Request)
| header field name: |sp| value |\r|\n| -]
————————————— ]
// … |—> Header lines (Option)
————————————— ]
| header field name: |sp| value |\r|\n| -]
|\r|\n| —–> blank
| |
| Data… | —–> Entity body
| |
不熟悉的可以看看RFC2616。
函数首先解析Request line,解析出来的结果分贝存放在:con->request.http_method, con->request.http_version和con->request.uri
前两个变量都是枚举类型,后一个是个buffer。程序的第一个for循环解析完request line后,进入第二个for循环,这个循环很长,但做的工作很简单。就是分析header lines。在找到一个header field name以后,就和所有已经定义的field name比较,看看是哪个。确定之后,就将field name和value保存到con->request.headers中。request.headers是一个array类型变量,存放的是data_string类型数据。其中,data_string的key是filed name,value就是field的成员。解析完之后做一些检查工作。最后,判断此次连接是否有POST数据,如果有则读取POST数据,否则进入HANDLE_REQUEST。
在阅读这个函数的时候,读者可参考RFC2616中的规定。
在这里,我们假设有POST数据要读。这时候,连接进入READ_POST状态。其实,READ_POST状态的处理和READ状态一样,在connection_state_mechine函数中,可以看到这两个switch分支一样。主要区别是在connection_handle_read_state函数中。connection_handle_read_state函数的前半部分读取数据,大家都一样,在后面处理数据的时候是分开的。对于读取POST数据,由于数据可能很大,这时候可能会用到临时文件来存储。在程序中,作者对于小于64k的数据,直接存储在buffer中,如果大于64k则存储在临时文件中。在向临时文件写数据的时候,每个临时文件只写1M的数据。数据大于1M就在打开一个临时文件。POST数据保存在con->requeset_content_queue,这是一个chunkqueue类型的成员变量,它是chunk结构体的链表,定义如下:
unused成员也是一个链表,不过这个链表是栈的形式,unused指向栈顶。这个栈用来存储不用的chunk结构体,如果需要chunk,则先从这个栈中查找有无空闲的。也就是看栈顶是否是NULL。如果chunk不使用了,可以加到栈顶。这样可以减少内存分配的时间,提高程序的效率。这时一个很通用的小技巧。unused_chunks标记栈中有多少数据。
first和last分别指向链表的开头和结尾。
chunk的定义如下:
从名字可以看出,chunk用来表述一块存储空间。这个存储空间可能在内存中,也可能在文件中。mem成员指向内存中的存储空间。而file结构体则表示在文件中的存储空间。tpye成员标记这个块是内存的还是文件的。对于在内存中的存储空间,实际就是一个buffer。这个没什么说的。对于文件中的存储空间,程序首先会使用mmap函数将文件映射到内存中,mmap结构体的start成员保存映射到内存中的地址。然后,对于文件的操作就可以像使用内存一样。
关于这两个结构体的操作函数读者课自行查看。
当POST数据读取完毕之后,程序进入
上面的代码删除了一些代码,重点突出插件的调用。完整代码读者可自行查阅。下面我们开始分析。
首先我们先来看看HTTP协议,前面提到的HTTP Request Messag格式可以看出,在整个HTTP requset head中,只有url和methods可以用来确定请求所对应的插件。比如,对于cgi请求,url中的文件的扩展名肯定是配置文件中定义的,不会是.html。在http_response_prepare函数中,通过对url的解析,逐步的调用插件来处理。对url解析的结果存放在con->uri中。uri的定义如下:
uri的定义为:(scheme)://(authority)(path)?(query)#fragment。上面的结构体和这个定义对应。举个例子:
http://user:passwd@www.google.com/pages/index.html?key1=data1&key2=data2#frag
解析之后:
scheme = http
authority = user:passwd
path = www.google.com/pages/index.html
path_raw = 未进行解码的path
query = key1=data1&key2=date2
对于path_raw要说明一下。在浏览器向服务器发送url请求的时候,会对其中的保留字符和不安全字符进行编码(具体参见RFC2396),最长见的就是对汉字。编码的形式是% HEX HEX,一个%加两个十六进制的数字。服务器在接受到请求之后,要对这些编码过的字符进行解码。path_raw中保存的就是还没有解码的url,path保存的是已经解码过的url。
前面的程序已经解析了HTTP头,HTTP头中的request line中的uri被存储在con->request.uri中。在函数的一开始,对这个uri地址进行解析。对于uri中的query和fragment部分,服务器不需要进行处理,因此将这些部分提取后存储到con -> uri中,对fragment的处理时直接抛弃,因为fragment是浏览器使用的。当解析出url中的path之后,服务器调用插件的plugins_call_handle_uri_raw函数,在这个函数中,插件根据分析出的为解码的url path进行处理。如果没有插件进行处理,服务器接着调用哪个插件的plugins_call_handle_uri_clean函数,这个函数自然就是根据解码过的url path进行处理。这两个函数最典型的应用就是proxy服务器。proxy服务器根据解析出来的url地址直接将请求转发,不需要对请求进行处理。
当请求仍然没有被处理时,说明这个请求必须在要被处理。首先服务器调用插件的plugins_call_handle_docroot函数对处理请求时的根目录进行设置。对于不同种类的资源,可以设置不同的根目录,提供一个虚拟服务器。设置根目录也可以简化请求的url地址。
接着,服务器根据根目录和请求的url地址,拼接处资源在本机上对应的物理地址。比如,doc root = /abc/root, url path = /doc/index.html,得到的物理地址就是/abc/root/doc/index.html。然后服务器调用插件的plugins_call_handle_physical函数。这个函数根据得到的物理地址进行相应的处理。
接着,服务器调用插件的plugins_call_handle_subrequest_start函数和plugins_call_handle_subrequest函数进行最后的处理。
下面总结一下插件接口的作用:
1、 plugins_call_handle_uri_raw
在得到http头中,request line的url地址直接调用,此时的url地址没有解码。url地址中不包含query。
2、plugins_call_handle_uri_clean
对url地址进行解码之后调用。
以上两个函数典型的应用时proxy服务器。直接将请求转发。
3、plugins_call_handle_docroot
设置处理请求时的根目录。
4、plugins_call_handle_physical
得到请求资源在服务器上的物理地址,根据这个物理地址做相应的处理。
5、plugins_call_handle_subrequest_start
子请求处理开始。
6、plugins_call_handle_subrequest
处理子请求。
最后,连接进入CON_STATE_RESPONSE_START状态。进入这个状态之后,服务器根据处理得到的记过准备给客户端的response。包括准备response头和写数据。这在后面的文章中继续讨论。
前面大概的介绍了一下lighttpd的状态机。在这篇中,将通过状态机,看看lighttpd到底是怎样处理连接请求的。
在本篇中,我们只介绍lighttpd的最基本功能──处理静态页面。lighttpd处理静态页面要使用mod_staticfile.c插件。从名字中也可以看出是用来处理静态文件的。另外这个插件在配置文件中没有配置,是lighttpd默认会加载的。
首先,连接建立以后,连接进入CON_STATE_REQUEST_START状态。在这个状态中,服务器仅仅是记录了做一些标记,如记录连接开始的时间,读操作发呆的时间等。接着,连接就进入了CON_STATE_READ状态。
在READ状态中,服务器从连接读取HTTP头并存放在con->requeset.request。在读取过程中,可能一次调用没有读取全部的数据,连接的状态继续停留在READ,然后,继续等待剩下的数据可读。由于这个连接同时也在作业队列中,在对作业队列进行处理的时候,依然会调用connecion_handle_read_state函数进行处理,不过,函数中通过con->is_readable来判断是否有数据可读,如果没有,则只是处理一下以前已经读取的数据。数据读取完毕之后,连接进入CON_STATE_REQUEST_END状态。
在REQUEST_END状态中,主要就是调用了
http_request_parse函数。从函数名字就可以看出来是在解析request请求。request的请求格式如下:
HTTP Request Message的格式:
| methods |sp| URL |sp| Version |\r|\n| —–> Request line (Request)
| header field name: |sp| value |\r|\n| -]
————————————— ]
// … |—> Header lines (Option)
————————————— ]
| header field name: |sp| value |\r|\n| -]
|\r|\n| —–> blank
| |
| Data… | —–> Entity body
| |
不熟悉的可以看看RFC2616。
函数首先解析Request line,解析出来的结果分贝存放在:con->request.http_method, con->request.http_version和con->request.uri
前两个变量都是枚举类型,后一个是个buffer。程序的第一个for循环解析完request line后,进入第二个for循环,这个循环很长,但做的工作很简单。就是分析header lines。在找到一个header field name以后,就和所有已经定义的field name比较,看看是哪个。确定之后,就将field name和value保存到con->request.headers中。request.headers是一个array类型变量,存放的是data_string类型数据。其中,data_string的key是filed name,value就是field的成员。解析完之后做一些检查工作。最后,判断此次连接是否有POST数据,如果有则读取POST数据,否则进入HANDLE_REQUEST。
在阅读这个函数的时候,读者可参考RFC2616中的规定。
在这里,我们假设有POST数据要读。这时候,连接进入READ_POST状态。其实,READ_POST状态的处理和READ状态一样,在connection_state_mechine函数中,可以看到这两个switch分支一样。主要区别是在connection_handle_read_state函数中。connection_handle_read_state函数的前半部分读取数据,大家都一样,在后面处理数据的时候是分开的。对于读取POST数据,由于数据可能很大,这时候可能会用到临时文件来存储。在程序中,作者对于小于64k的数据,直接存储在buffer中,如果大于64k则存储在临时文件中。在向临时文件写数据的时候,每个临时文件只写1M的数据。数据大于1M就在打开一个临时文件。POST数据保存在con->requeset_content_queue,这是一个chunkqueue类型的成员变量,它是chunk结构体的链表,定义如下:
typedef struct { chunk *first; chunk *last; /** * 这个unused的chunk是一个栈的形式。 * 这个指针指向栈顶,而chunk中的next指针则将栈连 接起来。 */ chunk *unused; size_t unused_chunks; array *tempdirs; off_t bytes_in, bytes_out; } chunkqueue;
unused成员也是一个链表,不过这个链表是栈的形式,unused指向栈顶。这个栈用来存储不用的chunk结构体,如果需要chunk,则先从这个栈中查找有无空闲的。也就是看栈顶是否是NULL。如果chunk不使用了,可以加到栈顶。这样可以减少内存分配的时间,提高程序的效率。这时一个很通用的小技巧。unused_chunks标记栈中有多少数据。
first和last分别指向链表的开头和结尾。
chunk的定义如下:
typedef struct chunk { enum { UNUSED_CHUNK, MEM_CHUNK, FILE_CHUNK } type; /* 内存中的存储块或预读缓存 */ buffer *mem; /* either the storage of the mem-chunk or the read-ahead buffer */ struct { /* * filechunk 文件块 */ buffer *name;/* name of the file */ off_t start;/* starting offset in the file */ off_t length;/* octets to send from the starting offset */ int fd; struct { char *start;/* the start pointer of the mmap'ed area */ size_t length;/* size of the mmap'ed area */ off_t offset; /* start is <n> octet away from the start of the file */ } mmap; int is_temp;/* file is temporary and will be deleted if on cleanup */ } file; off_t offset;/* octets sent from this chunk the size of the * chunk is either -mem-chunk: mem->used - 1 file-chunk: file.length */ struct chunk *next; } chunk;
从名字可以看出,chunk用来表述一块存储空间。这个存储空间可能在内存中,也可能在文件中。mem成员指向内存中的存储空间。而file结构体则表示在文件中的存储空间。tpye成员标记这个块是内存的还是文件的。对于在内存中的存储空间,实际就是一个buffer。这个没什么说的。对于文件中的存储空间,程序首先会使用mmap函数将文件映射到内存中,mmap结构体的start成员保存映射到内存中的地址。然后,对于文件的操作就可以像使用内存一样。
关于这两个结构体的操作函数读者课自行查看。
当POST数据读取完毕之后,程序进入
CON_STATE_REQUEST_END状态。在这个状态中,主要调用了是
http_response_prepare函数,然后根据这个函数的返回值进行相应的处理。
http_response_prepare函数定义在response.c文件中。粗略的浏览一遍这个函数,你会发现函数中调用了很多
plugins_call_handle_xxxx函数。其实插件系统的接口函数主要是在这个函数中调用,这个函数也是服务器和插件系统交互的地方。函数定义如下:
handler_t http_response_prepare(server * srv, connection * con) { handler_t r; if (con->mode == DIRECT && con->physical.path->used == 0) { char *qstr; /** * Name according to RFC 2396* * (scheme)://(authority)(path)?(query)#fragment * fragment: 用于页面内部的跳转。通常不属于uri的一部分。 * 服务器没有你要对其进行解析,后面的解析中,直接丢弃fragment。 */ /* 对HTTP头中的uri进行解析,分析出各个部分的内容。 */ switch (r = plugins_call_handle_uri_raw(srv, con)) { case HANDLER_GO_ON: break; case HANDLER_FINISHED: case HANDLER_COMEBACK: case HANDLER_WAIT_FOR_EVENT: case HANDLER_ERROR: return r; default: log_error_write(srv, __FILE__, __LINE__, "sd", "handle_uri_raw: unknown return value", r); break; } /*do we have to downgrade to 1.0 ? */ if (!con->conf.allow_http11) { con->request.http_version = HTTP_VERSION_1_0; } switch (r = plugins_call_handle_uri_clean(srv, con)) { case HANDLER_GO_ON: break; case HANDLER_FINISHED: case HANDLER_COMEBACK: case HANDLER_WAIT_FOR_EVENT: case HANDLER_ERROR: return r; default: log_error_write(srv, __FILE__, __LINE__, ""); break; } if (con->request.http_method == HTTP_METHOD_OPTIONS && con->uri.path->ptr[0] == '*' && con->uri.path_raw->ptr[1] == '\0') { /*将key=val加到response的head中。*/ response_header_insert(srv, con, CONST_STR_LEN("Allow"),CONST_STR_LEN("OPTIONS, GET, HEAD, POST")); con->http_status = 200; con->file_finished = 1; return HANDLER_FINISHED; } /** *将请求地址转换成服务器的物理地址,也就是文件路径。 */ buffer_copy_string_buffer(con->physical.doc_root, con->conf.document_root); buffer_copy_string_buffer(con->physical.rel_path, con->uri.path); switch (r = plugins_call_handle_docroot(srv, con)) { case HANDLER_GO_ON: break; case HANDLER_FINISHED: case HANDLER_COMEBACK: case HANDLER_WAIT_FOR_EVENT: case HANDLER_ERROR: return r; default: log_error_write(srv, __FILE__, __LINE__, ""); break; } /** * create physical filename * -> physical.path = docroot + rel_path */ buffer_copy_string_buffer(con->physical.path, con->physical.doc_root); BUFFER_APPEND_SLASH(con->physical.path); buffer_copy_string_buffer(con->physical.basedir, con->physical.path); if (con->physical.rel_path->used && con->physical.rel_path->ptr[0] == '/') { buffer_append_string_len(con->physical.path, con->physical.rel_path->ptr + 1, con->physical.rel_path->used - 2); } else { buffer_append_string_buffer(con->physical.path, con->physical.rel_path); } switch (r = plugins_call_handle_physical(srv, con)) { case HANDLER_GO_ON: break; case HANDLER_FINISHED: case HANDLER_COMEBACK: case HANDLER_WAIT_FOR_EVENT: case HANDLER_ERROR: return r; default: log_error_write(srv, __FILE__, __LINE__, ""); break; } /* * Noone catched away the file from normal path of execution yet (like mod_access) * Go on and check of the file exists at all */ if (con->mode == DIRECT) { char *slash = NULL; char *pathinfo = NULL; int found = 0; stat_cache_entry *sce = NULL; if (HANDLER_ERROR != stat_cache_get_entry(srv, con, con->physical.path, &sce)) { /*file exists */ } else { /*not found, perhaps PATHINFO*/ /*we have a PATHINFO */ } switch (r = plugins_call_handle_subrequest_start(srv, con)) { case HANDLER_GO_ON: /*request was not handled */ break; case HANDLER_FINISHED: default: /* something strange happend */ return r; } /*if we are still here, no one wanted the file, status 403 is ok I think*/ if (con->mode == DIRECT && con->http_status == 0) { switch (con->request.http_method) { case HTTP_METHOD_OPTIONS: con->http_status = 200; break; default: con->http_status = 403; } return HANDLER_FINISHED; } } switch (r = plugins_call_handle_subrequest(srv, con)) { case HANDLER_GO_ON: /*request was not handled, looks like we are done */ return HANDLER_FINISHED; case HANDLER_FINISHED: /*request is finished */ default: /*something strange happend */ return r; } /*can't happen */ return HANDLER_COMEBACK; }
上面的代码删除了一些代码,重点突出插件的调用。完整代码读者可自行查阅。下面我们开始分析。
首先我们先来看看HTTP协议,前面提到的HTTP Request Messag格式可以看出,在整个HTTP requset head中,只有url和methods可以用来确定请求所对应的插件。比如,对于cgi请求,url中的文件的扩展名肯定是配置文件中定义的,不会是.html。在http_response_prepare函数中,通过对url的解析,逐步的调用插件来处理。对url解析的结果存放在con->uri中。uri的定义如下:
typedef struct { buffer *scheme; //http , https and so on buffer *authority; //user:password buffer *path; //www.xxx.com/xxx/xxxx.html buffer *path_raw; //www.xxx.com buffer *query; //key1=data1&key2=data2 } request_uri;
uri的定义为:(scheme)://(authority)(path)?(query)#fragment。上面的结构体和这个定义对应。举个例子:
http://user:passwd@www.google.com/pages/index.html?key1=data1&key2=data2#frag
解析之后:
scheme = http
authority = user:passwd
path = www.google.com/pages/index.html
path_raw = 未进行解码的path
query = key1=data1&key2=date2
对于path_raw要说明一下。在浏览器向服务器发送url请求的时候,会对其中的保留字符和不安全字符进行编码(具体参见RFC2396),最长见的就是对汉字。编码的形式是% HEX HEX,一个%加两个十六进制的数字。服务器在接受到请求之后,要对这些编码过的字符进行解码。path_raw中保存的就是还没有解码的url,path保存的是已经解码过的url。
前面的程序已经解析了HTTP头,HTTP头中的request line中的uri被存储在con->request.uri中。在函数的一开始,对这个uri地址进行解析。对于uri中的query和fragment部分,服务器不需要进行处理,因此将这些部分提取后存储到con -> uri中,对fragment的处理时直接抛弃,因为fragment是浏览器使用的。当解析出url中的path之后,服务器调用插件的plugins_call_handle_uri_raw函数,在这个函数中,插件根据分析出的为解码的url path进行处理。如果没有插件进行处理,服务器接着调用哪个插件的plugins_call_handle_uri_clean函数,这个函数自然就是根据解码过的url path进行处理。这两个函数最典型的应用就是proxy服务器。proxy服务器根据解析出来的url地址直接将请求转发,不需要对请求进行处理。
当请求仍然没有被处理时,说明这个请求必须在要被处理。首先服务器调用插件的plugins_call_handle_docroot函数对处理请求时的根目录进行设置。对于不同种类的资源,可以设置不同的根目录,提供一个虚拟服务器。设置根目录也可以简化请求的url地址。
接着,服务器根据根目录和请求的url地址,拼接处资源在本机上对应的物理地址。比如,doc root = /abc/root, url path = /doc/index.html,得到的物理地址就是/abc/root/doc/index.html。然后服务器调用插件的plugins_call_handle_physical函数。这个函数根据得到的物理地址进行相应的处理。
接着,服务器调用插件的plugins_call_handle_subrequest_start函数和plugins_call_handle_subrequest函数进行最后的处理。
下面总结一下插件接口的作用:
1、 plugins_call_handle_uri_raw
在得到http头中,request line的url地址直接调用,此时的url地址没有解码。url地址中不包含query。
2、plugins_call_handle_uri_clean
对url地址进行解码之后调用。
以上两个函数典型的应用时proxy服务器。直接将请求转发。
3、plugins_call_handle_docroot
设置处理请求时的根目录。
4、plugins_call_handle_physical
得到请求资源在服务器上的物理地址,根据这个物理地址做相应的处理。
5、plugins_call_handle_subrequest_start
子请求处理开始。
6、plugins_call_handle_subrequest
处理子请求。
最后,连接进入CON_STATE_RESPONSE_START状态。进入这个状态之后,服务器根据处理得到的记过准备给客户端的response。包括准备response头和写数据。这在后面的文章中继续讨论。
相关文章推荐
- HTTP协议中,GET方法与POST方法比较
- The superclass "javax.servlet.http.HttpServlet" was not found on the Java Build Path
- linux网络编程之shutdown() 与 close()函数详解
- iptables实现网络防火墙及地址转换
- iptables实现网络防火墙及地址转换
- tcp三次握手和time wait --- 转
- SSL中间人监测关键技术---SSL会话劫持
- HTTP相关
- iOS-原生网络请求
- 上下界网络流学习小记
- SDN软件定义网络
- iOS网络编程—NSURLSession的简单使用(iOS9)
- Node:Q与node http模块搭配
- Charles抓Https的包
- 详细介绍Ubuntu网络配置方法
- AS3 通过 TCP/IP 协议控制WatchOut播放
- Java 网络编程(五) 使用TCP/IP的套接字(Socket)进行通信
- socket客户端用例测试-HTTP
- Android APP架构的那点事儿[网络模块]
- 移动网络广告优化(速度优化篇)