返回介绍

11.6 综合:TINY Web 服务器

发布于 2024-10-04 13:24:44 字数 28377 浏览 0 评论 0 收藏 0

我们通过开发一个虽小但功能齐全的称为 TINY 的 Web 服务器来结束对网络编程的讨论。TINY 是一个有趣的程序。在短短 250 行代码中,它结合了许多我们已经学习到的思想,例如进程控制、Unix I/O、套接字接口和 HTTP。虽然它缺乏一个实际服务器所具备的功能性、健壮性和安全性,但是它足够用来为实际的 Web 浏览器提供静态和动态的内容。我们鼓励你研究它,并且自己实现它。将一个实际的浏览器指向你自己的服务器,看着它显示一个复杂的带有文本和图片的 Web 页面,真是非常令人兴奋(甚至对我们这些作者来说,也是如此!)。

1. TINY 的 main 程序

图 11-29 展示了 TINY 的主程序。TINY 是一个迭代服务器,监听在命令行中传递来的端口上的连接请求。在通过调用 open_listenfd 函数打开一个监听套接字以后,TINY 执行典型的无限服务器循环,不断地接受连接请求(第 32 行),执行事务(第 36 行),并关闭连接的它那一端(第 37 行)。

/*
* tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
* GET method to serve static and dynamic content
*/
#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 *shortmsg, char *longmsg);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    /* Check command-line args */
    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
                    port, MAXLINE, 0);
        printf("Accepted connection from (%s, %s)\n", hostname, port);
        doit(connfd);
        Close(connfd);
    }
}

图 11-29 TINY Web 服务器

2. doit 函数

图 11-30 中的 doit 函数处理一个 HTTP 事务。

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);
    printf("Request headers:\n");
    printf("%s", buf);
    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);

    /* Parse 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) { /* Serve 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 { /* Serve 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);
    }
}

图 11-30 TINY doit 处理一个 HTTP 事务

首先,我们读和解析请求行(第 11 ~ 14 行)。注意,我们使用图 11-8 中的 rio_readlineb 函数读取请求行。

TINY 只支持 GET 方法。如果客户端请求其他方法(比如 POST),我们发送给它一个错误信息,并返回到主程序(第 15 ~ 19 行),主程序随后关闭连接并等待下一个连接请求。否则,我们读并且(像我们将要看到的那样)忽略任何请求报头(第 20 行)。

然后,我们将 URI 解析为一个文件名和一个可能为空的 CGI 参数字符串,并且设置一个标志,表明请求的是静态内容还是动态内容(第 23 行)。如果文件在磁盘上不存在,我们立即发送一个错误信息给客户端并返回。

最后,如果请求的是静态内容,我们就验证该文件是一个普通文件,而我们是有读权限的(第 31 行)。如果是这样,我们就向客户端提供静态内容(第 36 行)。相似地,如果请求的是动态内容,我们就验证该文件是可执行文件(第 39 行),如果是这样,我们就继续,并且提供动态内容(第 44 行)。

3. clienterror 函数

TINY 缺乏一个实际服务器的许多错误处理特性。然而,它会检査一些明显的错误,并把它们报告给客户端。图 11-31 中的 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));
}

图 11-31 TINY clienterror 向客户端发送一个出错消息

回想一下,HTML 响应应该指明主体中内容的大小和类型。因此,我们选择创建 HTML 内容为一个字符串,这样一来我们可以简单地确定它的大小。还有,请注意我们为所有的输出使用的都是图 10-4 中健壮的 rio_writen 函数。

4. read_requesthdrs 函数

TINY 不使用请求报头中的任何信息。它仅仅调用图 11-32 中的 read_requesthdrs 函数来读取并忽略这些报头。注意,终止请求报头的空文本行是由回车和换行符对组成的,我们在第 6 行中检査它。

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

图 11-32 TINY read_requesthdrs 读取并忽略请求报头

5. parse_uri 函数

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

图 11-33 中的 parse_uri 函数实现了这些策略。它将 URI 解析为一个文件名和一个可选的 CGI 参数字符串。如果请求的是静态内容(第 5 行),我们将清除 CGI 参数字符串(第 6 行),然后将 URI 转换为一个 Linux 相对路径名,例如 ./index.html(第 7 ~ 8 行)。如果 URI 是用结尾的(第 9 行),我们将把默认的文件名加在后面(第 10 行)。另一方面,如果请求的是动态内容(第 13 行),我们就会抽取出所有的 CGI 参数(第 14 ~ 20 行),并将 URI 剩下的部分转换为一个 Linux 相对文件名(第 21 ~ 22 行)。

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(filename, "home.html");
        return 1;
    }
    else { /* Dynamic content */
        ptr = index(uri, ’ ? ’);
        if (ptr) {
            strcpy(cgiargs, ptr + 1);
            *ptr = ’\0’;
        }
        else
            strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}

