CSAPP Tiny web server源代码分析及搭建执行

时间:2022-04-18 18:35:54

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标准要求每一个文本行都由一对回车和换行符来结束
CSAPP Tiny web server源代码分析及搭建执行

(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>\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方法,假设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, "\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行)。而且发送响应行和响应报头给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\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函数一開始就向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\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程序,并指定port号(1024–49151可用,其它为知名port)
$ ./tiny 1024
  1. 浏览器訪问静态内容(home.html)
    CSAPP Tiny web server源代码分析及搭建执行

  2. 浏览器訪问不存在的内容
    CSAPP Tiny web server源代码分析及搭建执行

  3. 浏览器訪问动态内容
    CSAPP Tiny web server源代码分析及搭建执行

  4. 还能够訪问图片哦
    CSAPP Tiny web server源代码分析及搭建执行

(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 能够得到数据,则能够推断为少了一个空行。