zoukankan      html  css  js  c++  java
  • CSAPP Tiny web server源代码分析及搭建执行

    1. Web基础

    webclient和server之间的交互使用的是一个基于文本的应用级协议HTTP(超文本传输协议)。

    一个webclient(即浏览器)打开一个到server的因特网连接,而且请求某些内容。server响应所请求的内容,然后关闭连接。浏览器读取这些内容。并把它显示在屏幕上。

    对于webclient和server而言。内容是与一个MIME类型相关的字节序列。

    常见的MIME类型:

    MIME类型 描写叙述
    text/html HTML页面
    text/plain 无格式文本
    image/gif GIF格式编码的二进制图像
    image/jpeg JPEG格式编码的二进制图像

    webserver以两种不同的方式向客服端提供内容:
    1. 静态内容:取一个磁盘文件。并将它的内容返回给client
    2. 动态内容:执行一个可执行文件,并将它的输出返回给client

    统一资源定位符:URL

    http://www.google.com:80/index.html

    表示因特网主机 www.google.com 上一个称为 index.html 的HTML文件。它是由一个监听port80的Webserver所管理的。

    HTTP默认port号为80

    可执行文件的URL能够在文件名称后包含程序參数, “?”字符分隔文件名称和參数,而且每一个參数都用“&”字符分隔开。如:

    http://www.ics.cs.cmu.edu:8000/cgi-bin/adder?

    123&456

    表示一个 /cgi-bin/adder 的可执行文件,带两个參数字符串为 123 和 456

    确定一个URL指向的是静态内容还是动态内容没有标准的规则,常见的方法就是把全部的可执行文件都放在 cgi-bin 文件夹中

    2. HTTP

    HTTP标准要求每一个文本行都由一对回车和换行符来结束

    (1)HTTP请求

    一个HTTP请求:一个请求行(request line) 后面尾随0个或多个请求报头(request header), 再尾随一个空的文本行来终止报头

    请求行: <method> <uri> <version>
    HTTP支持很多方法。包含 GET,POST,PUT,DELETE,OPTIONS,HEAD,TRACE。


    URI是对应URL的后缀,包含文件名称和可选參数
    version 字段表示该请求所遵循的HTTP版本号

    请求报头:<header name> : <header data> 为server提供了额外的信息。比如浏览器的版本号类型
    HTTP 1.1中 一个IP地址的server能够是 多宿主主机。比如 www.host1.com www.host2.com 能够存在于同一server上。


    HTTP 1.1 中必须有 host 请求报头,如 host:www.google.com:80 假设没有这个host请求报头,每一个主机名都仅仅有唯一IP,IP地址非常快将用尽。

    (2)HTTP响应

    一个HTTP响应:一个响应行(response line) 后面尾随0个或多个响应报头(response header)。再尾随一个空的文本行来终止报头,最后尾随一个响应主体(response body)

    响应行:<version> <status code> <status message>
    status code 是一个三位的正整数

    状态代码 状态消息 描写叙述
    200 成功 处理请求无误
    301 永久移动 内容移动到位置头中指明的主机上
    400 错误请求 server不能理解请求
    403 禁止 server无权訪问所请求的文件
    404 未发现 server不能找到所请求的文件
    501 未实现 server不支持请求的方法
    505 HTTP版本号不支持 server不支持请求的版本号

    两个最重要的响应报头:
    Content-Type 告诉client响应主体中内容的MIME类型
    Content-Length 指示响应主体的字节大小
    响应主体中包含着被请求的内容。

    3.服务动态内容

    (1) client怎样将程序參数传递给server

    GET请求的參数在URI中传递, “?”字符分隔了文件名称和參数,每一个參数都用一个”&”分隔开,參数中不同意有空格,必须用字符串“%20”来表示
    HTTP POST请求的參数是在请求主体中而不是 URI中传递的

    (2)server怎样将參数传递给子进程

    GET /cgi-bin/adder?123&456 HTTP/1.1

    它调用 fork 来创建一个子进程。并调用 execve 在子进程的上下文中执行 /cgi-bin/adder 程序

    在调用 execve 之前,子进程将CGI环境变量 QUERY_STRING 设置为”123&456”, adder 程序在执行时能够用unix getenv 函数来引用它

    (3)server怎样将其它信息传递给子进程

    环境变量 描写叙述
    QUERY_STRING 程序參数
    SERVER_PORT 父进程侦听的port
    REQUEST_METHOD GET 或 POST
    REMOTE_HOST client的域名
    REMOTE_ADDR client的点分十进制IP地址
    CONTENT_TYPE 仅仅对POST而言。请求体的MIME类型
    CONTENT_LENGTH 仅仅对POST而言,请求体的字节大小

    (4) 子进程将它的输出发送到哪里

    一个CGI程序将它的动态内容发送到标准输出。在子进程载入并执行CGI程序之前,它使用UNIX dup2 函数将它标准输出重定向到和client相关连的已连接描写叙述符
    因此,不论什么CGI程序写到标准输出的东西都会直接到达client

    4. 综合: Tiny web server源代码及分析

    (1) main程序

    Tiny是一个迭代server,监听在命令行中传递来的port上的连接请求,在通过调用 open_listenfd 函数打开一个监听套接字以后。执行无限server循环,不断接受连接请求(第16行)。执行事务(第17行),并关闭连接它的那一端(第18行)

    int main(int argc, char **argv)
    {
            int listenfd, connfd, port, clientlen;
            struct sockaddr_in clientaddr;
    
            if (argc != 2) {
                    fprintf(stderr, "usage: %s <port>
    ", argv[0]);
                    exit(1);
            }
            port = atoi(argv[1]);
    
            listenfd = Open_listenfd(port);
            while (1) {
                    clientlen = sizeof(clientaddr);
                    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
                    doit(connfd);
                    Close(connfd);
            }
    }
    

    (2) doit函数

    doit函数处理一个HTTP事物,首先读和解析请求行(request line)(第11-12行),注意,我们使用rio_readlineb函数读取请求行。
    Tiny仅仅支持GET方法,假设client请求其它方法,发送一个错误信息。


    然后将URI解析为一个文件名称和一个可能为空的CGI參数字符串。而且设置一个标志表明请求的是静态内容还是动态内容(第21行)
    假设请求的是静态内容。就验证是否为普通文件,有读权限(第29行)
    假设请求的是动态内容,就验证是否为可执行文件(第37行),假设是,就提供动态内容(第42行)

    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
        }
    }

    (4)read_requesthdrs 函数

    Tiny不使用请求报头中的不论什么信息。仅仅调用 read_requesthdrs函数来读取并忽略这些报头。
    注意。终止请求报头的空文本行是由 回车和换行符组成的。在第6行中检查

    void read_requesthdrs(rio_t *rp) 
    {
        char buf[MAXLINE];
    
        Rio_readlineb(rp, buf, MAXLINE);
        while(strcmp(buf, "
    ")) {          //line:netp:readhdrs:checkterm
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
        }
        return;
    }

    (5)parse_uri 函数

    Tiny假设静态内容的主文件夹就是当前文件夹,可执行文件的主文件夹是 ./cgi-bin/ 不论什么包含字符串 cgi-bin 的URI都觉得是对动态内容的请求。
    首先将URI解析为一个文件名称和一个可选的CGI參数字符串。
    假设请求的是静态内容(第5行)。就清除CGI參数串(第6行)。然后将URI转换为一个相对的unix 路径名,比如 ./index.html
    假设URI是用’/’ 结尾的(第9行) ,我们就把默认的文件名称加在后面(第10行)
    假设请求的是动态内容(第13行),就会抽取全部的CGI參数(第14-20行),并将URI剩下的部分转换为一个对应的unix文件名称(第21-22行)

    int parse_uri(char *uri, char *filename, char *cgiargs) 
    {
        char *ptr;
    
        if (!strstr(uri, "cgi-bin")) {  /* Static content */ //line:netp:parseuri:isstatic
        strcpy(cgiargs, "");                             //line:netp:parseuri:clearcgi
        strcpy(filename, ".");                           //line:netp:parseuri:beginconvert1
        strcat(filename, uri);                           //line:netp:parseuri:endconvert1
        if (uri[strlen(uri)-1] == '/')                   //line:netp:parseuri:slashcheck
            strcat(filename, "home.html");               //line:netp:parseuri:appenddefault
        return 1;
        }
        else {  /* Dynamic content */                     //line:netp:parseuri:isdynamic
        ptr = index(uri, '?');                           //line:netp:parseuri:beginextract
        if (ptr) {
            strcpy(cgiargs, ptr+1);
            *ptr = '';
        }
        else 
            strcpy(cgiargs, "");                         //line:netp:parseuri:endextract
        strcpy(filename, ".");                           //line:netp:parseuri:beginconvert2
        strcat(filename, uri);                           //line:netp:parseuri:endconvert2
        return 0;
        }
    }

    (6)serve_static 函数

    Tiny提供四种不同的静态内容:HTML文件、无格式的文本文件、GIF编码格式图片、JPEG编码格式图片
    serve_static 函数发送一个HTTP响应,其主体包含一个本地文件的内容。
    首先我们通过检查文件名称的后缀来推断文件类型(第7行)。而且发送响应行和响应报头给client(第8-12行)。

    注意用一个空行终止报头
    第16行,我们使用 unix mmap函数将被请求文件映射到一个虚拟问存储器空间,调用mmap将文件srcfd的前filesize个字节映射到一个从地址srcp開始的私有仅仅读虚拟存储器区域。
    一旦文件映射到存储器,就不再须要它的描写叙述符了,关闭这个文件(第17行)。
    第18行执行的是到client的实际文件传动。rio_writen 函数拷贝从srcp位置開始的filesize个字节(已经被映射到了所请求的文件) 到client的已连接描写叙述符。
    第19行释放了映射的虚拟存储器区域,避免潜在的存储器泄漏

    void serve_static(int fd, char *filename, int filesize) 
    {
        int srcfd;
        char *srcp, filetype[MAXLINE], buf[MAXBUF];
    
        /* Send response headers to client */
        get_filetype(filename, filetype);       //line:netp:servestatic:getfiletype
        sprintf(buf, "HTTP/1.0 200 OK
    ");    //line:netp:servestatic:beginserve
        sprintf(buf, "%sServer: Tiny Web Server
    ", buf);
        sprintf(buf, "%sContent-length: %d
    ", buf, filesize);
        sprintf(buf, "%sContent-type: %s
    
    ", buf, filetype);
        Rio_writen(fd, buf, strlen(buf));       //line:netp:servestatic:endserve
    
        /* Send response body to client */
        srcfd = Open(filename, O_RDONLY, 0);    //line:netp:servestatic:open
        srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
        Close(srcfd);                           //line:netp:servestatic:close
        Rio_writen(fd, srcp, filesize);         //line:netp:servestatic:write
        Munmap(srcp, filesize);                 //line:netp:servestatic:munmap
    }
    
    /*
     * get_filetype - derive file type from file name
     */
    void get_filetype(char *filename, char *filetype) 
    {
        if (strstr(filename, ".html"))
        strcpy(filetype, "text/html");
        else if (strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
        else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
        else
        strcpy(filetype, "text/plain");
    }

    (7)serve_dynamic 函数

    Tiny通过派生一个子进程并在子进程的上下文中执行一个CGI程序。来提供各种类型的动态内容。
    serve_dynamic函数一開始就向client发送一个表明成功的响应行,,同一时候还包含带有信息的server报头。


    第13行,子进程用来自请求URI的CGI參数初始化QUERY_STRING环境变量
    第14行,子进程重定向它的标准输出到已连接文件描写叙述符
    第15行,载入并执行CGI程序。由于CGI程序执行在子进程的上下文中,它能够訪问全部在调用execve函数之前就存在的打开文件和环境变量
    第17行,父进程堵塞在对wait的调用中,等待子进程终止的时候。回收操作系统那个分配给子进程的资源

    void serve_dynamic(int fd, char *filename, char *cgiargs) 
    {
        char buf[MAXLINE], *emptylist[] = { NULL };
    
        /* Return first part of HTTP response */
        sprintf(buf, "HTTP/1.0 200 OK
    "); 
        Rio_writen(fd, buf, strlen(buf));
        sprintf(buf, "Server: Tiny Web Server
    ");
        Rio_writen(fd, buf, strlen(buf));
    
        if (Fork() == 0) { /* child */ //line:netp:servedynamic:fork
        /* Real server would set all CGI vars here */
        setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
        Dup2(fd, STDOUT_FILENO);         /* Redirect stdout to client */ //line:netp:servedynamic:dup2
        Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
        }
        Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
    }

    5.调试及执行

    (1) 下载csapp.h 和 csapp.c

    http://csapp.cs.cmu.edu/public/ics2/code/include/csapp.h
    http://csapp.cs.cmu.edu/public/ics2/code/src/csapp.c
    关于CSAPP代码下载的技巧:比方code/conc/sbuf.c,对应的下载地址在
    http://csapp.cs.cmu.edu/public/ics2/code/conc/sbuf.c

    (2) 编译

    将全部源文件tiny.c、csapp.c和csapp.h放在同一个文件夹下。

    $ gcc -o tiny tiny.c csapp.c -lpthread

    注:加-lpthread是由于csapp.c中有些函数用了多线程库

    (3) 执行前准备

    1. 将被訪问的文件放在tiny同级文件夹下(home.html、photo.jpg)
    <html>
    <head>
    <title>Hello World</title>
    </head>
    <body>
    <h1>Welcome to Tiny Web Server</h1>
    </body>
    </html>
    
    1. 将測试用CGI程序放到cgi-bin文件夹下。并编译成可执行程序
    $ gcc -o adder adder.c

    (4) 执行流程及其结果

    1. 执行Tiny程序,并指定port号(1024–49151可用,其它为知名port)
    $ ./tiny 1024
    1. 浏览器訪问静态内容(home.html)
      TWS

    2. 浏览器訪问不存在的内容
      TWS Error

    3. 浏览器訪问动态内容
      TWS adder

    4. 还能够訪问图片哦
      Kali Linux

    (5) Telnet 測试

    1. 连接到Tinyserver
    $ telnet localhost 1024
    1. 输入请求头(注意空行)
    GET /home.html HTTP/1.0
    
    
    1. 验证结果(注意空行)
    HTTP/1.0 200 OK
    Server: Tiny Web Server
    Content-length: 108
    Content-type: text/html
    
    <html>
    <head>
    <title>Hello World</title>
    </head>
    <body>
    <h1>Welcome to Tiny Web Server</h1>
    </body>
    </html>
    Connection closed by foreign host.
    1. 错误的返回
    HTTP/1.0 404 Not found
    Content-type: text/html
    Content-length: 143
    
    <html><title>Tiny Error</title><body bgcolor=ffffff>
    404: Not found
    <p>Tiny couldn't find this file: .kkk
    <hr><em>The Tiny Web Server</em>
    Connection closed by foreign host.

    (6) 提醒

    须要注意的是 HTTP 协议的头部和数据之间有一个空行,假设浏览器无法查看到内容,而通过 Telnet 能够得到数据,则能够推断为少了一个空行。

  • 相关阅读:
    树莓派系统安装初始化
    CentOS7搭建配置SVN服务器
    搭建web定时任务管理平台
    Linux 内存清理
    使用kubeadm安装Kubernetes
    Web页面执行shell命令
    解决"libc.so.6: version `GLIBC_2.14' not found"问题
    crontab 任务带日期输出
    Linux 源码安装 Python3
    MongoDB 数据恢复与导出
  • 原文地址:https://www.cnblogs.com/yfceshi/p/7401085.html
Copyright © 2011-2022 走看看