TinyHTTPd

TinyHTTPd是一个超轻量级的http服务器, 使用C语言开发, 代码只有500多行, 不用于实际生产, 只是为了学习使用. 通过阅读代码可以理解初步web服务器的本质.

主页地址 : http://tinyhttpd.sourceforge.net/

注释后的源码 :  https://github.com/tw1996/TinyHTTPd

 

HTTP协议

在阅读源码之间, 我们先要初步了解HTTP协议. 简单地说HTTP协议就是规定了客户端和服务器的通信格式, 它建立在TCP协议的基础上, 默认使用80端口. 但是并不涉及数据包的传输, 只规定了通信的规范. HTTP本身是无连接的, 也就是说建立TCP连接后就可以直接发送数据, 不必再建立HTTP连接,  对于数据包丢失重传由TCP实现, 下面简单介绍HTTP几个版本.

HTTP/0.9

TCP连接建立后, 客户端只能使用GET方式请求

GET /index.html

服务器只能回应html格式的字符串

<html>
  <body>Hello World</body>
</html>

 

 发送完毕后马上断开TCP连接.

 

HTTP/1.0

与HTTP/0.9相比, 增加了许多新的功能,  支持任何格式传输, 包括文本, 二进制数据, 文件, 音频等.  支持GET, POST, HEAD命令.

改变了数据通信的格式, 增加了头信息; 其他的新增功能还包括状态码(status code),多字符集支持,多部分发送(multi-part type),权限(authorization),缓存(cache),内容编码(content encoding)等, 所以HTTP协议一共可分为3部分 , 开始行, 首部行, 实体主体.  其中在首部行和实体主体之间以空格分开,  开始行和首部行都是以 \r\n 结尾 举个例子 : 

请求信息

GET / HTTP/1.0  //请求行
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)   //请求头
Accept: */*                               //请求头

 

响应信息

HTTP/1.0 200 OK                   //响应行
Content-Type: text/plain             //响应头
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

<html>                           //响应主体
  <body>Hello World</body>
</html>

 

HTTP/1.0规定, 头信息必须是ASCII码, 后面的数据可以是任何格式,   Content-Type 用于规定格式.  下面是一些常见的 Content-Type 字段取值.

text/plain
text/html
text/css
image/jpeg
image/png
image/svg+xml
audio/mp4
video/mp4
application/javascript
application/pdf
application/zip
application/atom+xml

 

有的浏览器为了提高通信效率, 使用了一个非标准的字段  Connection:keep-alive. 即维持一个TCP连接不断开, 多次发送HTTP数据, 直到客户端或服务器主动断开.

 

HTTP/1.1

现在最流行的HTTP协议, 默认复用TCP连接,  即不需要手动设置Connection:keep-alive,  客户端在最后一个请求时发送 Connection:close 断开连接.

增加了许多方法 : PUT, PUTCH, HEAD, OPTIONS, DELETE. 

引入管道机制, 以前是先发送一个请求, 等待回应继续发送下一个请求.  现在可以连续发送多个请求, 不用等待, 但是服务器仍然会按顺序回应. 使用 Content-Lenth字段区分数据包属于哪一个回应.

为了避免队头堵塞, 只有两种办法 : 少发送数据, 同时开多个持久连接.

 

HTTP/2

这里就不多做介绍了

 

CGI与FASTCGI

参考这篇文章 : http://www.php-internals.com/book/?p=chapt02/02-02-03-fastcgi

工作流程

 TinyHTTPd源码分析

  1. 服务器启动,  如果没有指定端口则随机选取端口建立套接字监听客户端连接
  2. accept()会一直阻塞等待客户端连接, 如果客户端连接上, 则创建一个新线程处理该客户端连接.
  3. 在accetp_request() 主要处理客户端连接,  首先解析HTTP请求报文. 只支持GET/POST请求, 否则返回HTTP501错误.  如果有请求参数的话, 记录在query_string中.  将请求的路径记录在path中, 如果请求的是目录, 则访问该目录下的index.html文件.
  4. 最后判断请求类型, 如果是静态请求, 直接读取文件发送给客户端; 如果是动态请求, 则fork()一个子进程, 在子进程中调用exec()函数簇执行cgi脚本. 然后父进程读取子进程执行结果 父子进程之间通过管道通信实现.
  5. 父进程等待子进程结束后, 关闭连接, 完成一次HTTP请求.

源码分析

首先看程序入口,  这里建立套接字, 然后与sockaddr_in结构体进行绑定, 然后用listen监听该套接字上的连接请求, 这几步都在startup()中实现.  

然后服务器在通过accept接受客户端请求, 如没有请求accept()会阻塞, 如果有请求就会创建一个新线程去处理客户端请求.

int main(void)
{
    /* 定义socket相关信息 */
    int server_sock = -1;
    u_short port = 4000;
    int client_sock = -1;
    struct sockaddr_in client_name;
    socklen_t  client_name_len = sizeof(client_name);
    pthread_t newthread;

    server_sock = startup(&port);
    printf("httpd running on port %d\n", port);

    while (1)
    {
        /* 通过accept接受客户端请求, 阻塞方式 */
        client_sock = accept(server_sock,
                (struct sockaddr *)&client_name,
                &client_name_len);
        if (client_sock == -1)
            error_die("accept");
        /* accept_request(&client_sock); */
        /* 开启线程处理客户端请求 */
        if (pthread_create(&newthread , NULL, accept_request, (void *)&client_sock) != 0)
            perror("pthread_create");
    }

    close(server_sock);

    return(0);
}

 

 

 

