概述
10.1 概述
熟悉UNIX/Linux网络编程的读者知道,在编写网络通信程序的时候离不开这几个系统调用:如socket()、bind()、listen()、connect()、accept()、write()/read()、close()等。作为Web服务器网络应用程序,Lighttpd当然也毫不例外地要调用这些系统函数来接受客户端请求,提供资源服务。在第8章里,我们跳过了监听描述符的创建过程而直接详细分析了在监听描述符上的事件管理,本章将讲叙包括如何创建监听描述符以及监听描述符接收到客户端请求后如何处理等相关内容,使得读者对Lighttpd网络服务响应请求流程有清晰透彻的理解。
本节相关部分源码:
base.h
server.c
network.c
10.2 简单网络服务通信模型
在开始对Lighttpd的服务通信分析之前,我们先来看一下简单的网络服务通信模型,如图10-1所示,该模型是单进程阻塞并发服务模型。
这个模型虽然简单,但是其已经将基于TCP的socket编程表现得很清楚。在服务器端和客户端进行实际的消息传递通信之前,必须在这两端之间建立起可信任的连接以及相互之间获取两端的相关信息,该连接通过如下步骤来建立。
首先是服务器端调用系统函数socket()<sup>73</sup>建立套接口描述符,同时指定期望的通信协议类型,比如TCP、UDP或UNIX域字节流等,利用socket()创建的描述符还需要被命名以便其他网络设备(如客户端)可以找到这个套接口,因此接下来调用bind()函数将套接口描述符与某个协议地址(一般就是本地主机地址加上本地端口号)关联起来。有名字的套接口可以很轻易地让其他远程端找到,但是如要要让该套接口能接收远程端的连接请求还需调用listen()函数将其转换为被动套接口,这是因为socket()创建的套接口被默认为主动套接口,即用来主动连接其他套接口的。被动套接口通知内核为其接收远程端发来的连接请求,此时内核为其准备两个连接队列:一个为未完成三次握手协议的连接队列,一个为已完成三次握手协议的连接队列(这两个队列总大小由listen()函数的第二个参数指定。另外,从这里可以看到三次握手协议由内核自动完成)。服务器进程接下来调用系统函数accept()接受内核准备好了的远程客户连接请求(即从已完成三次握手协议的连接队列中取出队头项连接),如果没有远程客户连接请求或者内核还没有完成连接的创建(此时已完成三次握手协议的连接队列为空)则函数accept()调用阻塞。只要函数accept()被内核准备的已建立的客户端连接唤醒,它就将为服务器进程获取当前连接的对端信息(比如协议地址等)。通过这些步骤,两端之间的可信任的连接就建立起来,而相互之间的信息也都知道,此时服务进程fork()一个和自己完全一样的子进程为该连接服务,而服务器端父进程继续等待下一个连接请求(由于监听描述符和连接描述符在子进程也会被复制,但是监听描述符对于子进程没有用,因此需要关闭,同样连接描述符对于父进程也没有用而需要关闭)。子进程可以通过系统调用read()/write()传递信息,当所有信息传递完毕之后,子进程和客户端进程双方调用close()退出进程执行。
上面描述的是服务器端情况,对于客户端情况就简单多了,客户端利用socket()创建的套接口通过系统调用connect()建立与服务器的连接,这个连接的建立过程也就是所谓的三次握手协议。对于客户端有两点值得说明:第一,客户端套接口调用connect()函数前不必调用bind()函数为该套接口命名,因为如果需要,内核会自动确定源IP地址并且选择一个临时端口作为源端口;第二,既然客户端要连接服务器端,所以必须要知道服务器的套接口名称,即IP地址和端口。IP地址一般通过域名解析可得,而端口服务器就得选择熟知端口(如HTTP的80端口)才能让更广泛的客户端连接进来。
10.3 Lighttpd网络服务通信模型
10.3.1 通信模型总图
上一节给出的简单网络服务通信模型并非是毫无理由的摆设,它是我们认识Lighttpd网络服务通信模型的基础。图10-2给出了Lighttpd网络服务通信模型的总图(以单进程且选择SELECT多路I/O复用为例)。
图 10-2 Lighttpd网络服务通信模型
从图10-2中可以看到Lighttpd网络服务通信模型和上一节给出的简单网络服务通信模型相比,在建立监听套接口描述符之前的步骤都是完全一样的,而之后由于采用了I/O复用技术却又大为不同。虽然Lighttpd网络服务通信也是单进程阻塞并发服务模型,但是其阻塞的位置不再是accept()函数处而是复用select()函数点,而且此处的select()函数并不会将进程永远阻塞,它将等待一段时间(如1秒)内的事件,如果没有事件发生则该函数超时返回,让进程可以处理一些其他事情,如管理流量控制、清除超时连接等。当有事件发生的情况下,如果是监听描述符上的事件发生则调用accept()函数接受远程连接获取信息建立连接套接口描述符并加入到I/O复用,可以认为此处的accept()函数不会阻塞,因为我们已经知道监听描述符上有发生事件。对于其他连接套接口描述符发生的事件,进程也会进行处理,如读写操作,所有这些事件处理完后,进程继续回到select()函数等待下一次事件。从整个这个过程看到,Lighttpd网络服务通信模型中所有的事件都是由主进程自己处理并没有像图10-1中的模型那样fork()一个子进程对连接套接口单独处理,这样做的好处就是节约系统资源,提高效率。
10.3.2 通信模型源码分析
在这一节我们将讲解Lighttpd中网络服务通信模型的具体实现源码,这里包括三个分步过程讲解:第一个过程为Lighttpd监听套接口描述符的创建过程(至于如何加入到I/O复用监控,第9章已经详细讲解,因此本节不再重复);第二,监听套接口发生请求连接事件后建立连接套接口描述符以及将其加入I/O复用监控的过程;第三,连接套接口描述符发生请求服务事件后的响应过程。
1.创建监听套接口描述符
如果忽略众多细节,创建监听套接口描述符的过程非常简单,仅涉及两个源文件中的三个函数。
首先是源文件server.c中main()函数的network_init()函数调用,这个调用出现了两次,但是刚好分别出现在if...else...语句的不同分支里,因此虽然network_init()函数被调用了两次但它却会且仅会执行一次,如清单10-1所示。
清单10-1 主程序中调用函数network_init
//server.c
1.if(i_am_root){
2.//……省略……
3.if(0!=network_init(srv)){
4.plugins_free(srv);
5.server_free(srv);
6.return-1;
7.}
8.//……省略……
9.}else{
10.//……省略……
11.if(0!=network_init(srv)){
12.plugins_free(srv);
13.server_free(srv);
14.return-1;
15.}
16.}
network_init()函数内没有创建监听套接口描述符的实际动作,但是它主要用于获取那些用户配置的将要被创建的监听套接口信息,这包括主Web站点和基于IP/端口的虚拟主机Web站点。
清单10-2 函数network_init
//network.c
17.int network_init(server*srv){
18.buffer*b;
19.size_t i;
20.network_backend_t backend;
/*类似于之前讲过的I/O复用技术的选择,这里Lighttpd也按照所谓的优劣次序选择服务器向客户端发送数据的方式。这些方式包括有所谓的“零拷贝”方式(这是目前性能最优的网络数据传递方式,我将在后面章节试图讲解它)、散布读/聚集写方式以及一般的读写方式。*/
21.struct nb_map{
22.network_backend_t nb;
23.const char*name;
24.}network_backends[]={
25./*lowest id wins*/
26.#if defined USE_LINUX_SENDFILE
27.{NETWORK_BACKEND_LINUX_SENDFILE,"linux-sendfile"},
28.#endif
29.#if defined USE_FREEBSD_SENDFILE
30.{NETWORK_BACKEND_FREEBSD_SENDFILE,"freebsd-sendfile"},
31.#endif
32.#if defined USE_SOLARIS_SENDFILEV
33.{NETWORK_BACKEND_SOLARIS_SENDFILEV,"solaris-sendfilev"},
34.#endif
35.#if defined USE_WRITEV
36.{NETWORK_BACKEND_WRITEV,"writev"},
37.#endif
38.{NETWORK_BACKEND_WRITE,"write"},
39.{NETWORK_BACKEND_UNSET,NULL}
40.};
/*创建主Web站点的监听套接口描述符,绑定的IP地址由配置项server.bind指定,端口由配置项server.port指定。如果用户未指定配置项,server.bind则自动绑定到通配地址INADDR_ANY,而配置项server.port未指定则默认为HTTP常规端口80。
示例:
server.bind="127.0.0.1"
server.port=3000*/
41.b=buffer_init();
42.buffer_copy_string_buffer(b,srv->srvconf.bindhost);
43.buffer_append_string_len(b,CONST_STR_LEN(":"));
44.buffer_append_long(b,srv->srvconf.port);
/*调用函数network_server_init()实际创建监听套接口描述符。*/
45.if(0!=network_server_init(srv,b,srv->config_storage[0])){
46.return-1;
47.}
48.buffer_free(b);
49.#ifdef USE_OPENSSL
50.srv->network_ssl_backend_write=network_write_chunkqueue_openssl;
51.#endif
52./*get a usefull default*/
53.backend=network_backends[0].nb;/*选择系统支持的最好的数据读写方式。*/
54./*match name against known types*/
55.if(!buffer_is_empty(srv->srvconf.network_backend)){/*用户的实际选择。*/
56.for(i=0;network_backends[i].name;i++){
57./**/
58.if(buffer_is_equal_string(srv->srvconf.network_backend,
59.network_backends[i].name,strlen(network_backends[i].name))){
60.backend=network_backends[i].nb;
61.break;
62.}
63.}
64.if(NULL==network_backends[i].name){
/*用户选择了一个无效的读写方式。*/
65./*we don't know it*/
66.log_error_write(srv,__FILE__,__LINE__,"sb",
67."server.network-backend has a unknown value:",
68.srv->srvconf.network_backend);
69.return-1;
70.}
71.}
72.switch(backend){/*根据最终选择的数据读写方式,关联回调函数。*/
73.case NETWORK_BACKEND_WRITE:
74.srv->network_backend_write=network_write_chunkqueue_write;
75.break;
76.#ifdef USE_WRITEV
77.case NETWORK_BACKEND_WRITEV:
78.srv->network_backend_write=network_write_chunkqueue_writev;
79.break;
80.#endif
81.#ifdef USE_LINUX_SENDFILE
82.case NETWORK_BACKEND_LINUX_SENDFILE:
83.srv->network_backend_write=network_write_chunkqueue_linuxsendfile;
84.break;
85.#endif
86.#ifdef USE_FREEBSD_SENDFILE
87.case NETWORK_BACKEND_FREEBSD_SENDFILE:
88.srv->network_backend_write=network_write_chunkqueue_freebsdsendfile;
89.break;
90.#endif
91.#ifdef USE_SOLARIS_SENDFILEV
92.case NETWORK_BACKEND_SOLARIS_SENDFILEV:
93.srv->network_backend_write=network_write_chunkqueue_solarissendfilev;
94.break;
95.#endif
96.default:
97.return-1;
98.}
/*check for$SERVER["socket"]*/
/*基于IP/端口的虚拟主机Web站点监听套接口描述符。
配置项正确格式如下:
$SERVER["socket"]=="127.0.0.1:3001"{...}
*/
/*0下标元素保存的是基本全局配置信息,因此从索引1开始。*/
99.for(i=1;i<srv->config_context->used;i++){
100.data_config*dc=(data_config*)srv->config_context->data[i];
101.specific_config*s=srv->config_storage[i];
102.size_t j;
103./*not our stage*//*不是虚拟主机配置项。*/
104.if(COMP_SERVER_SOCKET!=dc->comp)continue;
105.if(dc->cond!=CONFIG_COND_EQ){
/*对于虚拟主机配置项只运行相等比较。*/
106.log_error_write(srv,__FILE__,__LINE__,"s",
107."only==is allowed for$SERVER["socket"].");
108.return-1;
109.}
110./*check if we already know this socket,
111.*if yes,don't init it*/
/*字段srv->srv_sockets为server_socket_array结构体类型,相关结构体都定义在头文件base.h内:
//base.h
typedef struct{
server_socket**ptr;
size_t size;
size_t used;
}server_socket_array;
typedef struct{
sock_addr addr;
int fd;
int fde_ndx;/*-1表示未注册到I/O多路复用中。*/
buffer*ssl_pemfile;
buffer*ssl_ca_file;
buffer*ssl_cipher_list;
unsigned short ssl_use_sslv2;
unsigned short use_ipv6;
unsigned short is_ssl;
/*ipv4:port
[ipv6]:port*/
buffer*srv_token;
#ifdef USE_OPENSSL
SSL_CTX*ssl_ctx;
#endif
unsigned short is_proxy_ssl;
}server_socket;
typedef union{
#ifdef HAVE_IPV6
struct sockaddr_in6 ipv6;
#endif
struct sockaddr_in ipv4;
#ifdef HAVE_SYS_UN_H
struct sockaddr_un un;
#endif
struct sockaddr plain;
}sock_addr;
///usr/include/netinet/in.h
/*Structure describing an Internet socket address.*/
struct sockaddr_in
{
__SOCKADDR_COMMON(sin_);
in_port_t sin_port;/*Port number.*/
struct in_addr sin_addr;/*Internet address.*/
/*Pad to size ofstruct sockaddr'.*/
unsigned char sin_zero[sizeof(struct sockaddr)-
__SOCKADDR_COMMON_SIZE-
sizeof(in_port_t)-
sizeof(struct in_addr)];
};
/*Internet address.*/
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
/*Ditto,for IPv6.*/
struct sockaddr_in6
{
__SOCKADDR_COMMON(sin6_);
in_port_t sin6_port;/*Transport layer port#*/
uint32_t sin6_flowinfo;/*IPv6 flow information*/
struct in6_addr sin6_addr;/*IPv6 address*/
uint32_t sin6_scope_id;/*IPv6 scope-id*/
};
/*IPv6 address*/
struct in6_addr
{
union
{
uint8_t__u6_addr8[16];
#if defined__USE_MISC||defined__USE_GNU
uint16_t__u6_addr16[8];
uint32_t__u6_addr32[4];
#endif
}__in6_u;
#define s6_addr__in6_u.__u6_addr8
#if defined__USE_MISC||defined__USE_GNU
#define s6_addr16__in6_u.__u6_addr16
#define s6_addr32__in6_u.__u6_addr32
#endif
};
extern const struct in6_addr in6addr_any;/*::*/
extern const struct in6_addr in6addr_loopback;/*::1*/
#define IN6ADDR_ANY_INIT{{{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}}}
#define IN6ADDR_LOOPBACK_INIT{{{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}}}
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
///usr/include/sys/un.h
/*Structure describing the address of an AF_LOCAL(aka AF_UNIX)socket.*/
struct sockaddr_un
{
__SOCKADDR_COMMON(sun_);
char sun_path[108];/*Path name.*/
};
///usr/include/bits/socket.h
/*Structure describing a generic socket address.*/
struct sockaddr
{
__SOCKADDR_COMMON(sa_);/*Common data:address family and length.*/
char sa_data[14];/*Address data.*/
};
///usr/include/bits/sockaddr.h
/*POSIX.1g specifies this type name for thesa_family'member.*/
typedef unsigned short int sa_family_t;
/*This macro is used to declare the initial common members
of the data types used for socket addresses,struct sockaddr',
struct sockaddr_in',struct sockaddr_un',etc.*/
#define__SOCKADDR_COMMON(sa_prefix)
sa_family_t sa_prefix##family
#define__SOCKADDR_COMMON_SIZE(sizeof(unsigned short int))
我们在下一个network_server_init()函数的分析中可以看到,所有已经被创建了监听套接口描述符的对应server_socket信息都会被记录在该字段内,因此此处通过和该字段内保存的记录做比较可以知道该server_socket信息对应的监听套接口是否已经被创建。
*/
112.for(j=0;j<srv->srv_sockets.used;j++){
113.if(buffer_is_equal(srv->srv_sockets.ptr[j]->srv_token,dc->string)){
114.break;
115.}
116.}
117.if(j==srv->srv_sockets.used){
/*比较完所有项都未匹配则表示还没创建。*/
118.if(0!=network_server_init(srv,dc->string,s))return-1;
119.}
120.}
121.return 0;
122.}
函数network_server_init()代码量比较多,但是其执行主线还是很明朗,其按照网络TCP通信程序编程的一般函数调用顺序分别调用socket()、setsockopt()、bind()、listen()进行监听套接口的创建。清单10-3给出了该函数的大部分源码而省略了与本节讲解内容无关的SSL与WIN32部分。
清单10-3 函数network_server_init
//network.c
123.int network_server_init(server*srv,buffer*host_token,specific_config*s){
124.int val;
125.socklen_t addr_len;
126.server_socket*srv_socket;
127.char*sp;
128.unsigned int port=0;
129.const char*host;
130.buffer*b;
131.int is_unix_domain_socket=0;
132.int fd;
133.#ifdef SO_ACCEPTFILTER
134.struct accept_filter_arg afa;
135.#endif
136.#ifdef__WIN32
137.//……省略……
138.#endif
139.srv_socket=calloc(1,sizeof(*srv_socket));
140.srv_socket->fd=-1;
141.srv_socket->srv_token=buffer_init();
142.buffer_copy_string_buffer(srv_socket->srv_token,host_token);
143.b=buffer_init();
144.buffer_copy_string_buffer(b,host_token);
145./*ipv4:port
146.*[ipv6]:port
147.*/
/*函数strrchr()的原型为char*strrchr(const char*s,int c);,其功能是用来找出参数s字符串中最后一个出现的参数c,如果找到则将该字符出现的地址返回,否则返回NULL。使用该函数需要包含头文件string.h。因此,这里是用于分割IP地址和端口号。*/
148.if(NULL==(sp=strrchr(b->ptr,':'))){
149.log_error_write(srv,__FILE__,__LINE__,"sb","value of$SERVER["socket"]
has to be"ip:port".",b);
150.return-1;
151.}
152.host=b->ptr;
153./*check for[and]*/
154.if(b->ptr[0]=='['&&*(sp-1)==']'){
/*IPV6地址的给定字符串格式是[ipv6]:port。*/
155.*(sp-1)='