Web 服务器和动态内容

作者:Dan Teodor

当 Web 服务器首次出现时,它们的主要目的是从运行它们的机器上提供选定的信息。最初的想法是简单地获取文件的内容,并通过 TCP 连接以 HTTP 格式传输它们。早期发现的固有局限性是无法传递动态内容,因此定义了 CGI 接口并将其添加到 Web 服务器以解决这个问题。

通用网关接口 (CGI) 提供了一种方法,让 Web 服务器执行一个进程,该进程的输出由其执行的代码决定,然后将此输出传递回客户端浏览器,就像它是静态文件的内容一样。从那时起,许多结合了脚本引擎和 CGI 的变体不断演变,简化了程序员的工作,并使多个外部线程的执行更加高效(Perl、Python、PHP 等)。然而,在大多数情况下,这些脚本语言都存在需要解释的缺点。此外,它们忽略了一个事实,即在 C 和 Fortran 等语言中已经存在大量代码(还记得 Fortran 吗?)来解决复杂的、计算密集型应用程序。虽然基于查询结果构建动态内容非常有用,但使用这些脚本语言应用图像数据转换或计算傅里叶变换是不现实的,因为完成此类计算所需的时间相对于用户期望的响应时间而言太长了。

因此,由于现有的代码库以及必须由某些无法在脚本语言中有效实现的算法生成的动态内容的存在,因此绝对需要开发使用 CGI 的程序,这些程序使用 C 或 Fortran 等传统语言编写并编译为本机代码。

Web 服务器如何将数据传递给您的程序

HTTP 协议中将数据从浏览器传递到 Web 服务器的方式有两种:GET 数据(指定为 URL 的一部分,通常是 URL 中“?”之后的部分)和 POST 数据(正在被 Web 浏览器提交的表单中所有字段的收集的名称-值对)。要找出 GET 数据,只需查看 URL—http://www.mydomain.com/pages/external.cgi?additional-data。

GET data portion: additional-data

要找出 POST 数据,请查看生成请求的文档的源代码,并找到正在提交的表单

<FORM>
<INPUT TYPE="text"NAME="fld01"
VALUE="val01">
<INPUT TYPE="hidden"NAME="fld02"
VALUE="val02">
<INPUT TYPE="checkbox"NAME="fld03" CHECKED>
<SELECT NAME="fld04">
<OPTION VALUE="val03"> Val-03
<OPTION SELECTED VALUE="val04"> Val-04
<OPTION VALUE="val05"> Val-05
</SELECT>
</FORM>
                  POST data:
   fld01=val01&fld02=val02&fld03=on&fld04=val04
如您所见,POST 数据以连续字符串的形式提交,每个字段/值对用等号(“=”)分隔。每个字段/值实体用与号(“&”)分隔。

实际上,在传递 POST 数据的格式中还涉及其他复杂性。许多字符需要“转义”,以避免将 Web 服务器与控制字符或分隔符混淆。这通过插入加号(“+”)代替空格,以及格式为“%[0-9,A-F][0-9,A-F]”的转义序列来代替不可打印字符、与号、加号和等号来解决。(对于纯粹主义者来说,是的,空格可以表示为“+”符号或转义序列“%20”。我使用过的所有版本的 Apache 和 IIS 都接受这两种方式。)

Web 浏览器通过不同的机制将 GET 和 POST 数据传递给外部线程。GET 数据放置在对该线程的本地上下文可见的环境变量中。此环境变量是“QUERY_STRING”。因此,在 C/C++ 中访问 GET 数据很简单,只需使用以下命令

char *pszGetData = getenv("QUERY_STRING");

(这应该在所有 UNIX 和所有 Microsoft 开发环境中都有效。)

另一方面,POST 数据在标准输入流中传递到外部线程。对于那些不熟悉流的人来说,它与键盘是相同的数据生产者,因此无论您一直在做什么来从键盘读取输入,那都是您将用来访问 POST 数据的机制。但是,流的一个固有的缺点是您无法知道有多少数据在等待您。显而易见的解决方案是继续从该流中逐字节读取,直到没有更多数据为止,并相应地调整动态分配的缓冲区的大小。但是,Web 浏览器为开发人员提供了另一条信息,这省去了他们不得不增长缓冲区、产生额外的开销以及处理当多个动态内存分配之一决定失败时发生的异常的麻烦。

