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
表示因特网主机 www.google.com 上一个称为 index.html 的HTML文件。它是由一个监听port80的Webserver所管理的。
HTTP默认port号为80
可执行文件的URL能够在文件名称后包含程序參数, “?”字符分隔文件名称和參数,而且每一个參数都用“&”字符分隔开。如:
表示一个 /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>\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) 执行前准备
- 将被訪问的文件放在tiny同级文件夹下(home.html、photo.jpg)
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h1>Welcome to Tiny Web Server</h1>
</body>
</html>
- 将測试用CGI程序放到cgi-bin文件夹下。并编译成可执行程序
$ gcc -o adder adder.c
(4) 执行流程及其结果
- 执行Tiny程序,并指定port号(1024–49151可用,其它为知名port)
$ ./tiny 1024
浏览器訪问静态内容(home.html)
浏览器訪问不存在的内容
浏览器訪问动态内容
还能够訪问图片哦
(5) Telnet 測试
- 连接到Tinyserver
$ telnet localhost 1024
- 输入请求头(注意空行)
GET /home.html HTTP/1.0
- 验证结果(注意空行)
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.
- 错误的返回
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 能够得到数据,则能够推断为少了一个空行。