常用的Web服务器有:Boa,thttpd,httpd,其中httpd只支持静态页面,thttpd和Boa支持动态页面高级应用,Boa在资源利用效率上比thttpd好。Boa支持认证,支持CGI,作为该嵌入式系统的Web服务器,系统的软件开发模型选用B/S模型,嵌入式Web服务器在Web浏览器和设备之间提供了统一的GUI接口,客户机可以通过HTTP协议与嵌入式WebServer建立连接。

一.Boa主流程分析

       Boa.c程序main中主要流程:

wKioL1Om_43xclp7AAKHj1ZzJX4595.jpg

       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_limitcgi_rlimit_cpucgi_rlimit_datacgi_nicemax_connectionska_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 Refererheader 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

其中包含了错误时间内、请求客户端IPCGI发送方式为GET、路径信息和错误因素。

 

       create_server_socket();创建服务器的套接字,SOCK_STREAM提供有序的、可靠的、双向的和基于连接的字节流,IPPROTO_TCP指定了Internet地址族使用TCPset_nonblock_fd()设置非阻塞方式,fcntl(server_s, F_SETFD, 1)设定close-on-exec,调用exec函数时关闭server socket,避免CGI往里面写内容。然后进行bind捆绑,并监听客户端发送的请求。

      

       init_signals();初始化各种信号的回调函数,在多线程运行中控制程序运行。

 

       build_needs_escape();浏览器在发送请求时,会把请求字符串进行转义操作,服务器要对收到的请求进行反转移操作。

 

       do_forkfork子进程,创建守护进程,作为服务程序使用,等待客户端程序与它通信。

      

       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表示block0表示处理完毕,>0表示还有数据,需要对其进行处理后再发送。

 

三.loop循环分析

首先对信号进行分析,程序调用init_signals()初始化信号回调函数,包括SIGSEGVSIGBUSSIGTERMSIGHUPSIGINTSIGCHLDSIGALRMSIGPIPESIGUSR1SIGUSR2sigaction(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(&current_time);
        }
        max_fd = -1;
        if (request_block) {
            /* 如果request_block=1,则把请求信号移到就绪链表*/
            fdset_update();
        }
        if (pending_requests || request_ready){
           /* 有待处理请求或者就绪请求则进行请求处理*/
            process_requests(server_s);
        }
    }
}

sighup调用sighup_run(),清除mimepasswdalias哈希表,清空request_free链表,并重新读取配置文件。

sigchld_flag调用sigchld_run(),处理子进程。

sigalrm_flag调用sigalrm_run(),将mimepasswd哈希表信息写到日志里。

 

SIGTERM信号,

sigterm_flag =1

void sigterm_stage1_run(void)
{                               /* lame duck mode */
   time(&current_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

       CGICommon 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头信息,绑定pipecgi的输入输出,为数据传输做准备,创建子进程,子进程从管道pipes[0]读取数据,父进程写数据到pipes[1]并输出。

process_cgi_header(request * req);函数对浏览器发送的http头信息进行处理,成为浏览器可以读懂的字符串。服务器经过处理把结果发送回CGIcgi 程序输出HTML页面的方式是使用printf 把页面一行一行地打印出来,如

fprintf(cgiOut, "<HTML><HEAD>/n"); 

fprintf(cgiOut, "<TITLE>helloworld</TITLE></HEAD>/n"): 

fprintf(cgiOut, "<BODY><H1> hello world</H1>/n"); 

信息传送到浏览器并在显示器上显示。

 

结语:

       编程初入门,部分代码为猜测,内容可能出现较多错误,请不吝指正。