深入理解计算机系统 实现一个小型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程序写到标准输出的东西都会直接到达客户端

小型web服务器功能:为web浏览器提供静态的和动态的内容

使用到的思想:进程控制,Unix I/O,套接字接口,HTTP

主程序:监听在命令行中传递来的端口上的连接请求,在通过调用open_listenfd函数打开一个监听套接字以后,执行典型的无限服务器循环,不断接受连接请求,执行事务,然后关闭连接它的那一端。

open_clientfd:将socket和connect封装成一个叫做open_clientfd的函数是很方便的,客户端可以用它来和服务器建立连接。
int open_clientfd(char * hostname, int port);
         返回:成功则为描述符,若Unix出错则为-1,若DNS出错则为-2。

/*  HTTP/1.0 服务器,用GET 方法获取静态和动态内容 */

1.头文件:

[cpp] view plain copy

  1. /* 
  2.    TINY - A simple ,iterative HTTP/1.0 Web server 
  3. */  
  4. #ifndef __CSAPP_H__    
  5. #define __CSAPP_H__    
  6. #include <stdio.h>    
  7. #include <stdlib.h>    
  8. #include <unistd.h>    
  9. #include <string.h>    
  10. #include <ctype.h>    
  11. #include <setjmp.h>    
  12. #include <signal.h>    
  13. #include <sys/time.h>    
  14. #include <sys/types.h>    
  15. #include <sys/wait.h>    
  16. #include <sys/stat.h>    
  17. #include <fcntl.h>    
  18. #include <sys/mman.h>    
  19. #include <errno.h>    
  20. #include <math.h>    
  21. #include <semaphore.h>    
  22. #include <sys/socket.h>    
  23. #include <netdb.h>    
  24. #include <netinet/in.h>    
  25. #include <arpa/inet.h>    
  26. //以上的头文件按说都是在”csapp.h”中,但是我试了试不行的,所以就直接自己写了  
  27. #define DEF_MODE   S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH    
  28. #define DEF_UMASK  S_IWGRP|S_IWOTH    
  29. typedef struct sockaddr SA;    
  30. #define RIO_BUFSIZE 8192    
  31. typedef struct {    
  32.     int rio_fd;                /* 内部缓存区的描述符 */    
  33.     int rio_cnt;               /* 内部缓存区剩下还未读的字节数 */    
  34.     char *rio_bufptr;          /* 指向内部缓存区中下一个未读字节 */    
  35.     char rio_buf[RIO_BUFSIZE]; /* 内部缓存区 */    
  36. } rio_t;    
  37. extern char **environ;     
  38. #define MAXLINE  8192  /* 每行最大字符数 */    
  39. #define MAXBUF   8192  /* I/O缓存区的最大容量 */    
  40. #define LISTENQ  1024  /* 监听的第二个参数 */    
  41. /* helper functions */    
  42. ssize_t rio_writen(int fd,void *usrbuf,size_t n);    
  43. void rio_readinitb(rio_t *rp,int fd);  //将程序的内部缓存区与描述符相关联。  
  44. ssize_t rio_readlineb(rio_t *rp,void *usrbuf,size_t maxlen);  /*从内部缓存区读出一个文本行至buf中,以null字符来结束这个文本行。当然, 
  45.     每行最大的字符数量不能超过MAXLINE。*/  
  46. int open_clientfd(char *hostname, int portno);    
  47. int open_listenfd(int portno);    
  48. #endif     
  49.   
  50. void doit(int fd);  
  51. void read_requesthdrs(rio_t *rp);  //读并忽略请求报头  
  52. int parse_uri(char *uri, char *filename, char *cgiargs);   //解析uri,得文件名存入filename中,参数存入cgiargs中。  
  53. void serve_static(int fd, char *filename, int filesize);   //提供静态服务。  
  54. void get_filetype(char *filename, char *filetype);  
  55. void serve_dynamic(int fd, char *cause, char *cgiargs);    //提供动态服务。  
  56. void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);  
  57. /* 
  58.     Tiny是一个迭代服务器,监听在命令行中确定的端口上的连接请求。在通过open_listenedfd函数打开 
  59.     一个监听套接字以后,Tiny执行典型的无限服务循环,反复地接受一个连接(accept)请求,执行事务(doit), 
  60.     最后关闭连接描述符(close)

2.Tiny的main函数

[cpp] view plain copy

  1. int main(int argc, char const *argv[])  
  2. {  
  3.     int listenfd, connfd, port, clientlen;  
  4.     struct sockaddr_in clientaddr;  
  5.   
  6.     if(argc != 2) {  
  7.         fprintf(stderr, "usage: %s\n", argv[0]);  
  8.         exit(1);  
  9.     }     
  10.     port = atoi(argv[1]);  
  11.   
  12.     listenfd = open_listenfd(port);  
  13.     while(1) {  
  14.         clientlen = sizeof(clientaddr);  
  15.         connfd = accept(listenfd,(SA *)&clientaddr,&clientlen);  
  16.         doit(connfd);  
  17.         close(connfd);  
  18.     }  
  19. }  

    3.Tiny的doit函数:处理HTTP事务

1、 sscanf() 的作用:从一个字符串中读进与指定格式相符的数据.

原型: int sscanf (const char *str,const char * format,........);

说明: sscanf()会将参数str的字符串根据参数format字符串来转换并格式化数据。转换后的结果存于对应的参数内。

成功则返回参数数目,失败则返回0。

注意:sscanf与scanf类似,都是用于输入的,只是后者以键盘(stdin)为输入源,前者以固定字符串为输入源。

2、strcasecmp()函数:判断字符串是否相等(忽略大小写)

返回值:若参数s1 和s2 字符串相同则返回0。s1 长度大于s2 长度则返回大于0 的值,s1 长度若小于s2 长度则返回小于0 的值。

[cpp] view plain copy

  1. void doit(int fd)  
  2. {  
  3.     int is_static;  
  4.     struct stat sbuf;  
  5.     char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];  
  6.     char filename[MAXLINE],cgiargs[MAXLINE];  
  7.     rio_t rio;  
  8.   
  9.     rio_readinitb(&rio,fd);  
  10.     rio_readlineb(&rio,buf,MAXLINE);  
  11.     sscanf(buf,"%s %s %s",method,uri,version);  
  12.     if(strcasecmp(method,"GET")) {  
  13.         clienterror(fd,method,"501","Not Implemented","Tiny does not implement this method");  
  14.         return;  
  15.     }  
  16.     read_requesthdrs(&rio); //读并且忽略任何请求报头 
  17.   
  18.     is_static = parse_uri(uri,filename,cgiargs); //判断是静态内容还是动态内容 
  19.     if(stat(filename,&sbuf) < 0) {  判断文件的存在性
  20.         clienterror(fd,filename, "404""Not found","Tiny coundn't find this file");  
  21.         return;  
  22.     }  
  23.   
  24.     if(is_static) {  
  25.         if(!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {  //根据st_mode判断文件类型,S_ISEG判断是否为普通文件
  26.             clienterror(fd,filename, "403""Forbidden","Tiny coundn't read the file");  
  27.             return;  
  28.         }  
  29.         serve_static(fd,filename,sbuf.st_size);  //提供静态服务
  30.     }  
  31.     else {  
  32.         if(!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {  
  33.             clienterror(fd,filename, "403""Forbidden","Tiny coundn't run the CGI program");  
  34.             return;  
  35.         }  
  36.         serve_dynamic(fd,filename,cgiargs);  //提供动态服务
  37.     }  
  38. }  
  39. /* 
  40.     从doit函数中可知,我们的Tiny Web服务器只支持“GET”方法,其他方法请求的话则会发送一条错误消息,主程序返回 
  41.     ,并等待下一个请求。否则,我们读并忽略请求报头。(其实,我们在请求服务时,直接不用写请求报头即可,写上只是 
  42.     为了符合HTTP协议标准)。 
  43.  
  44.     然后,我们将uri解析为一个文件名和一个可能为空的CGI参数,并且设置一个标志位,表明请求的是静态内容还是动态 
  45.     内容。通过stat函数判断文件是否存在。 
  46.  
  47.     最后,如果请求的是静态内容,我们需要检验它是否是一个普通文件,并且可读。条件通过,则我们服务器向客服端发送 
  48.     静态内容;相似的,如果请求的是动态内容,我就核实该文件是否是可执行文件,如果是则执行该文件,并提供动态功能。 
  49.  
  50. */  

   4.Tiny的clienterror函数