当 Web 浏览器将 POST 数据传递到外部线程的标准输入时,它会将所有 POST 数据一次性放在那里;因此,在您首次在该流上遇到数据部分后,永远不会有任何机会将其他数据添加到该流中。

Web 浏览器还通过在对该线程的本地上下文可见的环境变量中写入(作为 ASCII 文本)等待从标准输入读取的字节数来告诉进程它在标准输入中放置了多少数据。此环境变量是“CONTENT_LENGTH”。因此,在 C/C++ 中访问 POST 数据需要一个三步过程,该过程在所有 UNIX 和 Microsoft 开发环境中都有效

long   iContentLength =
atol(getenv("CONTENT_LENGTH"));
char  *szFormData = (char *) malloc(iContentLength * sizeof(char));
bzero(szFormData, iContentLength * sizeof(char));
fread(szFormData, (iContentLength - 1) * sizeof(char), 1, stdin);

给定的浏览器文档可以同时包含 GET 和 POST 数据;因此,可以同时使用这两种机制。在 <FORM> 标记中,可以指定包含 GET 数据的目标,并且表单中包含的数据将作为 POST 数据传递给目标。

您的程序如何将数据传递回 Web 服务器

获得来自网页的数据后,您的程序现在可以执行其所有处理,并可以告诉 Web 服务器要回复什么。该回复可以是简单的纯文本消息、HTML 文档(最常见的形式)、图像(通常为 GIF 或 JPEG 格式)或任何其他复杂数据类型。这些数据类型称为 MIME 类型,并且在当今使用的几乎所有浏览器上都认可一个标准子集。您的程序将数据传递回 Web 服务器(以传输到客户端浏览器)的机制是通过将该数据写入线程的标准输出流,这与您在您喜欢的语言中用于将字符写入屏幕的机制相同。此数据的格式很简单

Content-type:[SPC][MIME-type];[CR][CR][Document-Data]

首先,您的程序必须声明 MIME 类型。最常见的 MIME 类型如下:

  • text/plain—纯文本,以块字符输出,并具有与传输时完全相同的对齐方式(无自动换行)。

  • text/html—标准 HTML 文档文本。

  • image/gif—使用 Compuserve GIF 图像规范之一编码的图像(应注意,该格式使用 Lempel-Ziv 压缩技术,该技术可能不在公共领域,并且可能要求软件生产者或软件用户从专利所有者 Unisys 处获得软件许可)。

  • image/jpeg—使用 JPEG 图像标准编码的图像。

其次,您的程序必须发送一个冒号(“:”),后跟两个回车符(“\n”)。

第三,您的程序必须准备您希望传输的文档的主体。它可以是纯文本或 HTML 文档的内容,也可以是构成 GIF 或 JPEG 图像原始数据块的二进制数据。

因此,让 Web 服务器发送一个简单的回复可以像这样简单:

printf("Content-type: text/html\n\n<HTML><HEAD>
   </HEAD>"
"<BODY><H3>My Quick Test Page</H3></BODY></HTML>\n");

