阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Tiny Web服务器代码分析

466次阅读
没有评论

共计 6460 个字符,预计需要花费 17 分钟才能阅读完成。

《深入理解计算机系统》中开发了一个小但是功能齐全的称为 Tiny 的 web 服务器,这里是 Tiny 服务器的源码解析。

1.Tiny 的 main 程序

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

深入理解计算机系统(原书第 2 版) PDF 清晰中文版  http://www.linuxidc.com/Linux/2015-03/114720.htm

让 Tiny 服务器运行起来  http://www.linuxidc.com/Linux/2015-04/116046.htm

本文永久更新链接地址:http://www.linuxidc.com/Linux/2015-04/116050.htm

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-20发表,共计6460字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19351
评论数
4
阅读量
7976414
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
开源MoneyPrinterTurbo 利用AI大模型,一键生成高清短视频!

开源MoneyPrinterTurbo 利用AI大模型,一键生成高清短视频!

  开源 MoneyPrinterTurbo 利用 AI 大模型,一键生成高清短视频! 在短视频内容...
如何免费使用强大的Nano Banana Pro?附赠邪修的用法

如何免费使用强大的Nano Banana Pro?附赠邪修的用法

如何免费使用强大的 Nano Banana Pro?附赠邪修的用法 前言 大家好,我是星哥,今天来介绍谷歌的 ...
国产开源公众号AI知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率

国产开源公众号AI知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率

国产开源公众号 AI 知识库 Agent:突破未认证号限制,一键搞定自动回复,重构运营效率 大家好,我是星哥,...
浏览器自动化工具!开源 AI 浏览器助手让你效率翻倍

浏览器自动化工具!开源 AI 浏览器助手让你效率翻倍

浏览器自动化工具!开源 AI 浏览器助手让你效率翻倍 前言 在 AI 自动化快速发展的当下,浏览器早已不再只是...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
4盘位、4K输出、J3455、遥控,NAS硬件入门性价比之王

4盘位、4K输出、J3455、遥控,NAS硬件入门性价比之王

  4 盘位、4K 输出、J3455、遥控,NAS 硬件入门性价比之王 开篇 在 NAS 市场中,威...
星哥带你玩飞牛NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手!

星哥带你玩飞牛NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手!

星哥带你玩飞牛 NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手! 作为动漫爱好者,你是否还在为...
你的云服务器到底有多强?宝塔跑分告诉你

你的云服务器到底有多强?宝塔跑分告诉你

你的云服务器到底有多强?宝塔跑分告诉你 为什么要用宝塔跑分? 宝塔跑分其实就是对 CPU、内存、磁盘、IO 做...
星哥带你玩飞牛NAS硬件03:五盘位+N5105+双网口的成品NAS值得入手吗

星哥带你玩飞牛NAS硬件03:五盘位+N5105+双网口的成品NAS值得入手吗

星哥带你玩飞牛 NAS 硬件 03:五盘位 +N5105+ 双网口的成品 NAS 值得入手吗 前言 大家好,我...
小白也能看懂:什么是云服务器?腾讯云 vs 阿里云对比

小白也能看懂:什么是云服务器?腾讯云 vs 阿里云对比

小白也能看懂:什么是云服务器?腾讯云 vs 阿里云对比 星哥玩云,带你从小白到上云高手。今天咱们就来聊聊——什...