常用的Web服务器有:Boa,thttpd,httpd,其中httpd只支持静态页面,thttpd和Boa支持动态页面高级应用,Boa在资源利用效率上比thttpd好。Boa支持认证,支持CGI,作为该嵌入式系统的Web服务器,系统的软件开发模型选用B/S模型,嵌入式Web服务器在Web浏览器和设备之间提供了统一的GUI接口,客户机可以通过HTTP协议与嵌入式WebServer建立连接。
一.Boa主流程分析
Boa.c程序main中主要流程:
umask()函数设置限制新文件权限的掩码,umask设置的是权限是补码,用户登陆系统时,umask命令都被执行,并自动设置掩码改变默认值,新的权限将会把旧的覆盖。
devnullfd = open("/dev/null", 0);dup2(devnullfd,STDIN_FILENO;对输入重定向,写入/dev/null的东西会被系统丢掉,目的是对stdin进行保护。
parse_commandline(argc, argv);执行命令行,其中case'r':chdir(optarg);chroot(optarg);chdir("/");重新设定根目录,限制用户访问路径,提高服务器安全性。
fixup_server_root();用来检查服务器路径有没有被定义,如果没定义就停止执行并提示。
read_config_files(void);解析配置文件,保证所有的全局变量正确初始化.gethostname函数返回本地主机的标准主机名,并通过gethostbyname()返回该主机名的包含主机名字和地址信息的hostent结构指针,主机名保存到server_name。
single_post_limit、cgi_rlimit_cpu、cgi_rlimit_data、cgi_nice、max_connections、ka_timeout等变量与程序资源分配有关。
rlimit资源限制参考http://blog.csdn.net/yuyin86/article/details/8014840。
create_common_env(void);把CGI的环境变量保存到*common_env[]内,环境变量如"PATH"、"SERVER_SOFTWARE"、"SERVER_PORT"、"DOCUMENT_ROOT"、"SERVER_ROOT"、"SERVER_ADMIN"。
open_logs(void);程序主要记录access_log访问信息、error_log错误信息和cgi_log信息。
access_log记录了虚拟主机地址、访问客户端地址、访问时间、访问状态、读写数据大小、HTTP Referer、header user agent等,HTTP Referer记录了外连接情况,user agent用户代理是浏览器向访问网站提供浏览器信息的一种HTTP标志。
error_log记录效果:[08/Nov/1997:01:05:03-0600] request from 192.228.331.232 "GET /~joeblow/dir/ HTTP/1.0"("/usr/user1/joeblow/public_html/dir/"): write: Broken pipe
其中包含了错误时间内、请求客户端IP、CGI发送方式为GET、路径信息和错误因素。
create_server_socket();创建服务器的套接字,SOCK_STREAM提供有序的、可靠的、双向的和基于连接的字节流,IPPROTO_TCP指定了Internet地址族使用TCP,set_nonblock_fd()设置非阻塞方式,fcntl(server_s, F_SETFD, 1)设定close-on-exec,调用exec函数时关闭server socket,避免CGI往里面写内容。然后进行bind捆绑,并监听客户端发送的请求。
init_signals();初始化各种信号的回调函数,在多线程运行中控制程序运行。
build_needs_escape();浏览器在发送请求时,会把请求字符串进行转义操作,服务器要对收到的请求进行反转移操作。
do_fork,fork子进程,创建守护进程,作为服务程序使用,等待客户端程序与它通信。
drop_privs();通过修改服务进程ID、组ID、改变权限。
loop(int server_s),是一个select循环,Web服务器正常运行在循环中,循环检测各种信号发生,根据需要修改请求状态(阻塞和就绪),并作相应的处理,后文详细描述。
二.request请求处理
request结构:
struct request { enum REQ_STATUS status; /*请求状态,包括BODY_READ,BODY_WRITE,WRITE,PIPE_READ,PIPE_WRITE,IOSHUFFLE,DONE,TIMED_OUT,DEAD*/ enum KA_STATUS keepalive; /* keepalive status 表示连接状态,是检测死连接的一种机制 */ enum HTTP_VERSION http_version; enum HTTP_METHOD method; /* M_GET, M_POST, etc.HTTP与浏览器的交互,从url读取信息的方法 */ enum RESPONSE_CODE response_status; /* HTTP码应码. 1xx:信息,请求收到,继续处理 2xx:成功,行为被成功地接受、理解和采纳 3xx:重定向,为了完成请求,必须进一步执行的动作 4xx:客户端错误,请求包含语法错误或者请求无法实现 5xx:服务器错误,服务器不能实现一种明显无效的请求 */ enum CGI_TYPE cgi_type; enumCGI_STATUS cgi_status; char *pathname; /* 请求文件路径名 */ Range *ranges; /* 请求内容搜索范围 */ int data_fd; /* 数据文件描述符 */ unsigned long filesize; /* 文件大小*/ unsigned long filepos; /* position in file */ unsigned long bytes_written; /* 被写入长度*/ char *data_mem; /* 数据映射*/ char *header_line; /* beginning of un or incompletelyprocessed header line */ char *header_end; /* last known end of header, or endof processed data */ int parse_pos; /* how much have we parsed */ int buffer_start; /* buffer开始地址*/ int buffer_end; /* buffer结束地址 */ char *if_modified_since; /* 判断服务器端的资源是否被修改 */ time_t last_modified; /* 服务器端的资源修改时间 */ int cgi_env_index; /* CGI变量排序*/ /* Agent and referer for logfiles */ char *header_host; /*主机信息*/ char *header_user_agent; /*代理信息*/ char *header_referer; /*参考信息*/ char *header_ifrange; char *host; int post_data_fd; /* post数据临时文件描述符 */ /* env variable */ char *path_info; char *path_translated; char *script_name; char *query_string; char *content_type; char *content_length; struct mmap_entry *mmap_entry_var; int fd; /* 客户端套接字描述符 */ time_t time_last; /* 上一次时间 */ char local_ip_addr[BOA_NI_MAXHOST]; /* 本地虚拟主机IP地址*/ char remote_ip_addr[BOA_NI_MAXHOST]; /* 远程客户端IP地址 */ unsigned int remote_port; /* 远程客户端端口号 */ unsigned int kacount; /*存活连接数目*/ int client_stream_pos; char buffer[BUFFER_SIZE + 1]; /* 通用I/O缓冲数据*/ char request_uri[MAX_HEADER_LENGTH + 1]; /*uri*/ char client_stream[CLIENT_STREAM_SIZE]; /* 客户端发送的数据流 */ char *cgi_env[CGI_ENV_MAX + 4]; /* CGI 环境变量 */ #ifdef ACCEPT_ON char accept[MAX_ACCEPT_LENGTH]; /* Accept:fields */ #endif struct request *next; /* 下一个请求*/ struct request *prev; /*上一个请求 */ };
request状态分别是获取、就绪、执行、阻塞、释放。
与request相关的变量:
pending_requests 待处理请求,为1时表示有请求需要处理。
request *request_ready 就绪请求,就绪状态的请求可以马上处理。
request *request_block 阻塞请求,阻塞的请求处于等待状态,需要先移到就绪区才能进行处理。
request *request_free 空闲的请求空间。
与其相关函数有
ready_request ( ),block_request( )。
update_blocked();fd_update(),process_requests()。
get_requests( ); free_request( );
req_flush();
ready_request(request * req),把请求从阻塞链表移到就绪链表,并删除对应req->status的文件描述符集合。
fd_update()先判断是否为死链接,然后调用update_blocked完成请求就绪,并删除文件描述符集合的操作。
block_request(request* req),把请求从就绪链表移到阻塞链表,并根据req->status添加文件描述符的集合。
update_blocked();根据存活时间判断阻塞链表内请求的状态更新阻塞链表,决定是否把请求从阻塞链表移到就绪链表。
get_requests( );通过accept接收客户端连接信息,进行连接以后调用new_request()申请请求空间,开始接收客户端浏览器发送的请求。
请求信息例:
conn->fd = fd; conn->status = READ_HEADER; conn->header_line =conn->client_stream; conn->time_last = current_time; conn->kacount = ka_max; conn->remote_port = net_port(&remote_addr); conn->http_version= HTTP10; conn->method = M_GET; conn->status = DONE;
free_request(request * req); 与get_requests( )相反,负责关闭连接,释放请求空间。
process_requests()执行请求,首先判断是否有请求,如果有请求则进行连接并接收请求。根据current->status进行
read_header(current);
read_body(current);
write_body(current);
process_get(current);
read_from_pipe(current);
write_from_pipe(current);
执行DONE时调用req_flush(),req_flush函数计算需要发送数据的字节,字节数大于0则进行发送bytes_written = write(req->fd, req->buffer +req->buffer_start,bytes_to_write);
发送后返回整数值, -2表示错误,-1表示block,0表示处理完毕,>0表示还有数据,需要对其进行处理后再发送。
三.loop循环分析
首先对信号进行分析,程序调用init_signals()初始化信号回调函数,包括SIGSEGV,SIGBUS,SIGTERM,SIGHUP,SIGINT,SIGCHLD,SIGALRM,SIGPIPE,SIGUSR1,SIGUSR2。sigaction(SIGSEGV, &sa, NULL);sigaction函数是用作检查/修改与指定信号相关联的处理动作.
函数分析
void loop(int server_s) { FD_ZERO(BOA_READ);//每次循环都要清空集合,否则不能检测描述符变化 FD_ZERO(BOA_WRITE); max_fd = -1; /*捕捉信号,并调用对应函数进行处理*/ while (1) { if (sighup_flag) sighup_run(); if (sigchld_flag) sigchld_run(); if (sigalrm_flag) sigalrm_run(); if (sigterm_flag) { if (sigterm_flag == 1) { sigterm_stage1_run(); BOA_FD_CLR(req, server_s,BOA_READ); close(server_s); /* make sure the server isn'tin the block list */ server_s = -1; } if (sigterm_flag == 2 &&!request_ready && !request_block) { sigterm_stage2_run(); /*terminal */ } } else { if (total_connections >max_connections) { /*连接数太多,则清除一个文件描述符集合*/ BOA_FD_CLR(req, server_s,BOA_READ); } else { /*创建一个文件描述符集合*/ BOA_FD_SET(req, server_s,BOA_READ); /* server always set */ } } pending_requests = 0; if (max_fd) { struct timeval req_timeout; /*timeval for select */ req_timeout.tv_sec = (request_ready ? 0 :default_timeout); req_timeout.tv_usec = 0l; /* resettimeout */ /* 监视我们需要监视的文件描述符的变化情况,读写或异常Select函数使用参考http://genime.blog.163.com/blog/static/1671577532012418341877/ */ if (select(max_fd + 1, BOA_READ, BOA_WRITE, NULL, (request_ready ||request_block ? &req_timeout :NULL)) == -1) { if (errno == EINTR) continue; /* while(1) */ else if (errno != EBADF) { DIE("select"); } } if (!sigterm_flag &&FD_ISSET(server_s, BOA_READ)) { pending_requests = 1; } time(¤t_time); } max_fd = -1; if (request_block) { /* 如果request_block=1,则把请求信号移到就绪链表*/ fdset_update(); } if (pending_requests || request_ready){ /* 有待处理请求或者就绪请求则进行请求处理*/ process_requests(server_s); } } }
sighup调用sighup_run(),清除mime、passwd、alias哈希表,清空request_free链表,并重新读取配置文件。
sigchld_flag调用sigchld_run(),处理子进程。
sigalrm_flag调用sigalrm_run(),将mime、passwd哈希表信息写到日志里。
SIGTERM信号,
sigterm_flag =1时
void sigterm_stage1_run(void) { /* lame duck mode */ time(¤t_time); log_error_time(); fputs("caught SIGTERM, starting shutdown\n", stderr); sigterm_flag =2; }
sigterm_flag =2时,进行关闭操作。
void sigterm_stage2_run(void) { /* lame duckmode */ log_error_time(); fprintf(stderr, "exiting Boa normally (uptime %d seconds)\n", (int)(current_time - start_time)); chdir(tempdir); clear_common_env(); //清空环境变量 dump_mime(); //清空mime_hashtable dump_passwd(); //清空passwd_hashtable dump_alias(); //清空alias free_requests(); //释放请求空间 range_pool_empty(); //清空队列 free(server_root); //释放服务器 free(server_name); server_root =NULL; exit(EXIT_SUCCESS); //退出。 }
Loop循环先进行信号捕获,如果捕获到信号进行相应的操作,如果没有则进行请求的处理:首先检测是否有待处理请求,当一个请求到来时,将创建一个子进程为用户的连接服务。根据请求的不同,服务器返回HTML文件或者通过CGI调用外部应用程序,返回处理结果。服务器通过CGI与外部程序和脚本之间进行交互,根据客户端在进行请求时所采取的方法,服务器会收集客户所提供的信息,并将该部分信息发送给指定的CGI扩展程序。CGI扩展程序进行信息处理并将结果返回服务器,然后服务器对信息进行分析,并将结果发送回客户端。
四.CGI
CGI(Common Gateway Interface)是外部应用扩展应用程序与WWW服务器交互的一个标准接口。按照CGI标准编写的外部扩展应用程序可以处理客户端浏览器输入的数据,从而完成客户端与服务器的交互操作。而CGI规范就定义了Web服务器如何向扩展应用程序发送消息,在收到扩展应用程序的信息后又如何进行处理等内容。通过CGI可以提供许多静态的HTML网页无法实现的功能,比如搜索引擎、基于Web的数据库访问等等。
服务器程序可以通过三种途径接收信息:环境变量、命令行和标准输入.
switch (req->method) { case M_POST: w ="POST"; //环境变量 break; case M_HEAD: //标准输入 w ="HEAD"; break; case M_GET: //命令行 w ="GET"; break; default: w ="UNKNOWN"; break; }
BOA使用get方法获得静态文档。调用init_get(request * req)
先打开目标文件目录
memcpy(gzip_pathname, req->pathname, len);
data_fd = open(gzip_pathname, O_RDONLY);
读取文件信息,然后根据req结构更新文件信息,并把数据写入req->data_mem
memcpy(req->buffer + req->buffer_end, req->data_mem+ r->start, bytes_free);
修改后调用process_get().
BOA使用POST方法处理较大的数据以及动态脚本,程序调用create_common_env(void)通过env_gen_extra( )函数读取环境变量"PATH"、"SERVER_SOFTWARE"、"SERVER_PORT"、"DOCUMENT_ROOT"、"SERVER_ROOT"、"SERVER_ADMIN"的值并保存在**common_env内.
add_to_common_env( )增加环境变量项,clear_common_env()删除环境变量项。
init_cgi(request * req)函数处理HTTP头信息,绑定pipe与cgi的输入输出,为数据传输做准备,创建子进程,子进程从管道pipes[0]读取数据,父进程写数据到pipes[1]并输出。
process_cgi_header(request * req);函数对浏览器发送的http头信息进行处理,成为浏览器可以读懂的字符串。服务器经过处理把结果发送回CGI,cgi 程序输出HTML页面的方式是使用printf 把页面一行一行地打印出来,如
fprintf(cgiOut, "/n");
fprintf(cgiOut, "
fprintf(cgiOut, " hello world
/n");
信息传送到浏览器并在显示器上显示。
结语:
编程初入门,部分代码为猜测,内容可能出现较多错误,请不吝指正。