[cpp] view plain copy

  1. void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)  
  2. {  
  3.     char buf[MAXLINE],body[MAXBUF];  
  4.   
  5.     sprintf(body,"<html><title>Tiny Error</title>");  
  6.     sprintf(body,"%s<body bgcolor=""ffffff"">\r\n",body);  
  7.     sprintf(body,"%s%s: %s\r\n",body,errnum,shortmsg);  
  8.     sprintf(body,"%s<p>%s: %s\r\n",body,longmsg,cause);  
  9.     sprintf(body,"%s<hr><em>The Web server</em>\r\n",body);  
  10.   
  11.     sprintf(buf,"HTTP/1.0 %s %s\r\n",errnum,longmsg);  
  12.     rio_writen(fd,buf,strlen(buf));  
  13.     sprintf(buf,"Content-type: text/html\r\n");  
  14.     rio_writen(fd,buf,strlen(buf));  
  15.     sprintf(buf,"sContent-length: %d\r\n\r\n",(int)strlen(body));  
  16.     rio_writen(fd,buf,strlen(buf));  
  17.     rio_writen(fd,body,strlen(body));  
  18. }  
  19. /* 
  20.    向客户端返回错误信息。 
  21.  
  22.    sprintf(buf,"------------"):将字符串“------------”输送到buf中。 
  23.  
  24.    rio_writen(fd,buf,strlen(buf)):将buf中的字符串写入fd描述符中。 
  25. */  

    5.Tiny的read_requesthdrs函数

