【深入理解计算机系统】TINY Web 服务器的搭建

TINY Web服务器是一个具有有限(但必需的)功能服务器,能够支持静态访问和动态访问,支持静态访问的HTML文件,无格式文本文件,编码为GIF, PNG,JPG格式的图片。麻雀虽小,五脏俱全,十分适合linux 下网络编程的初学者。当你用浏览器访问自己搭建的服务器,显示出图片或者加载出运行的程序时,是一件十分值得高兴的事情。

TINY服务器需要三个文件,tiny.c, csapp.h, csapp.c。这三个文件都可以在csapp上下载。

然后将csapp.c 生成 csapp.o文件:gcc -c csapp.c
将 .o文件打包成静态库文件 csapp.a : ar -rc csapp.a csapp.o
生成可执行文件tiny: gcc -o tiny tiny.c csapp.a

运行该服务器,指定一个端口,必须是1024–49151之间的端口,其余端口不能使用。
比如我指定1024端口: ./tiny 1024

要测试服务器动态加载程序,在csapp上下载adder.c文件,在当前目录下创建文件夹cgi-bin, 将adder.c文件放在该文件夹里并生成可执行文件。

用TELNET 程序测试
在运行了TINY 服务器的基础上,打开另一个terminal,在linux shell输入:telnet localhost 1024

然后输入 HTTP 请求行:GET /cgi-bin/adder?100&20 HTTP/1.0
GET 是HTTP 方法
“/cgi-bin/adder?100&20” 是URI, 也就是包含了文件名和参数,其中?前边的是文件名,后边的是程序参数,每个参数用&分隔开。
HTTP/1.0 是HTTP版本

然后获得 HTTP 响应,可以看到我们传进去的参数求和之后获得了最后的结果,中间还有一些头部信息。如图
这里写图片描述

用浏览器测试
在tiny程序所在的目录放入一张图片test.jpg, 打开浏览器,输入http://localhost:1024/test.jpg, 然后可以看到图片被加载了出来。
这里写图片描述
测试程序的加载: 输入http://localhost:1024/cgi-bin/adder?100&20 然后看到如下结果,至此服务器的功能测试完毕。当然还可以在文件夹里放入html文件,可以加载出相应的内容。
这里写图片描述

源码分析:

  1. main 函数
#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, 
         char *shortmsg, char *longmsg);

int main(int argc, char **argv) 
{
    int listenfd, connfd, port, clientlen;
    struct sockaddr_in clientaddr;

    /* Check command line args */
    if (argc != 2) {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
    }
    port = atoi(argv[1]);

    listenfd = Open_listenfd(port);
    while (1) {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); //line:netp:tiny:accept
    doit(connfd);                                             //line:netp:tiny:doit
    Close(connfd);                                            //line:netp:tiny:close
    }
}

在main函数里,关键是listenfd = Open_listenfd(port)这一句,服务器在这里创建和返回一个监听描述符,这个描述符准备好在端口port上接受连接请求,这个函数是包装了socket() ,bind(), listen() 这三个套接字接口函数的辅助函数。

mian函数执行典型的无限服务器循环,不断地接受连接请求(Accept()函数实现的), 其中Accept() 函数就是套接字接口函数中的accept()加入一些出错提示信息封装成的函数,这是csapp.c中常用的做法。

最后执行一个doit 函数,用来处理一个HTTP事务。请注意,TINY只支持GET 方法。

  1. doit 函数
void doit(int fd) 
{
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[MAXLINE], cgiargs[MAXLINE];
    rio_t rio;

    /* Read request line and headers */
    Rio_readinitb(&rio, fd);
    Rio_readlineb(&rio, buf, MAXLINE);                   //line:netp:doit:readrequest
    sscanf(buf, "%s %s %s", method, uri, version);       //line:netp:doit:parserequest
    if (strcasecmp(method, "GET")) {                     //line:netp:doit:beginrequesterr
       clienterror(fd, method, "501", "Not Implemented",
                "Tiny does not implement this method");
        return;
    }                                                    //line:netp:doit:endrequesterr
    read_requesthdrs(&rio);                              //line:netp:doit:readrequesthdrs

    /* Parse URI from GET request */
    is_static = parse_uri(uri, filename, cgiargs);       //line:netp:doit:staticcheck
    if (stat(filename, &sbuf) < 0) {                     //line:netp:doit:beginnotfound
    clienterror(fd, filename, "404", "Not found",
            "Tiny couldn't find this file");
    return;
    }                                                    //line:netp:doit:endnotfound

    if (is_static) { /* Serve static content */          
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //line:netp:doit:readable
        clienterror(fd, filename, "403", "Forbidden",
            "Tiny couldn't read the file");
        return;
    }
    serve_static(fd, filename, sbuf.st_size);        //line:netp:doit:servestatic
    }
    else { /* Serve dynamic content */
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
        clienterror(fd, filename, "403", "Forbidden",
            "Tiny couldn't run the CGI program");
        return;
    }
    serve_dynamic(fd, filename, cgiargs);            //line:netp:doit:servedynamic
    }
}

doit 函数 中使用了封装了系统I/O 的robust I/O, 这是csapp 特有的。 这个函数首先读取请求行的信息,得到方法 , URI, 版本号。

然后通过调用parse_uri() 函数来解析URI ,判断这个请求是动态内容还是静态内容。在该程序中,判断方法简单的处理为:要是URI里边有cgi-bin 的话,就是动态内容,否则就是静态内容。

如果是静态内容的话,就调用静态处理函数 serve_static() 函数, 否则调用 serve_dynamic()函数。

其中serve_dynamic()函数十分值得我们注意,它是通过派生一个子进程,并在子进程的上下文中运行这个CGI 程序。其中调用了在前面章节讲到的setenv()函数(用来初始化QUERY_STRING 环境变量),dup2() 函数(重定向标准输出到已连接文件描述符), execv() 函数(加载并运行CGI程序)。

到这里可以体会到这本书的精髓所在了,书中前面章节的讲解的一些函数和知识,在后边的章节就慢慢地用上了,作者十分注重在代码中解释这些复杂的问题,尽可能地减少枯燥的感觉。(不过涉及到操作系统的东西确实比较枯燥O(∩_∩)O~)

展开阅读全文

没有更多推荐了,返回首页