# 11.5.4 服务动态内容

如果我们停下来考虑一下,一个服务器是如何向客户端提供动态内容的,就会发现一些问题。例如,客户端如何将程序参数传递给服务器?服务器如何将这些参数传递给它所创建的子进程?服务器如何将子进程生成内容所需要的其他信息传递给子进程?子进程将它的输出发送到哪里?一个称为 CGI(Common Gateway Interface,通用网关接口)的事实标准的出现解决了这些问题。

# 1. 客户端如何将程序参数传递给服务器

GET 请求的参数在 URI 中传递。正如我们看到的,一个“?”字符分隔了文件名和参数,而每个参数都用一个“&”字符分隔开。参数中不允许有空格,而必须用字符串“%20”来表示。对其他特殊字符,也存在着相似的编码。

旁注 在 HTTP POST 请求中传递参数

HTTP POST 请求的参数是在请求主体中而不是 URI 中传递的。

# 2. 服务器如何将参数传递给子进程

在服务器接收一个如下的请求后

GET /cgi-bin/adder?15000&213 HTTP/1.1

它调用 fork 来创建一个子进程,并调用 execve 在子进程的上下文中执行 /cgi-bin/adder 程序。像 adder 这样的程序,常常被称为 CGI 程序,因为它们遵守 CGI 标准的规则。而且,因为许多 CGI 程序是用 Perl 脚本编写的,所以 CGI 程序也常被称为 CGI 脚本。在调用 execve 之前,子进程将 CGI 环境变量 QUERY_STRING 设置为“15000&213”,adder 程序在运行时可以用 Linux getenv 函数来引用它。

# 3. 服务器如何将其他信息传递给子进程

CGI 定义了大量的其他环境变量,一个服务器在它运行一个 CGI 程序时可以设置这些环境变量。图 11-26 给出了其中的一部分。

环境变量 描述
QUERY_STRING 程序参数
SERVER_PORT 父进程侦听的端口
REQUEST_METHOD GET 或 POST
REMOTE_HOST 客户端的域名
REMOTE_ADDR 客户端的点分十进制 IP 地址
CONTENT_TYPE 只对 POST 而言:请求体的 MIME 类型
CONTENT_LENGTH 只对 POST 而言:请求体的字节大小

图 11-26 CGI 环境变量示例

# 4. 子进程将它的输出发送到哪里

一个 CGI 程序将它的动态内容发送到标准输出。在子进程加载并运行 CGI 程序之前,它使用 Linux dup2 函数将标准输出重定向到和客户端相关联的已连接描述符。因此,任何 CGI 程序写到标准输出的东西都会直接到达客户端。

注意,因为父进程不知道子进程生成的内容的类型或大小,所以子进程就要负责生成 Content-type 和 Content-length 响应报头,以及终止报头的空行。

图 11-27 展示了一个简单的 CGI 程序,它对两个参数求和,并返回带结果的 HTML 文件给客户端。图 11-28 展示了一个 HTTP 事务,它根据 adder 程序提供动态内容。

code/netp/tiny/cgi-bin/adder.c

#include "csapp.h"

int main(void) {
    char *buf, *p;
    char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
    int n1=0, n2=0;

    /* Extract the two arguments */
    if ((buf = getenv("QUERY_STRING")) != NULL) {
        p = strchr(buf,'&');
        *p = '\0';
        strcpy(arg1, buf);
        strcpy(arg2, p+1);
        n1 = atoi(arg1);
        n2 = atoi(arg2);
    }

    /* Make the response body */
    sprintf(content, "QUERY_STRING=%s", buf);
    sprintf(content, "Welcome to add.com: ");
    sprintf(content, "%sTHE Internet addition portal.\r\n<p>", content);
    sprintf(content, "%sThe answer is: %d + %d = %d\r\n<p>",
            content, n1, n2, n1 + n2);
    sprintf(content, "%sThanks for visiting!\r\n", content);

    /* Generate the HTTP response */
    printf("Connection: close\r\n");
    printf("Content-length: %d\r\n", (int)strlen(content));
    printf("Content-type: text/html\r\n\r\n");
    printf("%s", content);
    fflush(stdout);

    exit(0);
}

图 11-27 对两个整数求和的 CGI 程序

 1  linux> telnet kittyhawk.cmcl.cs.cmu.edu 8000   Client: open connection
 2  Trying 128.2.194.242...
 3  Connected to kittyhawk.cmcl.cs.cmu.edu.
 4  Escape character is '^]'.
 5  GET /cgi-bin/adder?15000&amp;213 HTTP/1.0           Client: request line
 6                                                       Client: empty line terminates headers
 7  HTTP/1.0 200 OK                                  Server: response line
 8  Server: Tiny Web Server                          Server: identify server
 9  Content-length: 115                              Adder: expect 115 bytes in response body
10  Content-type: text/html                          Adder: expect HTML in the response body
11                                                       Adder: empty line terminates headers
12  Welcome to add.com: THE Internet addition portal. Adder: first HTML line
13  &lt;p>The answer is: 15000 + 213 = 15213            Adder: second HTML line in response body
14  &lt;p>Thanks for visiting!                          Adder: third HTML line in response body
15  Connection closed by foreign host.               Server: closes connection
16  linux>                                           Client: closes connection and terminates

图 11-28 一个提供动态 HTML 内容的 HTTP 事务

旁注 将 HTTP POST 请求中的参数传递给 CGI 程序

对于 POST 请求,子进程也需要重定向标准输入到已连接描述符。然后,CGI 程序会从标准输入中读取请求主体中的参数。

练习题 11.5 在 10.11 节中,我们警告过你关于在网络应用中使用 C 标准 I/O 函数的危险。然而,图 11-27 中的 CGI 程序却能没有任何问题地使用标准 I/O。为什么呢?