[cpp] view plain copy

  1. void read_requesthdrs(rio_t *rp)  
  2. {  
  3.     char buf[MAXLINE];  
  4.     rio_readlineb(rp,buf,MAXLINE);  
  5.     while(strcmp(buf,"\r\n")) {  
  6.         rio_readlineb(rp,buf,MAXLINE);  
  7.         printf("%s", buf);  
  8.     }  
  9.     return;  
  10. }  
  11. /* 
  12.     Tiny不需要请求报头中的任何信息,这个函数就是来跳过这些请求报头的,读这些请求报头,直到空行,然后返回。 
  13. */  

    6.Tinyparse_uri函数

strstr(*str1, *str2)实现从字符串str1中查找是否有字符串str2,如果有,从str1中的str2位置起,返回str1中str2起始位置的指针,如果没有,返回null。

[cpp] view plain copy

  1. int parse_uri(char *uri, char *filename,char *cgiargs)  
  2. {  
  3.     char *ptr;  
  4.   
  5.     if(!strstr(uri,"cgi-bin")) {  //静态内容
  6.         strcpy(cgiargs,"");  //清除CGI字符串
  7.         strcpy(filename,".");  
  8.         strcat(filename,uri);  
  9.         if(uri[strlen(uri)-1] == '/') {  
  10.             strcat(filename,"home.html");  
  11.         }  
  12.         return 1;  
  13.     }  
  14.     else {  
  15.         ptr = index(uri,'?');  
  16.         if(ptr) {  
  17.             strcpy(cgiargs,ptr+1);  
  18.             *ptr = '\0';  
  19.         }  
  20.         else {  
  21.             strcpy(cgiargs,"");  
  22.         }  
  23.         strcpy(filename,".");  
  24.         strcat(filename,uri);  
  25.         return 0;  
  26.     }  
  27. }  
  28. /* 
  29.    根据uri中是否含有cgi-bin来判断请求的是静态内容还是动态内容。如果没有cgi-bin,则说明请求的是静态内容。那么 
  30.    ,我们需把cgiargs置NULL,然后获得文件名,如果我们请求的uri最后为 “/”,则自动添加上home.html。比如说,我 
  31.    请求的是“/”,则返回的文件名为“./home.html”,而我们请求“/logo.gif”,则返回的文件名为“./logo.gif”。如果 
  32.    uri中含有cgi-bin,则说明请求的是动态内容。那么,我们需要把参数拷贝到cgiargs中,把要执行的文件路径写入 
  33.    ilename。举例来说,uri为/cgi-bin/adder?12&45,则cigargs中存放的是12&45,filename中存放的是 
  34.    “./cgi-bin/adder” 
  35.  
  36.    index(uri,'?') : 找出uri字符串中第一个出现参数‘?’的地址,并将此地址返回。 
  37. */  

   7.Tiny的serve_static函数

