小型web服务器thttpd的学习总结

1、软件的主要架构

软件的文件布局比较清晰,主要分为6个模块,主模块是thttpd.c文件,这个文件中包含了web server的主要逻辑,并调用了其他模块的函数。其他的5个模块都是单一的功能模块,之间没有任何耦合。

  • 其中包括多路IO复用的抽象模块fdwatch.h/c,这个模块中将常用的IO复用接口,如poll/select抽象为一类接口,从而保证了接口的单一性和软件的可移植性。
  • libhttpd模块包含的是libhttpd.h/c文件,主要的功能是完成地提供http请求的解析和处理服务,对外提供相应的接口。
  • match模块则是对外提供了一个match.c用来做为关键词的匹配作用,用于cgi符号匹配。
  • mmc模块包块的也是mmc.h/c文件,用来进行文件存储的缓存管理。
  • 另外一个就是timer.h/c,自己实现的一个定时器模块,主要用来做请求接收,发送和清理内存的操作定时。

2、各个模块代码分析

2.1 fdwatch模块

该模块对外提供了6个函数,就是对一般的select/poll类函数的使用方法进行了相应的抽象,包括

//获得系统可使用的fd的最大数目,并初始化数据结构
extern int fdwatch_get_nfiles(void)

//清除fdwatch中的数据结构
extern void fdwatch_clear( void )

//对fd set中的fds进行操作,其中rw表示是否可读可写
extern int fdwatch_add_fd(int fd, int rw)
extern int fdwatch_del_fd(int fd, int rw)

//fd多路复用的主循环函数 参数是超时时间
extern int fdwatch(long timeout_msecs)

//对fd状态的检查
extern void fdwatch_check_fd(int fd, int rw)

//提取出fdwatch当前的状态
extern void fdwatch(long* nselectP, long* npollP)

如果想自己简单实现的话,可以按照如下进行实现:

//首先设置模块全局变量
static int nfiles;   //可以watch的最大fd数目
static int maxfd;    //当前watch的最大fd值
static int conn_nums;  //当前连接的数目
static long nselect;    //当前select的次数

由于如果使用select的话,需要首先有一个fd_set来标记需要关注哪些fd可读,关注哪些fd可写。而将标记fd_set传入之后,该fd_set返回的指将是当前可读或者可写的fd列表,会改变标记set的值,因此,这里设置了两个fd_set,一个用于标记需要关注的fd,另一个用于传入select函数,获得当前可处理的fd情况。

//标记 set
static fd_set master_rfdset;   
static fd_set master_wfdset;
//工作 set
static fd_set working_rfdset;
static fd_set working_wfdset;

//内部函数的声明
static int fdwatch_select(long timeout_msecs);

该函数用于获得当前可以复用的fd的最大个数,这个最大个数受制于几个因素,一个是进程可以打开的最大的文件描述符数,getdtablesize()返回的值,还有资源限制的最大fd数,另外还不能超过fd_setsize值,一般现在的fd_set类型都是long int的数组,每一位代表一个fd的读写情况,取值一般为1024。

int fdwatch_get_nfiles( void )
{
#ifdef RLIMIT_NOFILE
    struct rlimit rl;
#endif
    //进程所能打开的最大文件描述符数
    nfiles = getdtablesize();

    //设置资源限制的最大fd值  
#ifdef RLIMIT_NOFILE
    if(getrlimit(RLIMIT_NOFILE, &rl) == 0)
    {
        nfiles = rl.rlim_cur;
        if( rl.rlim_max == RLIM_INFINITY )
            rl.rlim_cur = 8192;
        else
            rl.rlim_cur = rl.rlim_max;
        if( setrlimit( RLIMIT_NOFILE, &rl) == 0 )
            nfiles = rl.rlim_cur;
    }
#endif
//如果是SELECT不能超过FD_SETSIZE的值
    nfiles = MIN(nfiles, FD_SETSIZE);

    nselect = 0;

    return nfiles; 
}

清除标志位,直接调用FD_ZERO函数:

void fdwatch_clear( void )
{
    maxfd = -1;
    conn_nums = 0;
    FD_ZERO( &master_wfdset );
    FD_ZERO( &master_rfdset );
}

增加标志位,则是根据rw的情况调用FD_SET函数:

void fdwatch_add_fd( int fd, int rw )
{
    conn_nums++;
    if(fd > maxfd)
        maxfd = fd;
    switch( rw )
    {
        case FD_READ:
            FD_SET(fd, &master_rfdset);
        case FD_WRITE:
            FD_SET(fd, &master_wfdset);
        default:
            return;
    }

}

