CSAPP Tiny web 服务器源码分析及搭建运行

1. Web基础

web客户端和服务器之间的交互使用的是一个基于文本的应用级协议HTTP(超文本传输协议)。一个web客户端(即浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。

对于web客户端和服务器而言,内容是与一个MIME类型相关的字节序列。常见的MIME类型:

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

web服务器以两种不同的方式向客服端提供内容:

1. 静态内容:取一个磁盘文件,并将它的内容返回给客户端

2. 动态内容:执行一个可执行文件,并将它的输出返回给客户端

统一资源定位符:URL

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

表示因特网主机 www.google.com 上一个称为 index.html 的HTML文件,它是由一个监听端口80的Web服务器所管理的。 HTTP默认端口号为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> 为服务器提供了额外的信息,例如浏览器的版本类型

HTTP 1.1中 一个IP地址的服务器可以是 多宿主主机,例如 www.host1.com www.host2.com 可以存在于同一服务器上。

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 错误请求 服务器不能理解请求
403 禁止 服务器无权访问所请求的文件
404 未发现 服务器不能找到所请求的文件
501 未实现 服务器不支持请求的方法
505 HTTP版本不支持 服务器不支持请求的版本

两个最重要的响应报头:

Content-Type 告诉客户端响应主体中内容的MIME类型

Content-Length 指示响应主体的字节大小

响应主体中包含着被请求的内容。

3.服务动态内容

(1) 客户端如何将程序参数传递给服务器

GET请求的参数在URI中传递, “?”字符分隔了文件名和参数,每个参数都用一个”&”分隔开,参数中不允许有空格,必须用字符串“%20”来表示

HTTP POST请求的参数是在请求主体中而不是 URI中传递的

(2)服务器如何将参数传递给子进程

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

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

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

(3)服务器如何将其他信息传递给子进程

环境变量 描述
QUERY_STRING 程序参数
SERVER_PORT 父进程侦听的端口
REQUEST_METHOD GET 或 POST
REMOTE_HOST 客户端的域名
REMOTE_ADDR 客户端的点分十进制IP地址
CONTENT_TYPE 只对POST而言,请求体的MIME类型
CONTENT_LENGTH 只对POST而言,请求体的字节大小

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

一个CGI程序将它的动态内容发送到标准输出,在子进程加载并运行CGI程序之前,它使用UNIX dup2 函数将它标准输出重定向到和客户端相关连的已连接描述符

因此,任何CGI程序写到标准输出的东西都会直接到达客户端

4. 综合: Tiny web 服务器源码及分析

(1) main程序

Tiny是一个迭代服务器,监听在命令行中传递来的端口上的连接请求,在通过调用 open_listenfd 函数打开一个监听套接字以后,执行无限服务器循环,不断接受连接请求(第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>\n", 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方法,如果客户端请求其他方法,发送一个错误信息。

然后将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, "\r\n")) {          //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 = ‘\0‘;
    }
    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行),并且发送响应行和响应报头给客户端(第8-12行)。注意用一个空行终止报头

第16行,我们使用 unix mmap函数将被请求文件映射到一个虚拟问存储器空间,调用mmap将文件srcfd的前filesize个字节映射到一个从地址srcp开始的私有只读虚拟存储器区域。

一旦文件映射到存储器,就不再需要它的描述符了,关闭这个文件(第17行)。

第18行执行的是到客户端的实际文件传动。rio_writen 函数拷贝从srcp位置开始的filesize个字节(已经被映射到了所请求的文件) 到客户端的已连接描述符。

第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\r\n");    //line:netp:servestatic:beginserve
    sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", 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函数一开始就向客户端发送一个表明成功的响应行,,同时还包括带有信息的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\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: Tiny Web Server\r\n");
    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程序,并指定端口号(1024–49151可用,其它为知名端口)
$ ./tiny 1024
  1. 浏览器访问静态内容(home.html)

  2. 浏览器访问不存在的内容

  3. 浏览器访问动态内容

  4. 还可以访问图片哦

(5) Telnet 测试

  1. 连接到Tiny服务器
$ 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 可以得到数据,则可以判断为少了一个空行。

时间: 2024-10-28 13:12:08

CSAPP Tiny web 服务器源码分析及搭建运行的相关文章

【TencentOS tiny】深度源码分析(4)——消息队列