[cpp] view plain copy

  1. void serve_static(int fd, char *filename, int filesize)  
  2. {  
  3.     int srcfd;  
  4.     char *srcp,filetype[MAXLINE],buf[MAXBUF];  
  5.   
  6.     get_filetype(filename,filetype);  
  7.     sprintf(buf,"HTTP/1.0 200 OK\r\n");  
  8.     sprintf(buf,"%sServer:Tiny Web Server\r\n",buf);  
  9.     sprintf(buf,"%sContent-length:%d\r\n",buf,filesize);  
  10.     sprintf(buf,"%sContent-type:%s\r\n\r\n",buf,filetype);  
  11.     rio_writen(fd,buf,strlen(buf));  
  12.   
  13.     srcfd = open(filename,O_RDONLY,0);  
  14.     srcp = mmap(0,filesize, PROT_READ, MAP_PRIVATE,srcfd,0);  
  15.     close(srcfd);  
  16.     rio_writen(fd,srcp,filesize);  
  17.     munmap(srcp,filesize);  
  18. }  
  19. /* 
  20.     打开文件名为filename的文件,把它映射到一个虚拟存储器空间,将文件的前filesize字节映射到从地址srcp开始的 
  21.     虚拟存储区域。关闭文件描述符srcfd,把虚拟存储区的数据写入fd描述符,最后释放虚拟存储器区域。 
  22. */  
  23. void get_filetype(char *filename, char *filetype)  
  24. {  
  25.     if(strstr(filename,".html"))  
  26.         strcpy(filetype,"text/html");  
  27.     else if(strstr(filename,".gif"))  
  28.         strcpy(filetype,"image/gif");  
  29.     else if(strstr(filename,".jpg"))  
  30.         strcpy(filetype,"image/jpg");  
  31.     else   
  32.         strcpy(filetype,"text/plain");  
  33. }  

    8.Tiny的server_dynamic函数

[cpp] view plain copy

  1. void serve_dynamic(int fd, char *filename, char *cgiargs)  
  2. {  
  3.     char buf[MAXLINE],*emptylist[] = {NULL};  
  4.   
  5.     sprintf(buf,"HTTP/1.0 200 OK\r\n");  
  6.     rio_writen(fd,buf,strlen(buf));  
  7.     sprintf(buf,"Server:Tiny Web Server\r\n");  
  8.     rio_writen(fd,buf,strlen(buf));  
  9.   
  10.     if(fork() == 0) {  
  11.         setenv("QUERY_STRING",cgiargs,1);  
  12.         dup2(fd,STDOUT_FILENO);  
  13.         execve(filename,emptylist,environ);  
  14.     }  
  15.     wait(NULL);  
  16. }  
  17. /* 
  18.     Tiny通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容。 
  19.  
  20.     setenv("QUERY_STRING",cgiargs,1) :设置QUERY_STRING环境变量。 
  21.  
  22.     dup2 (fd,STDOUT_FILENO) :重定向它的标准输出到已连接描述符。此时,任何写到标准输出的东西都直接写到客户端。 
  23.  
  24.     execve(filename,emptylist,environ) :加载运行cgi程序。 
  25. */  

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
  • 1

注:加-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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. 将测试用CGI程序放到cgi-bin目录下,并编译成可执行程序
$ gcc -o adder adder.c
  • 1

(4) 运行流程及其结果

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

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

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

  4. 还可以访问图片哦 

(5) Telnet 测试

  1. 连接到Tiny服务器
$ telnet localhost 1024
  • 1
  1. 输入请求头(注意空行)
GET /home.html HTTP/1.0

  • 1
  • 2
  • 3
  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 可以得到数据,则可以判断为少了一个空行。


      

猜你喜欢

转载自blog.csdn.net/weixin_41413441/article/details/80534089
今日推荐