检查标志位,同样根据rw的情况调用FD_ISSET函数:

int fdwatch_check_fd( int fd, int rw)
{
    switch( rw )
        {
            case FD_READ:
                return FD_ISSET(fd, &working_rfdset);
            case FD_WRITE:
                return FD_ISSET(fd, &working_wfdset);
            default:
                return 0;
        }
}

在大循环中,将master_fdset的值赋值给working_fdset然后调用select传入working_fdset进行检测,检测的时候由参数timeout_msecs决定。

int fdwatch( long timeout_msecs )
{
    return fdwatch_select( timeout_msecs );
}

static int fdwatch_select( long timeout_msecs )
{
    struct  timeval timeout;

    ++nselect;
    working_rfdset = master_rfdset;
    working_wfdset = master_wfdset;
    if(timeout_msecs == INFTIM)
    {
        if((maxfd + 1) <= nfiles)
            return select(maxfd +1, &working_rfdset, &working_wfdset, NULL, (struct timeval*)0);
        else
        {
            perror("maxfd out of range");
            return -1;
        }
    }
    else
    {
        timeout.tv_sec = timeout_msecs / 1000L;
        timeout.tv_usec = timeout_msecs % 1000L * 1000L;
        if((maxfd + 1) <= nfiles)
            return select(maxfd + 1, &working_rfdset, &working_wfdset, NULL, &timeout);
        else
        {
            perror("maxfd out of range");
            return -1;
        }
    }   
}

下面两个函数主要是永远检测当前select模块的情况,方便后面打log。

void fdwatch_status( long* nselectP )
{
    *nselectP = nselect;
    nselect = 0;
}

int fdwatch_get_conn_nums(void)
{
    return conn_nums;
}

可以看到fdwatch模块仅仅是对select做了一个简单的封装,从而可以更加灵活的使用接口进行fd复用的操作,从而可以正常的处理小规模的服务器并发。

2.1 定时器模块

定时器模块主要是提供一个定时服务,而采用的时间则是从gettimeofday库函数来获得。建立起定时器模块,主要需要首先确定下模块中的几个结构体,如定时器触发后,响应的函数和该函数的参数选择:

//响应函数定义 
typedef void timeout_func(timeout_args args, struct timeval* now);
//传入参数
typedef union
{
    void* p;
    int i;
    long int l;
}timeout_args;

这里将参数定义为一个联合体,从而可以传入多样类型的值,同时节省空间。所以,定时器结构的定义为:

typedef struct timer_struct
{
    timeout_func* timer_proc;    //响应函数
    timeout_args args;            //响应函数参数
    struct timeval time;         //定时器触发时间
    long msecs;                   //定时多长时间
    int periodic;                 //周期性标志

    struct timer_struct* next;   //做成链表
} Timer;

然后可以看下定时器模块需要提供的模块接口,大抵也就是如下几种,创建一个定时器,运行一个定时器,重置一个定时器,取消一个定时器,这里还提供了一个查看最近定时器的触发时间的接口,用来在这段时间内通过select进行查看各个连接的情况,也就是说这个时间作为上述fdwatch函数的参数传入。此外在本定时器模块中,实际上是建立了两个链表,一个是当前定时器的列表,一个是被取消的定时器的列表。因此,还提供了tmr_clean函数用于合理释放无用定时器所占用的内存。而tmr_destroy函数则是销毁所有的定时器结构。

//创建一个定时器
extern Timer* tmr_create(timeout_func* timer_proc,  timeout_args args, struct timeval* now, long msecs, int periodic);

//运行一个定时器
extern void tmr_run(struct timeval* now);

//查看最近的定时器触发时间-毫秒
extern long tmr_timeout_ms(struct timeval* now);

//查看最近的定时器触发时间-struct timeval
extern struct timeval* tmr_timeout(struct timeval* now);

//重置一个定时器
extern void tmr_reset(Timer* timer, struct timeval* now);

//取消一个定时器
extern void tmr_cancle(Timer* timer);

//清除定时器结构
extern void tmr_clean(void);

//销毁所以定时器内存
extern void tmr_destroy(void);