消息队列 在前一篇文章中[TencentOS tiny学习]源码分析(3)--队列 我们描述了TencentOS tiny的队列实现,同时也点出了TencentOS tiny的队列是依赖于消息队列的,那么我们今天来看看消息队列的实现. 其实消息队列是TencentOS tiny的一个基础组件,作为队列的底层. 所以在tos_config.h中会用以下宏定义: #if (TOS_CFG_QUEUE_EN > 0u) #define TOS_CFG_MSG_EN 1u #else #define T

Spring源码分析——源码分析环境搭建

1.在Windows上安装Gradle gradle工具类似于maven,用于项目的构建,此处主要用于构建spring源码,以便我们将spring源码导入eclipse. 开发环境 Java:JDK8(必须是JDK或JRE7以上,使用java -version查看当前电脑java版本) 操作系统:Windows 安装步骤 下载最新的Gradle压缩包:Gradle官网:https://gradle.org/,当前最新版本下载地址:https://gradle.org/releases/,下载bi

【TencentOS tiny】深度源码分析(2)——调度器

温馨提示:本文不描述与浮点相关的寄存器的内容,如需了解自行查阅(毕竟我自己也不懂) 调度器的基本概念 TencentOS tiny中提供的任务调度器是基于优先级的全抢占式调度,在系统运行过程中,当有比当前任务优先级更高的任务就绪时,当前任务将立刻被切出,高优先级任务抢占处理器运行. TencentOS tiny内核中也允许创建相同优先级的任务.相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器),时间片轮转调度仅在当前系统中无更高优先级就绪任务的情况下才有效. 为了保证系统的实

C# web服务器 源码,无需IIS环境,针对ajax处理

一个c#写的的web服务器,只进行简单的处理HTTP请求,第一次写,功能比较简单,比较适合做API服务器 因为是类的方式,可以嵌入任何程序中 代码 using System;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading; namespace Web{ class HTTPServer { private const int BufferSize = 4096; private

web.py源码分析: application

本文主要分析的是web.py库的 application.py 这个模块中的代码.总的来说, 这个模块主要实现了WSGI兼容的接口,以便应用程序能够被WSGI应用服务器调用 .WSGI是 Web Server Gateway Interface 的缩写,具体细节可以查看 WSGI的WIKI页面 接口的使用 使用web.py自带的HTTP Server 下面这个例子来自官方文档的 Hello World ,这个代码一般是应用入口的代码: import web urls = ("/.*",

python之epoll服务器源码分析

#!/usr/bin/env python # -*- coding: utf8 -*- import socket, select EOL1 = b'/r/n' EOL2 = b'/r/n/r/n' # 拼接成的response response = b'HTTP/1.0 200 OK/r/nDate: Mon, 1 Jan 1996 01:01:01 GMT/r/n' response += b'Content-Type: text/plain/r/nContent-Length: 13/r

【TencentOS tiny】深度源码分析(8)——软件定时器

软件定时器的基本概念 TencentOS tiny 的软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,本质上软件定时器的使用相当于扩展了定时器的数量,允许创建更多的定时业务,它实现的功能与硬件定时器也是类似的. 硬件定时器是芯片本身提供的定时功能.一般是由外部晶振提供给芯片输入时钟,芯片向软件模块提供一组配置寄存器,接受控制输入,到达设定时间值后芯片中断控制器产生时钟中断.硬件定时器的精度一般很高,可以达到纳秒级别,并且是中

【TencentOS tiny】深度源码分析(3)——队列

队列基本概念 队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间.中断和任务间传递消息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时,读取消息的任务将被阻塞,用户还可以指定任务等待消息的时间timeout,在这段时间中,如果队列为空,该任务将保持阻塞状态以等待队列数据有效.当队列中有新消息时,被阻塞的任务会被唤醒并处理新消息:当等待的时间超过了指定的阻塞时间,即使队列中尚无有效数据,任务也会自动从阻塞态转为就绪态,消息队列是一种异

【TencentOS tiny】深度源码分析(7)——事件

引言 大家在裸机编程中很可能经常用到flag这种变量,用来标志一下某个事件的发生,然后在循环中判断这些标志是否发生,如果是等待多个事件的话,还可能会if((xxx_flag)&&(xxx_flag))这样子做判断.当然,如果聪明一点的同学就会拿flag的某些位做标志,比如这个变量的第一位表示A事件,第二位表示B事件,当这两个事件都发生的时候,就判断flag&0x03的值是多少,从而判断出哪个事件发生了. 但在操作系统中又将如何实现呢? 事件 在操作系统中,事件是一种内核资源,主要用