《深入理解计算机系统》中开发了一个小但是功能齐全的称为Tiny的web服务器,这里是Tiny服务器的源码解析。
Web 服务器,一个多么庄严而神圣的名词!没有接触之前,它神秘而不可侵犯;学习研究之后,它又是如此地平易近人。今天,让我们一起走近web服务器。
此时,想像一下,当你自己动手实现了一个服务器时,你会是怎么的兴奋;尤其是将一个真正的浏览器指向我们自己实现的服务器时,看着它显示自己本机上的文本及图片时,那将是怎样的激动时刻呀!
下面就来看看它的实现全过程:
Tiny的准备
Tiny的main程序
Tiny是一个迭代服务器,监听在命令行中确定的端口上的连接请求。在通过open_listenedfd函数打开一个监听套接字以后,Tiny执行典型的无限服务循环,反复地接受一个连接(accept)请求,执行事务(doit),最后关闭连接描述符(close)。
Tiny的doit函数
rio_readinitb(&rio,fd)
:将程序的内部缓存区与描述符相关联。
rio_readlineb(&rio,buf,MAXLINE) :从内部缓存区读出一个文本行至buf中,以null字符来结束这个文本行。当然,每行最大的字符数量不能超过MAXLINE。
sscanf(buf,"%s %s %s",method,uri,version) :作为例子,一般此时buf中存放的是“GET / HTTP/1.1”,所以可知method为“GET”,uri为“/”,version为“HTTP/1.1”。其中sscanf的功能:把buf中的字符串以空格为分隔符分别传送到method、uri及version中。
strcasecmp(method,"GET") :忽略大小写比较method与“GET”的大小,相等的话返回0。
read_requesthdrs(&rio) :读并忽略请求报头。
parse_uri(uri,filename,cgiargs) :解析uri,得文件名存入filename中,参数存入cgiargs中。
stat(filename,&sbuf) :将文件filename中的各个元数据填写进sbuf中,如果找不到文件返回0。
S_ISREG(sbuf,st_mode) :此文件为普通文件。
S_IRUSR & sbuf.st_mode :有读取权限。
serve_static(fd,filename,sbuf.st_size) :提供静态服务。
serve_dynamic(fd,filename,cgiargs) :提供动态服务。
从doit函数中可知,我们的Tiny Web服务器只支持“GET”方法,其他方法请求的话则会发送一条错误消息,主程序返回,并等待下一个请求。否则,我们读并忽略请求报头。(其实,我们在请求服务时,直接不用写请求报头即可,写上只是为了符合HTTP协议标准)。
然后,我们将uri解析为一个文件名和一个可能为空的CGI参数,并且设置一个标志位,表明请求的是静态内容还是动态内容。通过stat函数判断文件是否存在。
最后,如果请求的是静态内容,我们需要检验它是否是一个普通文件,并且可读。条件通过,则我们服务器向客服端发送静态内容;相似的,如果请求的是动态内容,我就核实该文件是否是可执行文件,如果是则执行该文件,并提供动态功能。
Tiny的clienterror函数
向客户端返回错误信息。
sprintf(buf,"------------"):将字符串“------------”输送到buf中。
rio_writen(fd,buf,strlen(buf)):将buf中的字符串写入fd描述符中。
Tiny的read_requesthdrs函数
Tiny不需要请求报头中的任何信息,所以我们这个函数就是来跳过这些请求报头的。具体做法就是读这些请求报头,直到空行,然后返回。OK!
Tiny的parse_uri函数
根据uri中是否含有cgi-bin来判断请求的是静态内容还是动态内容。如果没有cgi-bin,则说明请求的是静态内容。那么,我们需把cgiargs置NULL,然后获得文件名,如果我们请求的uri最后为 “/”,则自动添加上home.html。比如说,我们请求的是“/”,则返回的文件名为“./home.html”,而我们请求“/logo.gif”,则返回的文件名为“./logo.gif”。如果uri中含有cgi-bin,则说明请求的是动态内容。那么,我们需要把参数拷贝到cgiargs中,把要执行的文件路径写入filename。举例来说,uri为/cgi-bin/adder?3&5,则cigargs中
存放的是3&5,filename中存放的是“./cgi-bin/adder”,OK!
index(uri,'?') : 找出uri字符串中第一个出现参数‘?’的地址,并将此地址返回。
Tiny的serve_static函数
打开文件名为filename的文件,把它映射到一个虚拟存储器空间,将文件的前filesize字节映射到从地址srcp开始的虚拟存储区域。关闭文件描述符srcfd,把虚拟存储区的数据写入fd描述符,最后释放虚拟存储器区域。
Tiny的serve_dynamic函数
Tiny通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容。
setenv("QUERY_STRING",cgiargs,1) :设置QUERY_STRING环境变量。
dup2
(fd,STDOUT_FILENO) :重定向它的标准输出到已连接描述符。此时,任何写到标准输出的东西都直接写到客户端。
execve(filename,emptylist,environ) :加载运行cgi程序。
Tiny是一个迭代服务器,通过命令行中传递来的端口值,调用Open_listenfd()函数打开一个监听套接字,然后Tiny执行无限循环:服务器阻塞在accept,等待监听描述符listenfd上的连接请求,当服务器从accept返回connfd,表明已经与客户端建立起了连接,执行事务,并关闭连接它的那一端,进行下一次循环。
#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 *shorting,char *longmsg);
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(0);
}
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()处理HTTP事务
先了解一下HTTP请求的组成。一个HTTP请求是由一个请求行,后面跟随零个或者更多个请求报头,再跟随一个空的文本行来终止报头列表。
HTTP请求行的格式如下。
<method><uri><version>
HTTP支持许多的方法,包括GET、POST、OPTIONS、HEAD、PUT、DELETE和TRACE。目前Tiny只支持GET方法,GET方法指导服务器生成和返回URI标识的内容。URI是URL的后缀,包括文件名和参数。版本字段表明了该请求遵循的HTTP版本,有HTTP/1.0和HTTP/1.1。
doit()处理HTTP事务。
读取并解析请求行,代码中使用Rio_readlineb()从fd读取一行数据到buf,然后分别写入变量method,uri和version。
Tiny不使用请求报头中的任何信息,使用read_requesthdrs()函数忽略掉报头的信息。
从请求中提取URI信息,使用parse_uri()来从URI中提取文件名和请求参数,并返回值标识静态内容或者动态内容。使用stat()获取文件的状态并将状态保存到sbuf中,执行成功返回0,如果执行失败则会返回-1表示该文件在磁盘上不存在。
如果请求的是静态内容,需要验证该文件是一般文件(st_mode == S_ISREG)并且我们有读权限。如果是我们就向客户端提供静态内容。相似的如果请求的是动态内容,需要验证该文件是可执行文件,如果是我们就向客户端提供动态内容。
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);
sscanf(buf, "%s %s %s", method, uri, version);
if(strcasecmp(method,"GET"))
{
clienterror(fd, method, "501","Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio);
/*prase URI from GET request*/
is_static = parse_uri(uri, filename, cgiargs);
if(stat(filename, &sbuf) < 0)
{
clienterror(fd, filename, "404","Not Found",
"Tiny couldn't find this file");
return;
}
if(is_static)//server static content
{
if(!(S_ISREG(sbuf.st_mode) || !(S_IRUSR & sbuf.st_mode)))
{
clienterror(fd, filename, "403","Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size);
}
else//server dynamic content
{
if(!(S_ISREG(sbuf.st_mode) || !(S_IXUSR & sbuf.st_mode)))
{
clienterror(fd, filename, "403","Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs);
}
}
3.clienterror()用于检查一些错误
先了解一下HTTP响应行的组成。HTTP响应和HTTP请求是相似的。一个HTTP响应的组成有:一个响应行,后面跟随零个或者更多的响应报头,再跟随一个终止报头的空行,再跟随响应主体。响应行的格式是:
<version><status code><status message>
版本字段描述了响应所遵循的HTTP版本。状态码是一个三位的正整数,指明对请求的处理。状态消息给出与错误代码等价的英文描述。
clienterror()发送一个HTTP响应报文个给客户端,在响应行中包含状态码和状态消息,响应主体包含一个HTML文件,来向用户解释错误。
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg)
{
char buf[MAXLINE], body[MAXBUF];
/*Build the HTTP response body*/
sprintf(body, "<html><title>Tiny Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n",body);
sprintf(body, "%s%s: %s\r\n",body,errnum,shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Tiny Web Server</em><>\r\n",body);
/*Print the HTTP response*/
sprintf(buf, "HTTP/1.0 %s %s\r\n",errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n",(int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}
4.read_requesthdrs()来跳过请求报头的信息,直到遇见表示报头结束的空文本行。
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n"))
{
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
5.parse_uri()解析URI参数
parse_uri()将URI解析为一个文件名和一个可选的CGI参数字符串。如果是静态内容,我们将清除CGI参数串(即将cgiargs置空),然后将URI转换为一个相对的UNIX路径名,例如./home.html。如果URI是以/结尾的则要在后面添加默认文件名home.html。如果是动态内容,URI中的文件名和CGI参数是以?分割,以此可以抽出CGI参数和文件名。
int parse_uri(char *uri, char *filename, char *cgiargs) { char *ptr; if(!strstr(uri, "cgi-bin"))//static content { strcpy(cgiargs, ""); strcpy(filename, "."); strcat(filename, uri); if(uri[strlen(uri)-1] == '/') strcat(uri, "home.html"); return 1; } else { ptr = index(uri, '?'); if(ptr) { strcpy(cgiargs, ptr+1); *ptr = '\0'; } else strcpy(cgiargs, ""); strcpy(filename, "."); strcat(filename, uri); return 0; } }
6.serve_static()处理静态内容
Tiny提供四种不同的静态内容:HTML文件、无格式文本文件、编码为GIF和JPEG格式的图片。
serve_static()函数发送一个HTTP响应,响应的主体包括一个本地文件的内容。首先,我们检查文件名的后缀来判断文件类型,并在响应报头的Content-type字段中显示出来。随后我们发送响应行和响应报头给客户端,用一个空行来终止报头。
然后我们以只读的方式打开所请求的文件获得文件句柄srcfd,并用mmap函数将文件映射到虚拟存储器空间,代码中是将srcfd指向的文件的前filesize个字节映射到一个地址从srcp开始的只读私有虚拟存储器区域。一旦映射成功我们就可以通过srcp来操作文件不再需要文件句柄了,所以我们关闭srcfd。然后我们使用Rio_writen()将文件传送到客户端。这时静态内容的处理操作已经完成了,我们释放srcp的虚拟存储器映射。
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); sprintf(buf, "HTTP/1.0 200 OK\r\n"); sprintf(buf, "%sServer: Tiny Web Server\r\n", buf); sprintf(buf, "%sContent-lenght: %d\r\n", buf, filesize); sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype); Rio_writen(fd, buf, strlen(buf)); /*Send response body to client*/ srcfd = Open(filename, O_RDONLY, 0); srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE,srcfd,0); close(srcfd); Rio_writen(fd, srcp, filesize); Munmap(srcp, filesize); } 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程序,来提供各种类型的动态内容。
程序中首先发送一个表明成功的响应行,同时还包括带有信息的Server报头给客户端。CGI程序负责发送响应的剩余部分。
然后我们派生一个子进程,子进程用来自请求URI的CGI参数初始化QUERY_STRING环境变量,CGI程序会通过这个变量获取CGI参数值。接下来,子进程重定向标准输出到已连接文件描述符,然后加载并运行CGI程序。因为CGI程序运行在子进程的上下文中,它能够访问所有在调用execve函数之前就存在的打开文件和环境变量。因此CGI程序写到标准输出上的数据都会直接送到客户端进程,不会受到来自父进程的干涉。旗舰父进程阻塞在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) { setenv("QUERY_STRING", cgiargs, 1); Dup2(fd, STDOUT_FILENO); Execve(filename, emptylist,environ); } Wait(NULL); }