1. Web基础
web客户端和服务器之间的交互使用的是一个基于文本的应用级协议HTTP(超文本传输协议)。一个web客户端(即浏览器)打开一个到服务器的因特网连接,并且请求某些内容。服务器响应所请求的内容,然后关闭连接。浏览器读取这些内容,并把它显示在屏幕上。
对于web客户端和服务器而言,内容是与一个MIME类型相关的字节序列。常见的MIME类型:
MIME类型 | 描述 |
---|---|
text/html | HTML页面 |
text/plain | 无格式文本 |
image/gif | GIF格式编码的二进制图像 |
image/jpeg | JPEG格式编码的二进制图像 |
web服务器以两种不同的方式向客服端提供内容:
1. 静态内容:取一个磁盘文件,并将它的内容返回给客户端
2. 动态内容:执行一个可执行文件,并将它的输出返回给客户端
统一资源定位符:URL
表示因特网主机 www.google.com 上一个称为 index.html 的HTML文件,它是由一个监听端口80的Web服务器所管理的。 HTTP默认端口号为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>
为服务器提供了额外的信息,例如浏览器的版本类型
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
- /*
- TINY - A simple ,iterative HTTP/1.0 Web server
- */
- #ifndef __CSAPP_H__
- #define __CSAPP_H__
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <string.h>
- #include <ctype.h>
- #include <setjmp.h>
- #include <signal.h>
- #include <sys/time.h>
- #include <sys/types.h>
- #include <sys/wait.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <sys/mman.h>
- #include <errno.h>
- #include <math.h>
- #include <semaphore.h>
- #include <sys/socket.h>
- #include <netdb.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- //以上的头文件按说都是在”csapp.h”中,但是我试了试不行的,所以就直接自己写了
- #define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
- #define DEF_UMASK S_IWGRP|S_IWOTH
- typedef struct sockaddr SA;
- #define RIO_BUFSIZE 8192
- typedef struct {
- int rio_fd; /* 内部缓存区的描述符 */
- int rio_cnt; /* 内部缓存区剩下还未读的字节数 */
- char *rio_bufptr; /* 指向内部缓存区中下一个未读字节 */
- char rio_buf[RIO_BUFSIZE]; /* 内部缓存区 */
- } rio_t;
- extern char **environ;
- #define MAXLINE 8192 /* 每行最大字符数 */
- #define MAXBUF 8192 /* I/O缓存区的最大容量 */
- #define LISTENQ 1024 /* 监听的第二个参数 */
- /* helper functions */
- ssize_t rio_writen(int fd,void *usrbuf,size_t n);
- void rio_readinitb(rio_t *rp,int fd); //将程序的内部缓存区与描述符相关联。
- ssize_t rio_readlineb(rio_t *rp,void *usrbuf,size_t maxlen); /*从内部缓存区读出一个文本行至buf中,以null字符来结束这个文本行。当然,
- 每行最大的字符数量不能超过MAXLINE。*/
- int open_clientfd(char *hostname, int portno);
- int open_listenfd(int portno);
- #endif
- void doit(int fd);
- void read_requesthdrs(rio_t *rp); //读并忽略请求报头
- int parse_uri(char *uri, char *filename, char *cgiargs); //解析uri,得文件名存入filename中,参数存入cgiargs中。
- void serve_static(int fd, char *filename, int filesize); //提供静态服务。
- void get_filetype(char *filename, char *filetype);
- void serve_dynamic(int fd, char *cause, char *cgiargs); //提供动态服务。
- void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);
- /*
- Tiny是一个迭代服务器,监听在命令行中确定的端口上的连接请求。在通过open_listenedfd函数打开
- 一个监听套接字以后,Tiny执行典型的无限服务循环,反复地接受一个连接(accept)请求,执行事务(doit),
- 最后关闭连接描述符(close)
2.Tiny的main函数
[cpp] view plain copy
- int main(int argc, char const *argv[])
- {
- int listenfd, connfd, port, clientlen;
- struct sockaddr_in clientaddr;
- if(argc != 2) {
- fprintf(stderr, "usage: %s\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);
- }
- }
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
- 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;
- 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); //读并且忽略任何请求报头
- is_static = parse_uri(uri,filename,cgiargs); //判断是静态内容还是动态内容
- if(stat(filename,&sbuf) < 0) { 判断文件的存在性
- clienterror(fd,filename, "404", "Not found","Tiny coundn't find this file");
- return;
- }
- if(is_static) {
- if(!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //根据st_mode判断文件类型,S_ISEG判断是否为普通文件
- clienterror(fd,filename, "403", "Forbidden","Tiny coundn't read the file");
- return;
- }
- serve_static(fd,filename,sbuf.st_size); //提供静态服务
- }
- else {
- if(!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
- clienterror(fd,filename, "403", "Forbidden","Tiny coundn't run the CGI program");
- return;
- }
- serve_dynamic(fd,filename,cgiargs); //提供动态服务
- }
- }
- /*
- 从doit函数中可知,我们的Tiny Web服务器只支持“GET”方法,其他方法请求的话则会发送一条错误消息,主程序返回
- ,并等待下一个请求。否则,我们读并忽略请求报头。(其实,我们在请求服务时,直接不用写请求报头即可,写上只是
- 为了符合HTTP协议标准)。
- 然后,我们将uri解析为一个文件名和一个可能为空的CGI参数,并且设置一个标志位,表明请求的是静态内容还是动态
- 内容。通过stat函数判断文件是否存在。
- 最后,如果请求的是静态内容,我们需要检验它是否是一个普通文件,并且可读。条件通过,则我们服务器向客服端发送
- 静态内容;相似的,如果请求的是动态内容,我就核实该文件是否是可执行文件,如果是则执行该文件,并提供动态功能。
- */
4.Tiny的clienterror函数
[cpp] view plain copy
- void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
- {
- char buf[MAXLINE],body[MAXBUF];
- 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 Web server</em>\r\n",body);
- sprintf(buf,"HTTP/1.0 %s %s\r\n",errnum,longmsg);
- rio_writen(fd,buf,strlen(buf));
- sprintf(buf,"Content-type: text/html\r\n");
- rio_writen(fd,buf,strlen(buf));
- sprintf(buf,"sContent-length: %d\r\n\r\n",(int)strlen(body));
- rio_writen(fd,buf,strlen(buf));
- rio_writen(fd,body,strlen(body));
- }
- /*
- 向客户端返回错误信息。
- sprintf(buf,"------------"):将字符串“------------”输送到buf中。
- rio_writen(fd,buf,strlen(buf)):将buf中的字符串写入fd描述符中。
- */
5.Tiny的read_requesthdrs函数
[cpp] view plain copy
- 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;
- }
- /*
- Tiny不需要请求报头中的任何信息,这个函数就是来跳过这些请求报头的,读这些请求报头,直到空行,然后返回。
- */
6.Tiny的parse_uri函数
strstr(*str1, *str2)实现从字符串str1中查找是否有字符串str2,如果有,从str1中的str2位置起,返回str1中str2起始位置的指针,如果没有,返回null。
[cpp] view plain copy
- int parse_uri(char *uri, char *filename,char *cgiargs)
- {
- char *ptr;
- if(!strstr(uri,"cgi-bin")) { //静态内容
- strcpy(cgiargs,""); //清除CGI字符串
- strcpy(filename,".");
- strcat(filename,uri);
- if(uri[strlen(uri)-1] == '/') {
- strcat(filename,"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;
- }
- }
- /*
- 根据uri中是否含有cgi-bin来判断请求的是静态内容还是动态内容。如果没有cgi-bin,则说明请求的是静态内容。那么
- ,我们需把cgiargs置NULL,然后获得文件名,如果我们请求的uri最后为 “/”,则自动添加上home.html。比如说,我
- 请求的是“/”,则返回的文件名为“./home.html”,而我们请求“/logo.gif”,则返回的文件名为“./logo.gif”。如果
- uri中含有cgi-bin,则说明请求的是动态内容。那么,我们需要把参数拷贝到cgiargs中,把要执行的文件路径写入
- ilename。举例来说,uri为/cgi-bin/adder?12&45,则cigargs中存放的是12&45,filename中存放的是
- “./cgi-bin/adder”
- index(uri,'?') : 找出uri字符串中第一个出现参数‘?’的地址,并将此地址返回。
- */
7.Tiny的serve_static函数
[cpp] view plain copy
- void serve_static(int fd, char *filename, int filesize)
- {
- int srcfd;
- char *srcp,filetype[MAXLINE],buf[MAXBUF];
- 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-length:%d\r\n",buf,filesize);
- sprintf(buf,"%sContent-type:%s\r\n\r\n",buf,filetype);
- rio_writen(fd,buf,strlen(buf));
- 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);
- }
- /*
- 打开文件名为filename的文件,把它映射到一个虚拟存储器空间,将文件的前filesize字节映射到从地址srcp开始的
- 虚拟存储区域。关闭文件描述符srcfd,把虚拟存储区的数据写入fd描述符,最后释放虚拟存储器区域。
- */
- 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/jpg");
- else
- strcpy(filetype,"text/plain");
- }
8.Tiny的server_dynamic函数
[cpp] view plain copy
- void serve_dynamic(int fd, char *filename, char *cgiargs)
- {
- char buf[MAXLINE],*emptylist[] = {NULL};
- 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);
- }
- /*
- Tiny通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容。
- setenv("QUERY_STRING",cgiargs,1) :设置QUERY_STRING环境变量。
- dup2 (fd,STDOUT_FILENO) :重定向它的标准输出到已连接描述符。此时,任何写到标准输出的东西都直接写到客户端。
- execve(filename,emptylist,environ) :加载运行cgi程序。
- */
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) 运行前准备
- 将被访问的文件放在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
- 将测试用CGI程序放到cgi-bin目录下,并编译成可执行程序
$ gcc -o adder adder.c
- 1
(4) 运行流程及其结果
- 运行Tiny程序,并指定端口号(1024–49151可用,其它为知名端口)
$ ./tiny 1024
- 1
-
浏览器访问静态内容(home.html)
-
浏览器访问不存在的内容
-
浏览器访问动态内容
-
还可以访问图片哦
(5) Telnet 测试
- 连接到Tiny服务器
$ telnet localhost 1024
- 1
- 输入请求头(注意空行)
GET /home.html HTTP/1.0
- 1
- 2
- 3
- 验证结果(注意空行)
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 可以得到数据,则可以判断为少了一个空行。