具体的函数实现,这里就简单的阐述一下过程,不展开代码叙述了。创建定时器时,如果无用定时器列表中有内容,就直接使用其数据,否则malloc一个,然后初始化后,插入列表中。运行一个定时器,则是根据当前时刻的时间,在列表中依次比对,对于超时的定时器运行其回调函数,接着根据周期性选择回收这个定时器还是重新设置这个定时器。最近触发时间也是在链表中找出最近的时间返回。重置定时器就是根据传入的时间,重新确定定时器的触发时间。取消定时器就是将该定时器从当前定时器列表转移到无用定时器列表中。清除定时器和销毁定时器上面已经介绍过了,就是销毁某些内存。

一次性写很多真的看着都烦呀,那另外两个主要的模块就在下篇来介绍吧。

另注:本文中的代码是自己手写,和原代码并非都是一致的。


1、主函数模块分析

对于主函数而言,概括来说主要做了三点内容,也就是初始化系统,进行系统大循环,退出系统。下面主要简单阐述下在这三个部分,又做了哪些工作呢。

初始化系统

  1. 拿出程序的名字(argv[0])用来作为参数打开那个log(syslog)

  2. 解析命令行的参数(parse_args),初始化内部的参数变量

  3. 检查当前主机名(addr) 没有的话利用gethostbyname从hostname中获取

  4. 检查当前要使用的主机端口(port)

  5. 读取Throttle file(门限文件,这里省略)

  6. 检查logfile的值,有的话就创建一个logfp咯

  7. 获得系统用户的相关信息(getpwnam),使用的系统用户为nobody(安全),记录下uid,gid值

  8. 切换程序的工作空间为参数中的dir值

  9. 获得当前工作目录(保证以'/'结尾)

  10. 调用daemon函数进入后台工作

  11. 查看pidfile,如果pidfile不为空则打开该文件,写入pid值

  12. 根据参数选择是否chroot,(chroot的原因见这个链接

  13. 设置信号处理函数signal(处理SIGTERM SIGINT SIGPIPE SIGHUP SIGUSR1)

  14. 初始化http处理模块(调用该模块init函数)

  15. 设置一个occasional timer用于时不时的清除定时器模块和mmc模块的无用内存,如果有需要的话,设置一个status timer,用于记录状态

  16. 为了安全,放弃root权限,变成nobody(使用setgroups和setgid,setuid函数族)

  17. 利用fdwatch包装的api,获得最多可以复用的fd数

  18. 创建一个连接池(数组)每个连接的数据结构如下,并完成初始化操作。

    typedef struct {
    int conn_state; //连接状态
    httpd_conn* hc; //用户信息
    int tnums[MAXTHROTTLENUMS]; /* throttle indexes /
    int numtnums; //
    long limit; //
    time_t started_at; //起始时间
    Timer
     idle_read_timer; //空闲读取定时器
    Timer* idle_send_timer; //空闲发送定时器
    Timer* wakeup_timer; //苏醒定时器
    Timer* linger_timer; //
    long wouldblock_delay; //
    off_t bytes; //****
    off_t bytes_sent; //发送的数据
    off_t bytes_to_send; //还需要发送的数据
    } connecttab;

系统大循环

  1. 获得当前的时间,开始大循环

  2. 循环的条件时 terminate != 0 || numconnects > 0

  3. 循环的内容是:

    • 如果fdwatch_recompute标志是1,清除原来的fdwatch内部变量,重新设置哪些文件需要被监控读和写。如果某连接状态是reading或者lingering则观测该连接的读状态;而如果某个连接的状态是sending,则观测该连接的写状态,(记得还要观测服务器的监听fd读状态)。
    • 然后就开始fdwatch所有描述符了,时间参数是下一个定时器触发的时间,从而在定时器触发前一直监听。
    • 接着开始处理观测结果,如果没有fd准备好,那就开始运行定时器。
    • 否则,根据是新的连接还是当前连接池中的连接以及其事件进行相应的处理。

新来的连接处理

  • 首先,保证连接数不大于最大的连接数

  • 接着,找到连接池中的最靠前的free连接,新建一个用户数据结构,表明为未初始化;

  • 调用http模块的httpd_get_conn函数,初始化该用户信息,然后填充些连接信息到数据结构中,开启read定时器;

  • 设置该连接为非阻塞的连接;

fd可读时的处理

  • 首先,看是否有更多的空间来存取用户的请求数据,如果没有的话,给read_buf增加空间,每次1000字节,5000封顶;

  • 然后,从连接中读取数据;

  • 判断当前读入的数据是否能构成一个合理的http request;

  • 如果可以的话,进行http解析请求;

  • 设置需要给用户放回的数据;

  • 设置连接的状态为SENDING,停止该连接的读定时,开启该用户的写定时;

fd可写时的处理

  • 查看response中是否有值,如果没有,则直接开始写文件,该文件已经被映射到内存,直接从hc中的file_address中读即可。而如果有值的话,则写response中和file_address中的数据;
  • 如果没有写成功的话,设置连接状态为pause,并设置wakeup定时器,过会儿重新发送;
  • 重新设置定时器,根据发送数据的情况将responlen清零,并设置bytes_sent中的值,按情况清除连接还是直接返回。

fd需要linger时的处理

如果有数据直接读取数据扔掉

退出系统

  1. 清除已分配的内存
  2. 关闭系统日志
  3. 退出

2、httpd模块分析

在httpd模块中,定义了两个核心数据结构,服务器数据(http_server)和用户连接数据(httpd_conn)。

服务器的数据结构的定义分别如下:

/* A server. */
typedef struct {
    char* hostname;                  //主机名(ex:localhost)
    struct in_addr host_addr;         //主机地址
    int port;                        //端口号
    char* cgi_pattern;                //cgi样式
    char* cwd;                        //当前工作路径
    int listen_fd;                    //监听套接字
    FILE* logfp;                     //log文件描述符
    int no_symlinks;                 //有无符号连接标志
    int vhost;                       //虚拟主机标志
} httpd_server;

下面是连接的数据结构:

/* A connection. */
typedef struct {
        int initialized;                 //初始化标志
        httpd_server* hs;         //服务器结构地址
          struct in_addr client_addr;       //客户端地址      
        char* read_buf;                  //读缓存
        int read_size, read_idx, checked_idx; //缓存标志位
        int checked_state;               //检测状态标志
        int method;                      //请求方法标志
        int status;                      //当前连接状态
        off_t bytes;
        char* encodedurl;                 //encode后的url
        char* decodedurl;                //decode后的url
        char* protocol;                  //http协议类型
        char* origfilename;               //原来的文件名
        char* expnfilename;              //扩展后的文件名
        char* encodings;                  
        char* pathinfo;
        char* query;
        char* referer;
        char* useragent;
        char* accept;
        char* accepte;
        char* cookie;
        char* contenttype;
        char* reqhost;
        char* hdrhost;
        char* authorization;
        char* remoteuser;
        char* response;                 //发送缓存
        int maxdecodedurl, maxorigfilename, maxexpnfilename, maxencodings,maxpathinfo, maxquery, maxaccept, maxaccepte, maxreqhost,maxremoteuser, maxresponse;
#ifdef TILDE_MAP_2
        char* altdir;
        int maxaltdir;
#endif                      
        int responselen;
        time_t if_modified_since, range_if;
        off_t contentlength;
        char* type;     /* not malloc()ed */
        char* hostname; /* not malloc()ed */
        int mime_flag;
        int one_one;    /* HTTP/1.1 or better */
        int got_range;
        int tildemapped;    /* this connection got tilde-mapped */
        off_t init_byte_loc, end_byte_loc;
        int keep_alive;
        int should_linger;
        struct stat sb;
        int conn_fd;
        char* file_address;
} httpd_conn;

该模块提供的函数接口有:

//初始化http server数据结构
extern httpd_server* httpd_initialize(
char* hostname, u_int addr, int port, char* cgi_pattern, char* cwd,
FILE* logfp, int no_symlinks, int vhost );

//改变http server结构中的logfp
extern void httpd_set_logfp( httpd_server* hs, FILE* logfp );

//清除http server结构
extern void httpd_terminate( httpd_server* hs );

//当有一个新连接来临时,接收这个连接,并将该连接http client初始化
extern int httpd_get_conn( httpd_server* hs, httpd_conn* hc );

//根据连接hc的read_buf中的内容,判断当前接收的数据是否是一个完成的http请求,并返回对应结果
extern int httpd_got_request( httpd_conn* hc );

//解析上述的http请求,并把解析后的值放入hc对应的数据单元中
extern int httpd_parse_request( httpd_conn* hc );

//准备需要向客户端发送的数据
extern int httpd_start_request( httpd_conn* hc );

//把hc中response中的内容写给用户
extern void httpd_write_response( httpd_conn* hc );

//关闭一个连接并释放连接的空间
extern void httpd_close_conn( httpd_conn* hc, struct timeval* nowP );

//释放hc中所有的空间
extern void httpd_destroy_conn( httpd_conn* hc );


//向客户端发送一个错误信息
extern void httpd_send_err(
httpd_conn* hc, int status, char* title, char* form, char* arg );

//根据method号找到method内容
extern char* httpd_method_str( int method );

//重新分配一段string
extern void httpd_realloc_str( char** strP, int* maxsizeP, int size );

其中,系统操作这个httpd模块则可以分为如下几部进行理解。

  • 使用对应的接口进行httpd模块的初始化,对应http server的初始化采用httpd_initialize接口,初始化好了之后就在对应的端口上进行监听套接字;

  • 当监听的套接字可读之后,就可以使用httpd_get_conn函数,accept该用户,并开辟一个用户的httpd_conn结构,并该结构利用已有的信息进行初始化;

  • 接着当该用户的套接字可读时,又将会去调用httpd_got_request接口,该接口将会去将套接字上的数据读到hc结构中的read_buf中去,然后对于read_buf中的数据进行检测,查看收到的数据是否能构成一个完整的http请求;

  • 如果接收到的确实是一个完整的http请求,就会去调httpd_parse_request接口,对read_buf中的数据进行解析,并将http头中解析到的字段(如method,url等)放入hc结构体中。

  • 当数据都解析完成后,系统将会调用httpd_start_request接口来准备需要回复给用户的数据,这个数据的准备是根据解析到的具体情况来进行处理的,有可能就是一个index.html文件,而有可能就是在hc的response中放了一些错误信息。

  • 而准备好要发送的数据之后,就可以设置连接的状态为SENDING,这样下次select后就会对于该连接调用handle_send函数,将数据发送出去,并关闭连接。

再具体的说的话,可以看到thttpd预先开辟了大约1024个conn_tab结构,这里的conn_tab指的是连接的具体信息,其数据结构核心数据如下:

typedef struct 
{
    int conn_state;
    httpd_conn* hc; //has to define
    long limit;
    time_t started_at;
    int numtnums;
    Timer* idle_read_timer;
    Timer* idle_send_timer;
    Timer* wakeup_timer;
    Timer* linger_timer;

    off_t bytes;
    off_t bytes_sent;
    off_t bytes_to_send;
} conn_tab;

其中包含了连接的状态conn_state,内嵌了具体的用户信息hc,发送限制limit,开始时间started_at,四个定时器(读定时,写定时,连接苏醒定时,连接保持定时),和连接当前要发送的字节数bytes_to_send,已经发送的字节bytes_sent,这里的bytes好像没有啥重要意义;

对于接收到的一个用户而言,则按照上述的httpd_conn结构的定义,则初始化的时候需要考虑如下内容,init初始化标志,hs服务器结构地址,自己的client地址,然后就是读取信息需要的read_buf和用于其标记的idx和checked状态位,然后就是解析所需要的method, encodedurl, decodeurl, protocol, origfilename, expnfilename, encodings, pathinfo, query, referer, useragent, accept, accepete, cookie, contenttype, reqhost, hdrhost, quthorization, remoteuser, 及其最大字符长度,最后就是返回需要的response_buf,file_address, contentlength,还有些标志位信息如mimeflag,http1.1标志one_one,keep_alive标志,should_linger标志以及got_range标志。

这里以一个普通的GET请求为例,讲一下http_conn中各字段的值分别是什么。

HTTP REQUEST: GET /index.html?a=1 HTTP/1.1

解析完成后

method              1(GET)
protocol            HTTP/1.1
reqhost              ""
encodedurl          /index.html?a=1(可能有16进制数)
decodefurl          /index.html?a=1(没有16进制)
origfilename        index.html (url是"/"时设为“.”)
expnfilename        index.html (没有符号链接,且证明了文件存在性)
pathinfo            ""
query               a=1
accept              text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

accepte             gzip, deflate, sdch
remoteuser          ""
response            存放着http头部信息p
referer             ""
useragent           Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36
cookie              ""
contenttype         ""
hdrhost             127.0.0.1
authorization       ""
id_modified_since   -1
range_if            -1
contentlength       -1
got_range            0
init_byte_loc        0
end_byte_loc         -1
keep_alive           1
should_linger        1
hostname             NULL
mime_flag            1
bytes                111(html文件的大小)
file_address         文件内存地址

其中最后反馈的数据就是由response和file_address这里那个部分组成的。




猜你喜欢

转载自blog.csdn.net/qq_31186123/article/details/79376303
今日推荐