就是这样;这些是告诉 Web 浏览器回复客户端请求的基本知识。当然,可以在此基本格式中添加一些巧妙的东西,以在一定程度上控制文档的呈现方式。一个示例是在 MIME 类型之后(在回车符之前)添加“charset=”限定符,这确保浏览器将使用适当的字符集呈现正在传输的 HTML 文档(示例为“ISO-;9660-;1”、“ISO-;9660-;2”、“KOI-;8”、“WIN-;1225”等)。因此,精明的程序员可能希望像这样发送文档:

         printf("Content-type: text/html;
     charset=KOI-;8\n\n"
                "<HTML><HEAD></HEAD><BODY><H3>"
                "<BODY><H3>Maya malinkayaproba
      </H3></BODY></HTML>\n");
向浏览器推送持续更新

很多时候,网页的目的是监视一些漫长而复杂的过程,这些过程通常需要比一个超时周期更长的时间才能完成,或者生成完整的更新。这是另一种可以在 C/C++ 和 Fortran 等传统语言中很好地处理的情况。其想法是强制 Web 服务器保持与浏览器的 TCP 管道打开,并以您的程序指定的间隔不断地将新文档推送到浏览器。

此处给出的实现此目的的公式特定于 Apache Web 服务器,众所周知,它是 Linux 世界迄今为止最流行的 HTTP 守护进程。如果您不确定这是否适用于您的特定 HTTP 守护进程,请尝试一下并告诉我。以下是步骤:

  1. 将您的程序的二进制文件重命名为以字符“nph-”开头。这意味着如果您的程序的二进制文件名为“update.cgi”,则将其名称更改为“nph-update.cgi”。

  2. 传输 Web 服务器通常传递给 Web 浏览器的 HTTP 标头(这样做的原因将在下面解释)

printf("HTTP/1.0 200 Okay\n");
  1. 将文档的 MIME 类型定义为“multipart/x-mixed-replace”

printf("Content-;Type:multipart/x-;mixed-;replace;"
        "boundary=SoMeRaNdOmTeXt\n");
  1. 通过传递在“boundary”中声明的令牌来启动第一个文档传输

printf("\n—SoMeRaNdOmTeXt\n");
  1. 发送下一个文档更新。这只是一个文档,它应该显示,直到后续传输在未来的某个时间点沿同一打开的连接发出。更新之后是另一个在“boundary”中声明的令牌实例

printf("Content-type: text/html\n\n<HTML><HEAD>
    </HEAD>"
"<BODY><H3>Update #%d</H3></BODY></HTML>\n"
"\n-SoMeRaNdOmTeXt\n", Count++);
  1. 刷新标准输出缓冲区

fflush(stdout);
  1. 重复步骤五和六,直到所有更新都已传输完毕。在最后一次更新时,不要传输令牌,只需刷新标准输出并退出。这将使最后一次更新在您的程序退出后留在客户端浏览器的窗口中。

清单 1 中显示了一个简单的程序示例,该程序使用服务器端推送在您的浏览器屏幕上从一数到十,计数更新之间延迟一秒。

清单 1. 数到十

为了解释这是如何工作的,有必要稍微了解一下服务器在后台执行的操作。到目前为止,您的程序的输出已验证其有效性(即,正确的 MIME 类型、正确的分隔符等),并已连同一些附加的 HTTP 标头一起传递给客户端浏览器。为了更好地控制 Web 服务器/客户端浏览器交互,我们必须要求 Web 服务器停止执行这些有效性检查,并停止添加其正常的标头。这就是“nph”的含义——您的程序的新文件名“No Parsed Headers”(未解析标头)。当您的程序的名称以字母“nph-”开头时,这意味着 Web 服务器现在假定您的程序负责执行所有通常由 Web 服务器负责的验证检查和标头传输。Web 服务器将简单地保持与客户端浏览器的 TCP 管道打开,并从您的程序的标准输出流中抓取数据,并将其推送到该 TCP 管道中到浏览器。我们现在可以理解步骤二中发生的事情;这是一个必需的标头,通常由 Web 服务器传输,并且对于隐藏在 CGI 后面的程序来说是完全透明的。

接下来,我们必须告诉客户端浏览器期望持续更新,而不仅仅是一次数据突发……因此,一旦第一个文档被传输,它就不能关闭 TCP 管道。这是通过将文档的 MIME 类型指定为“multipart/x-mixed-replace”来完成的。此外,我们需要告诉浏览器如何区分即将传输的多个文档流中的文档。这是通过将限定符“boundary=SoMeRaNdOmTeXt”附加到 MIME 类型声明来完成的。这告诉 Web 浏览器,当它在输入流中遇到字节序列“--SoMeRaNdOmTeXt”时,它应该停止并假定以下数据将描述一个新文档,该文档将替换当前文档窗口中存在的文档。

分隔一个文档传输的结尾和下一个文档传输的开始的字符串通常称为边界令牌,并且此令牌通常比我们在此示例中显示的令牌复杂得多。通常,它是一个由随机函数生成的 50 或 60 字节长的字母数字字符串,将在本文后面介绍。该字符串应足够长,并且其内容应足够随机,以最大程度地减少它意外地作为文档正文的一部分出现的可能性。

最后,一旦文档已推送到标准输出,并且边界令牌也已推送出去,就必须刷新输出缓冲区,以确保数据被发送到客户端浏览器。如果未完成此操作,则在您的操作系统使用的流的缓冲区实现溢出并且操作系统触发刷新之前,数据将不会被发送。

Web 服务器输入的防弹和解析

使用这些传统语言编写 Web 程序的最大障碍,也可能是 Perl 和 PHP 开发背后的最大驱动力,一直是开发应用程序的难度和安全风险,这些应用程序需要具备智能和专业知识,以便在仅使用环境变量和标准输入流从 Web 浏览器向它们传递数据时进行解析并避免黑客攻击。

必须解决的第一个棘手问题是一种简单且内存高效的数据解析方法,以便人们可以简单地选择他们正在查找的字段,并以一次性、一击必杀的方式获取数据。此外,还需要解决某些安全问题,例如来自行为不端的客户端浏览器的缓冲区溢出,这些溢出旨在用溢出数据覆盖应用程序内存(或拒绝服务)。

我在此处为您提供一系列函数,这些函数提供了一种安全可靠的一次性、一击必杀的方法,用于在这些传统语言中获取 POST 数据。我提供的具体示例是 C 语言,但可以轻松移植到 Fortran 或为 C++ 封装

char  *TextField    = GetFormStringValue("TextField");
int    NumericField = GetFormIntegerValue("IntegerField");
float  FloatField   = GetFormFloatValue("FloatField");

这些函数的源代码显示在清单 2 中,其支持函数的源代码显示在清单 3 中。[由于清单 2 和 3 的长度,它们可从我们的 ftp 站点 ftp://ftp.linuxjournal.com/pub/lj/listings/issue82 获取。] 所有这些函数都经过测试,在 UNIX 和 Windows 开发环境中都能同样出色地工作,并且都能补偿缓冲区溢出和下溢。当首次调用这些函数中的任何一个时,将在后台执行动态内存分配以捕获和解析 POST 数据。然后,其解析后的形式将保存在内存中,并且在后续调用这些函数中的任何一个时,将执行对此内存空间中字段的简单线性扫描。内存分配仅执行一次,并且转义序列和特殊字符的所有转换都在此内存空间内线性执行(没有使用其他临时空间来完成此操作)。

由于此处显示的示例是简单的 C 语言,它无法像 C++ 那样提供自动析构函数,因此在您的程序退出时必须调用一个清理函数:ReleaseFormData()

这是释放动态分配的内存缓冲区所必需的。如果这些函数被移植到 C++ 类,则只需在 POST 数据访问功能移植到的类的析构函数方法中调用此函数即可。因此,清单 4 中显示了您的传统语言 CGI 程序的简单框架。

清单 4. 传统语言 CGI 框架

未来主题

当然,我们仅仅触及了冰山一角,当您释放 C/C++ 等快速高效的语言的力量来开发 Web 应用程序时,而无需承担执行通常由脚本解释器执行的所有繁琐工作的额外负担,就可以实现的功能。我们很容易理解为什么我们需要扩展此讨论以包括以下内容:

  • 使用本地文件系统为您的 CGI 程序维护“状态”。

  • 为什么可以在 Linux 的本地文件系统上维护状态,而无需像在其他操作系统上可能存在的那样担心磁盘开销。

  • 从您的 CGI 程序在客户端浏览器上创建、修改和销毁 Cookie。

  • 设置安全性,以便只有您和 CGI 程序可以访问本地文件系统中的文件中的状态信息,而其他人无法访问。

  • 展望轻量级线程和 FastCGI。

Web Servers and Dynamic Content
Dan Teodor 在德克萨斯州休斯顿的普华永道 (PriceWaterhouseCoopers) 从事管理咨询工作谋生。自从研究生时代和 Slackware 0.x 内核发布以来,他一直是一位隐藏的 Linux 极客。虽然热衷于大规模 Web 应用程序部署和时髦的商业架构,但他梦想着在新墨西哥州滑雪,并且每年都会启动一家新的破产互联网公司。
加载 Disqus 评论