图 11-33 TINY parse_uri 解析一个 HTTP URI

6. serve_static 函数

TINY 提供五种常见类型的静态内容:HTML 文件、无格式的文本文件,以及编码为 GIF、PNG 和 JPG 格式的图片。

图 11-34 中的 serve_static 函数发送一个 HTTP 响应,其主体包含一个本地文件的内容。

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, "%sConnection: close\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));
    printf("Response headers:\n");
    printf("%s", 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);
}

/*
* get_filetype - Derive file type from filename
*/
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, ".png"))
        strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    else
        strcpy(filetype, "text/plain");
}

图 11-34 TINY serve_static 为客户端提供静态内容

首先,我们通过检査文件名的后缀来判断文件类型(第 7 行),并且发送响应行和响应报头给客户端(第 8 ~ 13 行)。注意用一个空行终止报头。

接着,我们将被请求文件的内容复制到已连接描述符 fd 来发送响应主体。这里的代码是比较微妙的,需要仔细研究。第 18 行以读方式打开 filename,并获得它的描述符。在第 19 行,Linux mmap 函数将被请求文件映射到一个虚拟内存空间。回想我们在第 9.8 节中对 remap 的讨论,调用 mmap 将文件 srcfd 的前 filesize 个字节映射到一个从地址 srcp 开始的私有只读虚拟内存区域。

一旦将文件映射到内存,就不再需要它的描述符了,所以我们关闭这个文件(第 20 行)。执行这项任务失败将导致潜在的致命的内存泄漏。第 21 行执行的是到客户端的实际文件传送。rio_writen 函数复制从 srcp 位置开始的 filesize 个字节(它们当然已经被映射到了所请求的文件)到客户端的已连接描述符。最后,第 22 行释放了映射的虚拟内存区域。这对于避免潜在的致命的内存泄漏是很重要的。

7. serve_dynamic 函数

TINY 通过派生一个子进程并在子进程的上下文中运行一个 CGI 程序,来提供各种类型的动态内容。

图 11-35 中的 serve_dynamic 函数一开始就向客户端发送一个表明成功的响应行,同时还包括带有信息的 Server 报头。CGI 程序负责发送响应的剩余部分。注意,这并不像我们可能希望的那样健壮,因为它没有考虑到 CGI 程序会遇到某些错误的可能性。

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 */
        /* Real server would set all CGI vars here */
        setenv("QUERY_STRING", cgiargs, 1);
        Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */
        Execve(filename, emptylist, environ); /* Run CGI program */
    }
    Wait(NULL); /* Parent waits for and reaps child */
}

图 11-35 TINY serve_dynamic 为客户端提供动态内容

在发送了响应的第一部分后,我们会派生一个新的子进程(第 11 行)。子进程用来自请求 URI 的 CGI 参数初始化 QUERY_STRING 环境变量(第 13 行)。注意,一个真正的服务器还会在此处设置其他的 CGI 环境变量。为了简短,我们省略了这一步。

接下来,子进程重定向它的标准输出到已连接文件描述符(第 14 行),然后加载并运行 CGI 程序(第 15 行)。因为 CGI 程序运行在子进程的上下文中,它能够访问所有在调用 ex¬ecve 函数之前就存在的打开文件和环境变量。因此,CGI 程序写到标准输出上的任何东西都将直接送到客户端进程,不会受到任何来自父进程的干涉。其间,父进程阻塞在对 wait 的调用中,等待当子进程终止的时候,回收操作系统分配给子进程的资源(第 17 行)。

旁注 - 处理过早关闭的连接

尽管一个 Web 服务器的基本功能非常简单,但是我们不想给你一个假象,以为编写一个实际的 Web 服务器是非常简单的。构造一个长时间运行而不崩溃的健壮的 Web 服务器是一件困难的任务,比起在这里我们已经学习了的内容,它要求对 Linux 系统编程有更加深入的理解。例如,如果一个服务器写一个已经被客户端关闭了的连接(比如,因为你在浏览器上单击了 “Stop” 按钮),那么第一次这样的写会正常返回,但是第二次写就会引起发送 SIGPIPE 信号,这个信号的默认行为就是终止这个进程。如果捕获或者忽略 SIGPIPE 信号,那么第二次写操作会返回值 -1,并将 errno 设置为 EPIPE。strerr 和 perror 函数将 EPIPE 错误报告为 “Broken pipe”,这是一个迷惑了很多人的不太直观的信息。总的来说,一个健壮的服务器必须捕获这些 SIGPIPE 信号,并且检查 write 函数调用是否有 EPIPE 错误。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文