accept_request()主要处理客户端请求,  做出了基本的错误处理. 主要功能判断是静态请求还是动态请求, 静态请求直接读取文件发送给客户端即可, 动态请求则调用execute_cgi()处理. 

/**********************************************************************/
/* A request has caused a call to accept() on the server port to
 * return.  Process the request appropriately.
 * Parameters: the socket connected to the client 
 * 处理每个客户端连接
 * */
/**********************************************************************/
void *accept_request(void *arg)
{
    int client = *(int*)arg;
    char buf[1024];
    size_t numchars;
    char method[255];
    char url[255];
    char path[512];
    size_t i, j;
    struct stat st;
    int cgi = 0;      /* becomes true if server decides this is a CGI
                       * program */
    char *query_string = NULL;
    
    /* 获取请求行, 返回字节数  eg: GET /index.html HTTP/1.1 */
    numchars = get_line(client, buf, sizeof(buf));
    /* debug */
    //printf("%s", buf);

    /* 获取请求方式, 保存在method中  GET或POST */
    i = 0; j = 0;
    while (!ISspace(buf[i]) && (i < sizeof(method) - 1))
    {
        method[i] = buf[i];
        i++;
    }
    j=i;
    method[i] = '\0';

    /* 只支持GET 和 POST 方法 */
    if (strcasecmp(method, "GET") && strcasecmp(method, "POST"))
    {
        unimplemented(client);
        return NULL;
    }

    /* 如果支持POST方法, 开启cgi */
    if (strcasecmp(method, "POST") == 0)
        cgi = 1;

    i = 0;
    while (ISspace(buf[j]) && (j < numchars))
        j++;
    while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars))
    {
        url[i] = buf[j];
        i++; j++;
    }
    /* 保存请求的url, url上的参数也会保存 */
    url[i] = '\0';

    //printf("%s\n", url);

    if (strcasecmp(method, "GET") == 0)
    {
        /* query_string 保存请求参数 index.php?r=param  问号后面的 r=param */
        query_string = url;
        while ((*query_string != '?') && (*query_string != '\0'))
            query_string++;
        /* 如果有?表明是动态请求, 开启cgi */
        if (*query_string == '?')
        {
            cgi = 1;
            *query_string = '\0';
            query_string++;
        }
    }

//    printf("%s\n", query_string);

    /* 根目录在 htdocs 下, 默认访问当前请求下的index.html*/
    sprintf(path, "htdocs%s", url);
    if (path[strlen(path) - 1] == '/')
        strcat(path, "index.html");

    //printf("%s\n", path);
    /* 找到文件, 保存在结构体st中*/
    if (stat(path, &st) == -1) {
        /* 文件未找到, 丢弃所有http请求头信息 */
        while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
            numchars = get_line(client, buf, sizeof(buf));
        /* 404 no found */
        not_found(client);
    }
    else
    {

        //如果请求参数为目录, 自动打开index.html
        if ((st.st_mode & S_IFMT) == S_IFDIR)
            strcat(path, "/index.html");        
        //文件可执行
        if ((st.st_mode & S_IXUSR) ||
                (st.st_mode & S_IXGRP) ||
                (st.st_mode & S_IXOTH)    )
            cgi = 1;
        if (!cgi)
            /* 请求静态页面 */
            serve_file(client, path);
        else
            /* 执行cgi 程序*/
            execute_cgi(client, path, method, query_string);
    }

    close(client);
    return NULL;
}
View Code

相关文章: