Perl 中的互联网服务器
在我之前发表于《Linux Journal》第 35 期的文章中,我撰写了关于 Perl 中套接字库函数的内容,重点是编写互联网客户端程序。Perl 也是一种用于互联网服务器的优秀语言,不仅因为它具有套接字功能以及易于处理文件和数据,而且因为它还具有一种用于提高安全性的特殊模式。在本文中,我将介绍编写 Perl 服务器的几个方面,包括如何使用基本套接字函数、如何最好地处理多个连接、异步通信和安全问题。在此过程中,我们将开发一个类似于 fingerd 的简单互联网服务器,它通过 Web 工作。
套接字通信可以是面向连接的或无连接的。面向连接的协议,如互联网的传输控制协议 (TCP),在交换任何数据之前,会在客户端和服务器之间建立链接。无连接协议,如用户数据报协议 (UDP),只是简单地读取或写入数据,每次都指定客户端或服务器地址。大多数服务器使用面向连接的方案,我们在示例服务器中使用了这种方法(参见列表 1)。但是,我在下面讨论无连接方法。
任何互联网服务器,从最简单的到最复杂的,首先都使用 socket 和 bind 这两个函数来建立可识别的通信端点。服务器使用 socket 创建具有所需类型和协议的套接字。回想一下此函数的语法是
socket SOCKET, DOMAIN, TYPE, PROTOCOL
这里的 SOCKET 是一个 Perl 文件句柄,由对 socket 的调用初始化。对于互联网 TCP 应用程序,DOMAIN 是 AF_INET,TYPE 是 SOCK_STREAM。Perl 5 Socket 包定义了常量 AF_INET 和 SOCK_STREAM 以及其他与套接字相关的常量和函数;有关详细信息,请参阅之前的文章。该
互联网服务器必须使用 bind 函数将网络地址绑定到套接字。客户端可以绑定地址,但在面向连接的客户端中通常没有必要。这也称为“命名套接字”。此过程指定客户端必须连接到的网络地址,才能开始与服务器通信。bind 的语法是
bind SOCKET, NAME
SOCKET 参数仍然是由调用 socket 创建的文件句柄。NAME 是绑定到套接字的地址。此参数的内容可能非常复杂(同样,有关详细信息,请参阅之前的文章)。对于 Perl 5.2 及更高版本,Socket 包中的一个名为 sockaddr_in 的函数返回 NAME 参数的值,给定端口号和互联网主机地址。如果您正在编写类似于 ftp 或 HTTP 服务器之类的程序,则可以使用保留的“众所周知的”端口号(请参阅文件 /etc/services 获取这些数字)。否则,任何正的 16 位整数都足够了,只要它不是保留数字之一。对于服务器,特殊参数 INADDR_ANY 可用于互联网地址,这让内核为套接字选择一个地址。
对于像我们的示例程序这样的面向连接的服务器,我们现在可以使用 listen 函数来告知操作系统我们将接受套接字上的连接。此函数看起来像这样
listen SOCKET, QUEUESIZE
我们现在都知道 SOCKET 是什么了。QUEUESIZE 指定可以保持等待的尝试连接的数量;符号 SOMAXCONN 是此参数的最大值(通常为 5)。这让服务器可以处理多个近乎同时的连接请求,这是 HTTP 服务器或像 inetd 这样的守护程序的关键功能。
现在客户端程序可以尝试连接到服务器,但我们需要更多代码来实际创建链接。对于许多服务器,accept 函数在 listen 之后直接被调用,通常在某种循环中。accept 的语法是
accept NEWSOCKET, GENERICSOCKET
此函数打开 NEWSOCKET,这是一个文件句柄,您可以从中读取或写入以与连接的客户端通信。GENERICSOCKET 是任何打开的、已命名的套接字。对于我们的服务器,这是我们已经使用 socket 和 bind 创建的已命名套接字。accept 以与 bind 的 NAME 参数相同的形式返回 NEWSOCKET 的地址。
请注意,accept 调用会等待连接请求到达,因此在它完成之前无法进行任何处理。这通常不会造成问题,因为它符合大多数服务器的工作方式:它们等待请求,然后处理它。但是,有时应用程序可能会执行其他任务,例如计算或系统监控,这些任务不能停止以等待客户端连接。如果是这样,则可以异步完成通信——也就是说,可以使用信号处理程序临时中断处理,以建立套接字连接并处理客户端的请求。我不会详细介绍这一点,因为这需要对 fcntl 系统调用和信号处理程序进行冗长的离题,但列表 2 说明了基本思想。
UDP 不保证可靠性;额外的用户代码必须处理由未到达目的地的包引起的问题。互联网的主要无连接协议称为 UDP,或用户数据报协议。数据报包含将其发送到正确位置所需的所有信息。需要的。对于无连接服务器,不需要 listen 和 accept。无连接客户端通常确实需要使用 bind,以便将有效的返回地址传递给客户端数据包中的服务器,但我们在这里不担心客户端方面。要在我们的套接字上使用 UDP 而不是 TCP,我们只需将 socket 参数 SOCK_STREAM 替换为 SOCK_DGRAM,并将 getprotobyname 参数 tcp 替换为 udp。
在 C 语言中,我们使用系统函数 sendto 和 recvfrom 在客户端和服务器之间使用 UDP 发送数据,但 Perl 没有直接实现这些函数。相反,Perl 对面向连接和无连接协议都使用 send 和 recv。在使用 socket 和 bind 设置套接字后,无连接服务器通常会调用 recv
recv SOCKET, SCALAR, LEN, FLAGS
此函数会阻塞,直到 SOCKET 上有数据可用,然后将 LEN 字节读取到标量变量 SCALAR 中。FLAGS 是与 recv 系统调用相同的标志。recv 返回客户端的地址,然后可以使用该地址通过 send 函数发回信息
send SOCKET, MSG, FLAGS, TOTO 是客户端地址。最简单的无连接服务器中的套接字代码看起来像这样
socket(S, AF_INET, SOCK_DGRAM, \ getprotobyname('udp')); bind(S, sockaddr_in( $port, INADDR_ANY) ); $cli_addr = recv S, $request, 80, 0; send S, $message, 0, $cli_addr;现在回到我们的 TCP 服务器。记住我之前提到过,多个连接请求可以排队,以便服务器可以依次响应每个请求。如果服务器执行需要大量时间的操作,例如查询数据库或运行外部程序,这可能会效率低下(并且可能让客户端用户感到恼火)。为了解决这个问题,许多服务器在接受连接后会 fork 一个新进程来处理请求。查看我们的示例服务器代码以了解详细信息。唯一稍微棘手的部分是用于清理僵尸进程的 CHLD 信号处理程序。
服务器通常以 setuid 或 setgid 程序运行,这意味着进程具有拥有可执行文件的用户或组的权限,而与谁运行该程序无关。至少,服务器程序将以您自己的用户 ID 运行。由于原则上任何人都可以使用互联网服务器,因此您可以看到安全性至关重要。您必须确保服务器不会向重要的系统文件或您自己的机密数据提供特权访问。通常,这需要检查环境变量、文件权限、外部程序执行等,因此很难做到彻底。幸运的是,Perl 在这里通过其 污点模式 为我们提供了帮助,这是一种检查常见安全漏洞的模式。-T 命令行开关会启用此模式,因此我们只需将其添加到脚本顶部的“shebang”行即可。
示例服务器中的 exec 函数可能会引起至少两个安全问题。首先,执行外部程序意味着使用 PATH 环境变量。此变量被认为是受污染的,直到我们在脚本中显式设置它为止,因为它可能会被修改以导致执行与我们预期程序不同的程序。其次,我们将 exec 的参数分隔为程序名称和参数列表,这可以防止 exec 调用 shell 来执行元字符替换。如果未进行这些修改,污点模式将向终端发送警告并停止程序(实际上,这就是我发现这些问题的方式)。请记住,污点模式不能保证安全性,但它确实使识别众所周知的问题变得容易得多。
网络服务器是最复杂的软件之一,也就是说,您绝不应将本文视为对该主题的全面处理。尽管如此,您会惊讶地发现,即使在大型、复杂的服务器中,也出现了我们简单示例程序中的许多元素。Perl 确实降低了一些复杂性,因为您已经拥有方便的工具来处理困难的部分,例如解析协议和操作文件。即使您最终决定用 C 或其他某种编译语言编写程序,Perl 在服务器应用程序原型设计方面也是无与伦比的。价格也很合适,但我不需要说服 Linux 用户“免费”软件的价值。
Mike Mull 编写软件来模拟亚微观物体。更奇怪的是,人们付钱让他做这件事。Mike 认为 Linux 很棒。他最喜欢的编程项目是他 2 岁的儿子 Nathan。可以通过 mwm@cts.com 联系到 Mike。