# 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&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 <p>The answer is: 15000 + 213 = 15213 Adder: second HTML line in response body
14 <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。为什么呢?