使用 Perl 进行网络编程
Perl 被称为将互联网粘合在一起的胶水,因为它是一种极其强大的文本处理和 CGI 编程语言。虽然 Perl 最初被设计为一种文本操作语言,但它已发展成为一种强大的多用途编程语言。Perl 显示其威力的一个领域是网络编程。
Perl 通过提供内置函数,可以从头开始创建低级客户端/服务器程序,从而简化了网络编程。此外,许多模块可以免费使用,使常见的网络编程任务变得简单快捷。这些任务包括 ping 远程计算机、TELNET 和 FTP 会话。本文介绍了每种类型的网络程序的示例。
客户端/服务器网络编程需要一台机器上运行的服务器来为一个或多个客户端提供服务,这些客户端可以在同一台机器或不同的机器上运行。这些不同的机器可以位于网络上的任何位置。
要创建服务器,只需使用指示的内置 Perl 函数执行以下步骤
使用 socket 创建套接字。
使用 bind 将套接字绑定到端口地址。
使用 listen 监听端口地址的套接字。
使用 accept 接受客户端连接。
建立客户端甚至更容易
使用 socket 创建套接字。
使用 connect 将(套接字)连接到远程计算机。
在 Socket.pm 模块中定义了其他几个必需的函数和变量。此模块可能已安装在您的机器上,但如果未安装,则可以在 Comprehensive Perl Archive Network (CPAN)(官方 Perl 源代码存储库)中找到它(请参阅资源)。要在我们的程序中使用此模块,程序顶部需要以下语句
use Socket;
此语句将找到文件 Socket.pm 并导入其所有导出的函数和变量。
本文中的所有示例都使用了可以从 CPAN 免费获得的模块。
Perl 模块通常是自文档化的。如果模块的作者遵循创建 Perl 模块的普遍接受的规则,他们会将纯旧文档 (POD) 添加到模块的 .pm 文件中。查看 Socket 模块的 POD 的一种方法(假设 Perl 和 Socket.pm 已正确安装)是在 shell 中执行以下命令
perldoc Socket
此命令显示转换为 man 页面的 Socket.pm 的 POD。输出是对本模块中定义的函数和变量的相对全面的讨论。
查看文档的另一种方法是使用以下命令将 POD 转换为文本
pod2text \ /usr/lib/perl5/i686-linux/5.00404/Socket.pm | more
程序 pod2text 包含在 Perl 发行版中,程序 pod2html、pod2man、pod2usage 和 pod2latex 也是如此。
我们的第一个编程示例是一个简单的服务器,它在一台机器上运行,一次只能为一个从同一台或不同机器连接的客户端程序提供服务。回想一下,创建服务器的步骤是创建套接字,将其绑定到端口,监听端口并接受客户端连接。
列表 1 server1.pl 是此简单服务器的源代码。首先,通常最好使用 Perl 的严格规则进行编译
use strict;
这要求所有变量在使用前都使用 my 函数声明。使用 my 可能不方便,但它可以捕获许多常见的语法正确但逻辑上不正确的编程错误。
变量 $port 被分配第一个命令行参数或端口 7890 作为默认值。为您的服务器选择端口时,请选择机器上未使用的端口。请注意,确保选择的端口没有预定义用途的唯一方法是查看相应的 RFC(请参阅资源)。
接下来,使用 socket 函数创建套接字。套接字就像文件句柄——可以从中读取、写入或两者兼而有之。调用函数 setsockopt 以确保端口可以立即重用。
sockaddr_in 函数获取服务器上的端口。参数 INADDR_ANY 选择服务器的虚拟 IP 地址之一。您可以改为决定仅绑定虚拟 IP 地址之一,方法是将 INADDR_ANY 替换为
inet_aton("192.168.1.1")
或
gethostbyname("server.onsight.com")bind 函数将套接字绑定到端口,即将套接字插入该端口。然后,listen 函数使服务器开始在端口上监听。listen 函数的第二个参数是最大队列长度或最大挂起的客户端连接数。值 SOMAXCONN 是正在使用的机器的最大队列长度。
一旦服务器开始在端口上监听,它就可以使用 accept 函数接受客户端连接。当客户端被接受时,将创建一个名为 CLIENT 的新套接字,该套接字可以像文件句柄一样使用。从套接字读取会读取客户端的输出,而打印到套接字会将数据发送到客户端。
要在 Perl 中从文件句柄或套接字读取,请将其包装在尖括号中 (<FH>)。要写入它,请使用 print 函数
print SOCKET;
accept 函数的返回值是以打包格式的客户端的 Internet 地址。函数 sockaddr_in 采用该格式并返回客户端的端口号和客户端的数字 Internet 地址(以打包格式)。打包的数字 Internet 地址可以使用 inet_ntoa(数字到 ASCII)转换为表示数字 IP 的文本字符串。要将打包的数字地址转换为主机名,可以使用函数 gethostbyaddr。
假设本文中提到的所有服务器都在名为 server.onsight.com 的机器上启动。要在该机器上启动服务器,请执行
[james@server networking]$ server1.pl SERVER started on port 7890
服务器现在正在 server.onsight.com 上的端口 7890 上监听,等待客户端连接。
列表 2 client1.pl 显示了一个简单的客户端。此程序的第一个命令行参数是它应该连接到的主机名,默认为 server.onsight.com。第二个命令行参数是端口号,默认为 7890。
主机名和端口号用于使用 inet_aton(ASCII 到数字)和 sockaddr_in 生成端口地址。然后使用 socket 创建套接字,客户端使用 connect 将套接字连接到端口地址。
然后 while 循环读取服务器发送给客户端的数据,直到到达文件末尾,并将此输入打印到 STDOUT。然后关闭套接字。
假设所有客户端都在名为 client.avue.com 的机器上启动,尽管它们可以从网络上的任何机器执行。要执行客户端,请键入
[james@client networking]$ client1.pl server.onsight.com Hello from the server: Tue Oct 27 09:48:40 1998
以下是服务器的标准输出
got a connection from: client.avue.com [192.168.1.2]
当您想控制套接字的创建方式、要使用的协议等时,使用上述函数创建套接字是很好的。但是使用上面的函数太难了;我更喜欢简单的方法——IO::Socket。
模块 IO::Socket 提供了一种创建套接字的简单方法,然后可以像文件句柄一样使用它们。如果您的机器上没有安装它,可以在 CPAN 上找到它。要查看此模块的 POD,请键入
perldoc IO::Socket
列表 3 serverIO.pl 是一个使用 IO::Socket 的简单服务器。使用 new 方法创建一个新的 IO::Socket::INET 对象。请注意,该方法的参数包括主机名、端口号、协议、队列长度以及指示我们希望此端口立即可重用的选项。new 方法返回分配给 $sock 的套接字。此套接字可以像文件句柄一样使用——我们可以从中读取客户端输出,也可以通过向客户端发送数据来写入它。
使用 accept 方法接受客户端连接。请注意,当在标量上下文中评估时,accept 方法返回客户端套接字
$new_sock = $sock->accept()
当在列表上下文中评估时,返回客户端的套接字和客户端的 IP 地址
($new_sock, $client_addr) = $sock->accept()客户端地址的计算和打印方式与列表 1 server1.pl 中相同。然后读取与该客户端关联的套接字,直到文件末尾。读取的数据将打印到 STDOUT。此示例说明服务器可以使用套接字变量周围的 < > 从客户端读取。
列表 4 clientIO.pl 是一个使用 IO::Socket 的简单客户端。这次,创建一个新对象,该对象使用 TCP 协议连接到主机上的端口。然后将十个字符串打印到该服务器,然后关闭套接字。
如果执行列表 3 serverIO.pl 中的服务器,然后连接列表 4 clientIO.pl 中的客户端,则输出将是
[james@server networking]$ serverIO.pl
got a connection from: client.avue.com [192.168.1.2] hello, world: 1 hello, world: 2 hello, world: 3 hello, world: 4 hello, world: 5 hello, world: 6 hello, world: 7 hello, world: 8 hello, world: 9 hello, world: 10
可以创建彼此双向通信的服务器和客户端。例如,客户端可以向服务器发送信息,然后服务器可以将信息发回客户端。因此,可以编写网络程序,使服务器和客户端遵循一些预定的协议。
列表 5 server2way.pl 显示了如何创建一个简单的服务器来读取客户端的命令,然后向客户端打印适当的响应。模块 Sys::Hostname 提供了一个名为 hostname 的函数,该函数返回服务器的主机名。为了确保在我们打印时看到输出,使用 autoflush 函数关闭了 STDOUT 文件句柄的 IO 缓冲。然后执行一个 while 循环来接受连接。当客户端连接时,服务器从客户端读取一行,并删除换行符。然后执行一个 switch 语句。(switch 被巧妙地伪装成 foreach 循环,这恰好是我编写 switch 的最喜欢的方式之一。)根据客户端输入的输入,服务器输出适当的响应。读取客户端的所有行,直到文件末尾。
列表 6 client2way.pl 显示了配套客户端。与服务器建立连接后,客户端向服务器打印一些命令,读取响应并将响应打印到 STDOUT。
以下是列表 6 中客户端代码的输出
[james@client networking]$ client2way.pl server.onsight.com Hi server.onsight.com Tue Oct 27 15:36:19 1998 DEFAULT
如果您想编写一个接受来自 STDIN 的命令并将其发送到服务器的客户端,最简单的解决方案是编写一个 fork 子进程的客户端。(可以使用 select 编写一个不 fork 的解决方案,但这更复杂。)客户端的父进程将通过 STDIN 读取用户的命令并将其打印到服务器。然后,客户端的子进程将从服务器读取并将响应打印到 STDOUT。
列表 7 clientfork.pl 是一个 fork 的客户端示例。
要在 Perl 中 fork,请调用巧妙命名的 fork 函数。如果 fork 失败,它返回 undef。如果成功,它向子进程返回 0,向父进程返回非零值(子进程的 pid)。在 clientfork.pl 中,if 语句检查 $kid 的值,即 fork 的返回值。如果 $kid 为真(非零,子进程的 pid),则父进程执行从 STDIN 读取打印到服务器的操作。如果 $kid 为假(零),则子进程执行从服务器读取打印到 STDOUT 的操作。
以下是执行列表 7 clientfork.pl 中的客户端代码连接到列表 5 server2way.pl 中的代码的示例会话
[james@client networking]$ clientfork.pl server.onsight.com NAME server.onsight.com DATE Tue Oct 27 15:42:58 1998 HELP DEFAULT HELLO Hi
当父进程完成从 STDIN 读取后,它执行 kill 函数来终止子进程。父进程收割其子进程非常重要,这样子进程就不会比父进程活得更长而变成僵尸进程。
服务器通常不会一次只处理一个客户端。处理多个客户端的一种方法是服务器 fork 一个子进程来处理每个客户端连接。列表 8 serverfork.pl 是一个 forking 服务器的示例。
父进程收割其子进程的一种方法是定义一个子例程,并将对该子例程的引用分配给 $SIG{CHLD}。(哈希 %SIG 是 Perl 处理信号的方式。)在此示例中,定义了一个名为 REAP 的子例程,并将对该子例程的引用分配给 $SIG{CHLD}。当父进程接收到 CHLD(子进程终止)信号时,将调用 REAP 子例程。
在接受所有客户端连接的 while 循环中,服务器 fork。如果 fork 返回 true,则父进程正在运行,并且它执行 next 语句,该语句立即将控制权转移到 continue 块,执行关闭子套接字的内务处理步骤,并等待下一个客户端连接。如果 fork 返回 undef,则 fork 失败,因此服务器终止。如果 fork 返回既不是 true 也不是 undef,则子进程正在运行,因此父套接字被关闭,子进程从客户端读取并处理客户端。当子进程完成处理客户端后,子进程退出并被父进程收割。
Perl 版本 5.005 支持线程编程。这意味着可以创建一个线程网络程序作为服务器或客户端。
列表 9、10 和 11 是客户端的三个不同版本,这些客户端登录到多个 Web 服务器并确定正在使用的服务器类型(Apache、Netscape 等)。
列表 9 getservertype1.pl 显示了一个非 fork、非线程客户端。首先,创建一个主机数组并将其初始化为几个网站。子例程 doit 被定义为接收 Web 服务器名称作为参数,打开与端口 80(HTTP 端口)上该服务器的客户端连接,发送 HTTP 请求并读取响应的每一行。当读取以 Server: 开头的行时,它将提取服务器名称并将其存储在 $1 中。然后打印主机名和 Web 服务器名称。为 @hosts 数组中的每个主机调用此子例程。
这是 getservertype1.pl 的输出
processing www.ssc.com... www.ssc.com: Stronghold/2.2 Apache/1.2.5 PHP/FI-2.0b12 processing www.linuxjournal.com... www.linuxjournal.com: Stronghold/2.2 Apache/1.2.5 PHP/FI-2.0b12 processing www.perl.com... www.perl.com: Apache/1.2.6 mod_perl/1.11 processing www.perl.org... www.perl.org: Apache/1.2.5 processing www.nytimes.com... www.nytimes.com: Netscape-Enterprise/2.01 processing www.onsight.com... www.onsight.com: Netscape-Communications/1.12 processing www.avue.com... www.avue.com: Netscape-Communications/1.12
请注意,主机按照存储在 @hosts 中的相同顺序处理。
列表 10 getservertype2.pl 是 getservertype1.pl 的 fork 版本。fork 发生在 foreach 循环中。执行 fork,如果它返回 true,则父进程执行 next 语句到下一个主机名。如果 fork 返回 undef,则程序终止。否则,子进程调用 doit 函数传入主机,然后退出。在父进程完成其在 while 循环中的工作后,它等待所有子进程完成,然后退出。
这是 getservertype2.pl 的输出
processing www.ssc.com... processing www.linuxjournal.com... processing www.perl.com... processing www.perl.org... processing www.nytimes.com... processing www.onsight.com... processing www.avue.com... www.onsight.com: Netscape-Communications/1.12 www.nytimes.com: Netscape-Enterprise/2.01 www.avue.com: Netscape-Communications/1.12 www.linuxjournal.com: Stronghold/2.2 Apache/1.2.5 PHP/FI-2.0b12 www.perl.com: Apache/1.2.6 mod_perl/1.11 www.ssc.com: Stronghold/2.2 Apache/1.2.5 PHP/FI-2.0b12 www.perl.org: Apache/1.2.5 Parent exiting...
请注意,主机未按存储在 @hosts 中的顺序打印。它们按照处理顺序打印,较慢的主机花费的时间比较快的主机长。
列表 11 getservertype3.pl 是一个线程版本。在循环遍历主机名时,将创建一个新的 Thread 对象。创建 Thread 时,new 方法被传递一个对线程将执行的子例程的引用,以及传递给该子例程的参数。然后线程执行其子例程,当子例程返回时,线程被销毁。这是 getservertype3.pl 的输出
processing www.ssc.com... processing www.linuxjournal.com... processing www.perl.com... processing www.perl.org... processing www.nytimes.com... processing www.onsight.com... processing www.avue.com... www.nytimes.com: Netscape-Enterprise/2.01 www.onsight.com: Netscape-Communications/1.12 www.avue.com: Netscape-Communications/1.12 www.linuxjournal.com: Stronghold/2.2 Apache/1.2.5 PHP/FI-2.0b12 www.perl.com: Apache/1.2.6 mod_perl/1.11 www.ssc.com: Stronghold/2.2 Apache/1.2.5 PHP/FI-2.0b12 www.perl.org: Apache/1.2.5
Net::Ping 模块使 ping 主机变得容易。列表 12 ping.pl 是一个类似于我的服务器上程序的程序,该程序 ping 我的 ISP 以保持我的连接处于活动状态。首先,创建一个新的 Net::Ping 对象。选择的协议是 tcp(选择项为 tcp、udp 和 icmp;默认值为 udp)。第二个参数是超时(两秒)。然后执行无限循环,ping 所需的主机。如果主机响应,ping() 方法返回 true,否则返回 false,并打印相应的消息。然后程序休眠十秒钟并再次 ping。
列表 12 ping.pl 的示例输出为
Success: Wed Nov 4 14:47:58 1998 Success: Wed Nov 4 14:48:08 1998 Success: Wed Nov 4 14:48:18 1998 Success: Wed Nov 4 14:48:28 1998 Success: Wed Nov 4 14:48:38 1998 Success: Wed Nov 4 14:48:48 1998
Net::Telnet 模块使自动化 TELNET 会话变得容易。列表 13 telnet.pl 是连接到机器、发送一些系统命令并打印结果的示例。
首先,使用服务器和用户名。用户名默认为运行脚本的用户,方法是将 $user 分配给值 $ENV{USER}。(哈希 %ENV 包含脚本从 shell 继承的所有环境变量。)
接下来,请求密码,然后读入。请注意,关闭 stty 回显是通过 system 调用完成的。也可以使用 Term::ReadKey 模块完成。
然后,创建一个 Net::Telnet 对象。要使用此对象登录到服务器,请调用 login 方法。使用 cmd 方法执行几个系统命令,该方法返回系统命令的 STDOUT,然后打印该 STDOUT。请注意,该输出的一部分是系统提示符,它与命令的输出一起打印。
另请注意,代码 $tn->cmd('/usr/bin/who') 在列表上下文中求值并存储在 @who 中,@who 是一个数组,其中包含该命令的所有输出行,每个输出行对应一个数组元素。
执行完所有系统命令后,TELNET 会话关闭。
这是列表 13 telnet.pl 的示例输出
Enter password:
Hostname: server.onsight.com [james@server james] Here's who: james tty1 Oct 24 21:07 james ttyp1 Oct 27 20:59 (:0.0) james ttyp2 Oct 24 21:11 (:0.0) james ttyp6 Oct 28 07:16 (:0.0) james ttyp8 Oct 28 19:02 (:0.0) [james@server james] What is your command: date Thu Oct 29 14:39:57 EST 1998 [james@server james]
Net::FTP 模块使自动化 FTP 会话变得容易。列表 14 ftp.pl 是连接和获取文件的示例。
创建一个 Net::FTP 对象,调用 login 以登录到机器,cwd 更改工作目录,get 方法获取文件。然后使用 quit 终止会话。
有许多方法可以执行常见的 FTP 操作:put、binary、rename、delete 等。要查看所有可用方法的列表,请键入
perldoc Net::FTP
这是列表 14 ftp.pl 的示例输出
[james@k2 networking]$ ftp.pl server.onsight.com Enter your password: Before ---------------------------------------- /bin/ls: *.gz: No such file or directory ---------------------------------------- After ---------------------------------------- perl5.005_51.tar.gz ----------------------------------------
使用 Net::Telnet 和 Net::FTP,可以创建一个非常简单的脚本,该脚本可以存档远程计算机上的目录结构。
列表 15 taritup.pl 是一个 Perl 程序,类似于我使用的程序,该程序登录到我的 ISP 并存档我的网站。
此程序遵循的步骤是
使用 TELNET 在远程计算机上启动会话。
使用 cd 转到网页目录。
使用 tar 存档目录。
启动与远程计算机的 FTP 会话。
更改为包含 tar 文件的目录。
获取 tar 文件。
退出 FTP 会话。
返回 TELNET 会话,删除远程计算机上的 tar 文件。
关闭 TELNET 会话。
此程序输出文本,让用户知道脚本的进度。
