tiny web服务器源码分析

时间:2022-09-02 15:06:19

tiny web服务器源码分析

正如csapp书中所记,在短短250行代码中,它结合了许多我们已经学习到的思想,如进程控制,unix I/O,套接字接口和HTTP。虽然它缺乏一个实际服务器所具备的功能性,健壮性和安全性,但是它足够用来为实际的web浏览器提供静态和动态的内容。我们鼓励你研究它,并且自己实现它,将一个实际的浏览器指向你自己的服务器,看着它显示一个复杂的带有文本和图片的web页面,真是非常令人兴奋。接下来就看我们能从这之中发掘出什么来。

头文件及声明

#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>
/* 默认的文件访问权限为 DEF_MODE & ~DEF_UMASK */
#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);
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);
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 *errum,char *shorting,char *longmsg);

主函数

int main(int argc, char **argv)
{
int listenfd,connfd, port, clientlen;
struct sockaddr_in clientaddr; /* Check command line args */
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); //line:netp:tiny:accept
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}
}

TINY是一个迭代服务器,监听在命令行中传递来的端口上的连接请求。再通过调用open_listenfd()函数打开一个监听套接字以后,tiny执行典型的无限服务器循环,不断的接受连接请求,执行事务,并关闭连接它的那一端。

其中:

Open_listenfd(int port),是将socket,bind,listen函数结合的一个函数,

这也看作是是一个服务器初始化的过程,其主要步骤如下:

1.listenfd = socket(AF_INET,SOCK_STREAM,0),创建一个套接字。

2.setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(const void *)&optval,sizeof(int)).设置套接字的属性使它能够在计算机重启的时候可以再次使用套接字的端口和IP

3.bind(listenfd,(SA *)&serveraddr,sizeof(serveraddr)),将监听套接字与服务器套接字地址联系起来。

4.listen(listenfd,LISTENQ),将listenfd套接字从主动套接字转化为监听套接字。

doit函数

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
}
}

doit函数用来处理一个HTTP请求,读取请求后,首先tiny只支持get方法,如果客户端以其他方法请求,则返回错误,然后,解析uri,解析为文件路径和一个CGI参数字符串,然后再按请求为动态内容或静态内容分别处理。

细节:

用RIO包健壮的读写,因为打开的文件类型为网络套接字,那么内部缓冲约束和较长的网络延迟会造成read和write返回不足值。而RIO会处理这样的不足值。

 Rio_readinitb(&rio, fd);   //将文件描述符和内部缓冲区相联系。
Rio_readlineb(&rio, buf, MAXLINE); //从内部缓存区读出一个文本行至buf中,以null字符来结束这个文本行。当然,每行最大的字符数量不能超过MAXLINE。

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地址很快将用尽。

read_requsethdrs函数

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;
}

Tiny不使用请求报头中的任何信息,仅仅调用 read_requesthdrs函数来读取并忽略这些报头。

parse_uri函数

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;
}
}

uri解析函数,tiny默认静态内容的主目录就是它的当前目录,而可执行文件的主目录./cgi_bin.任何包含字符串cgi-bin的url都会被认为表示为对动态内容的请求。默认的静态文件名为 ./home.html.

clienterror函数

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));
}

clienterror,是向客户端发送一个HTTP响应,在响应行中包含相应的状态码和状态消息,响应主体中包含一个HTML文件,向浏览器的用户解释这个错误。

细节

HTTP响应

一个HTTP响应:一个响应行(response line) 后面跟随0个或多个响应报头(response header),再跟随一个空的文本行来终止报头,最后跟随一个响应主体(response body)

响应行:<version> <status code> <status message>

status code 是一个三位的正整数

serve_static函数

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
}
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");
}

serve_static函数发送一个HTTP响应,其主体包括一个本地文件内容。首先,我们通过检查文件名的后缀来判断文件类型,并且发送一个响应行和响应报头给客户端,注意:用一个空行来终止报头。

serve_dynamic函数

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
}

Tiny通过派生一个子进程并在子进程的上下文中运行一个cgi程序(可执行文件),来提供各种类型的动态内容。

setenv("QUERY_STRING",cgiargs,1) :设置QUERY_STRING环境变量。

dup2(fd,STDOUT_FILENO):重定向它的标准输出到已连接描述符。此时,任何写到标准输出的东西都直接写到客户端。

execve(filename,emptylist,environ) :加载运行cgi程序。

源码来源:

http://csapp.cs.cmu.edu/public/ics2/code/netp/tiny/tiny.c

配置

http://blog.sina.com.cn/s/blog_3e250da301019xne.html