zoukankan      html  css  js  c++  java
  • tiny web服务器源码分析

    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>
    ", 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, "
    ")) {          //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 = '';
    	}
    	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"">
    ", body);
        sprintf(body, "%s%s: %s
    ", body, errnum, shortmsg);
        sprintf(body, "%s<p>%s: %s
    ", body, longmsg, cause);
        sprintf(body, "%s<hr><em>The Tiny Web server</em>
    ", body);
    
        /* Print the HTTP response */
        sprintf(buf, "HTTP/1.0 %s %s
    ", errnum, shortmsg);
        Rio_writen(fd, buf, strlen(buf));
        sprintf(buf, "Content-type: text/html
    ");
        Rio_writen(fd, buf, strlen(buf));
        sprintf(buf, "Content-length: %d
    
    ", (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
    ");    //line:netp:servestatic:beginserve
        sprintf(buf, "%sServer: Tiny Web Server
    ", buf);
        sprintf(buf, "%sContent-length: %d
    ", buf, filesize);
        sprintf(buf, "%sContent-type: %s
    
    ", 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
    "); 
        Rio_writen(fd, buf, strlen(buf));
        sprintf(buf, "Server: Tiny Web Server
    ");
        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

  • 相关阅读:
    Codeforces Round #257 (Div. 2) E题:Jzzhu and Apples 模拟
    【浅墨Unity3D Shader编程】之二 雪山飞狐篇:Unity的基本Shader框架写法&amp;颜色、光照与材质
    poj 1741 楼教主男人八题之中的一个:树分治
    Localhost 回环IP 127.0.0.1
    网络营销着陆页:怎么让游客成顾客?
    窗体的消息处理
    运行Java -jar somefile.jar时发生了什么(二)
    Why is processing a sorted array faster than an unsorted array(Stackoverflow)
    NYOJ 330 一个简单的数学题【数学题】
    Java 实现的断点下载
  • 原文地址:https://www.cnblogs.com/battzion/p/4250629.html
Copyright © 2011-2022 走看看