Perl 网络编程基础

在计算机之间共享信息和传输文件是网络时代做任何事情都不可缺少的,Perl 也提供了很多函数用于在程序中获取网络信息。这对用于进程间通信机制(套接字、消息队列等)的程序,都是相当有用和方便的。在进行进程间通信时,可通过套接字以客户端/服务端模型来处理。
套接字是一种代表了两个通信进程端点(即服务器和客户端)之间的软件抽象;也就是说,套接字使得进程之间能否实现对话。Perl 5引入了一个特殊的Socket.pm模块来处理套接字,使程序可以更容易地从一台计算机输出到另一台计算机上。

一、Perl 协议函数
下面的函数用于从/etc/protocols文件检索信息。
1、getprotoent 函数
该函数会从网络协议数据库/etc/protocols中读取下一行内容并返回列表,其中的项目是协议的正式名称、协议的别名和替换名、还有协议号。
setprotoent 函数则负责打开并回滚文件/etc/protocols,如果STAYOPEN不为零,则成功调用getprotoent函数之后不关闭该数据库。
endprotoent 函数负责关闭数据库。

($name,$aliases,$proto) = getprotent;
setprotoent (STAYOPEN);
endprotoent;

2、getprotobyname 函数
该函数类似getprotoent,但以协议名作为参数,返回其名称、别名以及协议号。

($name,$aliases,$proto) = getprotobyname('tcp');

3、getprotobynumber 函数
同样的,该函数以协议号作为参数,返回其名称、别名以及协议号。
($name,$aliases,$proto) = getprotobynumber(0);

二、Perl 服务器函数
下面的函数负责在网络服务文件/etc/services中查询信息。
1、getservent 函数
该函数能从/etc/services文件中读取一行内容,如果STAYOPEN不为零,则每次调用之后都不关闭数据库文件。

($name,$aliases,$port,$proto) = getservent;
setservent (STAYOPEN);
endservent;

2、getservbyname 函数
该函数负责把服务端口名转译成相应的端口号。

($name,$aliases,$proto,$protocol) = getservbyname('telnet','tcp');

3、getservbyport 函数
以端口和协议为参数来获得相应的信息。

($name,$aliases,$port,$proto) = getservbyport('517','udp');

三、Perl 主机信息函数
下面的函数负责从/etc/hosts文件中检索信息。
1、gethostent 函数
该函数能返回一个由/etc/hosts文件每一行内容所构成的列表,其中包括主机的正式名称、主机地址、主机地址的字节长度以及按字节顺序排列的网络地址列表。

($name,$aliases,$addrtype,$length,@addrs) = gethostent;
sethostent (STAYOPEN);
endhostent;

※ 注意,gethostent 函数返回的原始地址按字节顺序排列,需要使用unpack函数解压为4个字节的形式,才方便输出。

($a,$b,$c,$d) = unpack ('C4',$addr[0]);
print "Local host address (unpacked) $a.$b.$c.$d\n";

这里的$a.$b.$c.$d的输出结果,就是类似127.0.0.1的形式。

2、gethostbyaddr 函数
该函数可将一个网络地址转译为对应的主机名。

$address = pack ('C4',127,0,0,1);
($name,$aliases,$addrtype,$length,@addrs) = gethostbyaddr ($address,2);
($a,$b,$c,$d) = unpack ('C4',$addrs[0]);
print "Hostname is $name and the Internet address is $a.$b.$c.$d.\n";

可见,在传递网络地址参数时,必须先把通常的xxx.xxx.xxx.xxx地址打包成4个字节的形式。另外,该函数的第二个参数是位于/usr/include/sys/socket.h中定义的域数值,在这里也可以使用AF_INET的形式,该常量的值就是2。

3、gethostbyname 函数
该函数以具体的主机名作为参数。

($name,$aliases,$addrtype,$length,@addrs) = gethostbyname('linuxing');

对网络地址的处理方式相同。

四、套接字
套接字使得网络上进程之间能够实现对话,进程通信的进程可以位于同一台计算机上,也可在不同计算机上。服务器进程会创建一个套接字,客户端通过其名称获得这个套接字。为了确保网络上数据的来源地址和发送地址,套接字会用到IP和端口。使用套接字的程序会打开套接字(类似打开文件),一旦套接字顺利打开,便可进行I/O操作,并读写信息到通信的进程。
当打开文件时,会返回一个文件描述符,同样的,在打开套接字时,也会将套接字的一个描述符返回给服务器,另一个返回给客户端。但套接字接口要比使用文件更复杂,因为通信过程往往是跨网络的。不过,这些复杂的处理大部分都会由Perl提供的函数帮我们解决了。
1、套接字的类型
每个套接字都拥有通信路径的类型,用于标识如何通过该套接字传输数据。两种最为常见的套接字类型是SOCK_STREAM和SOCK_DGRAM,二者分别使用了TCP和UDP协议。
流套接字(stream sockets)
即SOCK_STREAM类型提供了可靠的、面向连接的、序列化的双向服务。它提供了错误探测和流控制机制,并消除了重复的数据分段。底层协议是TCP。
数据报套接字(datagram sockets)
即SOCK_DGRAM提供了无连接的服务,它会以任何顺序接受发送的包,或者另一端根本收不到包,而它不会保证传递成功,并且包也可能出现重复,底层协议是UDP。

2、套接字域
在打开套接字时,会触发一些系统调用,它们描述了如何使用该套接字。进程必须规定通信的域(domain),又称为地址族(address family),它负责标识套接字命名的方式,并决定为发送和接收数据需要使用哪一种协议和地址。套接字域负责标识服务器和客户端的位置:是在同一台计算机,在Internet上,还是在Xerox网络上。
Unix域和Internet域是两种标准域。在使用套接字进行进程间通信时,Unix域负责同一台计算机上的进程通信,而Internet域则用于使用TCP/IP协议的远程计算机的进程通信。
Unix域,如果是本地计算机上的两个进程间进行通信,其地址族(AF)就是AF_UNIX。其支持SOCK_STREAM和SOCK_DGRAM两种套接字类型,SOCK_STREAM提供了进程间的双向通信能力,类似于pipe;而SOCK_DGRAM不可靠,一般少用。在/usr/include/sys/socket.h中,将AF_UNIX地址族赋值为常量1。
Internet域,标识为AF_INET地址族,用于不同计算机上的进程之间通信,同样也支持SOCK_STREAM和SOCK_DGRAM两种套接字类型,分别对应两个底层协议TCP和UDP。在/usr/include/sys/socket.h中,将AF_INET地址族定义为常量2。
套接字地址
在创建好套接字后,需要提供一个地址,以便通过该地址将数据发送给它。在AF_UNIX域中,这个地址就是一个文件名;而在AF_INET域中,则是IP地址和端口号。

3、创建套接字
使用socket函数会创建套接字,并返回一个指向该套接字的文件句柄。在服务器中,这个套接字又称为集结套接字(rendezvous socket)。

socket (SOCKET_FILEHANDLE,DOMAIN,TYPE,PROTOCOL);

例如:

$AF_UNIX=1;
$SOCK_STREAM=1;
$PROTOCOL=0;
socket (COMM_SOCKET,$AF_UNIX,$SOCK_STREAM,$PROTOCOL);

这里,socket函数使用地址族是Unix域,套接字类型是STREAM,协议号是0(允许系统为该套接字选择正确的协议)的参数创建了一个COMM_SOCKET的文件句柄。

4、绑定地址和套接字名
使用bind函数负责将地址或名称附加到SOCKET文件句柄。

bind (SOCK_FILEHANDLE,NAME);

这个动作,需要根据使用的域类型,给出文件路径或IP、端口地址参数,例如:

bind (COMM_SOCKET,sockaddr_un("/home/linuxing/perl/tserv"));
$hostname="linuxing";
$port="3000";
$AF_INET=2;
$SOCK_STREAM=1;
($name,$aliases,$addrtype,$length,$address) = gethostbyname($hostname);
$packed_address = pack (S n a4 x8,$AF_INET,$port,$address);
bind (TCP_SOCKET,$packed_address);

两个示例分别对应UNIX域和Internet域两种不同的情况。
※ 注意
其中,文件路径名和$packed_address都需要使用由pack函数按指定的格式打包后返回的值。(就像gethostbyaddr函数中的address一样)
这里,$packed_address就是使用pack把对应linuxing主机的IP和端口,根据$AF_INET类型打包来生成。
同样的,用于Unix域的时候也一样,不能直接提供文件路径(原来参考的书籍《Perl 实例精解》有误)。也需要利用pack来打包为指定的格式。但我实在找不到该格式应该如何编写,但Socket.pm模块提供了一个更方便的函数sockaddr_un来进行该转换。

◎ sockaddr_un函数
引用
sockaddr_un (pathname)
pack_sockaddr_un (pathname)
Takes one argument, a pathname, and returns the Unix domain socket address structure (the path packed in with AF_UNIX filled in). For Unix domain sockets, this structure is normally what you need for the arguments in bind, connect, and send, and is also returned by getpeername, getsockname, and recv.


5、创建套接字队列
使用listen函数负责在套接字上等待accept,并规定拒绝进一步请求之前所允许的排队连接总数。如果客户端请求数目超出队列的容量,则会返回错误信息。调用成功,返回真,否则返回假,并将错误代码保存在$!里面。

listen (SOCKET_FILEHANDLE,QUEUE_SIZE);

例如:

listen (TCP_SOCKET,5);

6、等待客户端请求
服务器进程中,使用accept函数会等待客户端请求的到达。如果队列中有请求,则在建立连接时从队列内删除第一个请求。然后,accept函数会使用与原来的套接字或一般套接字(又称作集结套接字)相同的属性打开新的套接字文件句柄,并将它附加给客户端的套接字。
新的套接字可以与客户端套接字通信。这时便可使用一般套接字来接受附加的连接了。
成功调用返回真,否则返回假,并将错误代码保存在$!里面。

accept (NEWSOCKET,GENERICSOCKET);

例如:

accept (NEWSOCKET,TCP_SOCKET);

服务器进程使用accept函数接收来自客户端的请求,并取队列中等待的第一个请求,并创建新的套接字文件句柄NEWSOCKET,而其用到的属性和TCP_SOCKET套接字文件句柄是相同的。

7、建立套接字连接
客户端使用connect函数以套接字文件句柄和要连接的服务器地址,配合服务端的accept函数生成一个集结。建立连接后,便可传输数据了。
如果使用的是Unix域,则应提供文件名(已经由bind函数创建,并存在的)。如果使用的是Internet域,其地址则是对该服务器类型正确的网络地址。
调用成功,返回真,否则返回假,并将错误代码保存在$!里面。

connect (SOCKET,ADDRESS);

例如:

connect (CLIENT_SOCKET,sockaddr_un("/home/linuxing/perl/tserv"));
connect (CLIENT_TCP_SOCKET,$packed_address);

第一个语句对应于Unix域的情况,附加的参数是当前服务器下的一个有效的socket文件打包后的值。第二个语句对应的是Internet域,提供的是一个服务端有效的IP和端口打包后的地址参数,该参数的生成方式,与上面bind函数的参数相同。

8、关闭套接字
使用shutdown函数按规定关闭套接字连接。
HOW参数为0,则在套接字上进一步的接收请求讲被拒绝;参数为1,则在套接字上进一步的发送请求会被拒绝;参数为2,则所有的接收和发送请求都会被拒绝。

shutdown (SOCKET,HOW);

例如:

shutdown (COMM_SOCK,2);

五、简单例子
这里的例子仅提供了一个,对应AF_UNIX域的情况。正如前面所说的,UNIX域一般不会使用SOCK_DGRAM类型来传输,所以,这里都是用SOCK_STREAM传输数据。而AF_INET域的情况是类似的。但因为实际中,一般不会完全依赖perl的基础函数来实现这些功能,特别是pack和unpack对IP地址、socket文件的转换等动作,否则处理起来相当麻烦。常用的,是借助Socket.pm模块提供的参数来协助转换,AF_INET域的示例,将在后一篇日志中以该模块的形式来说明。

这只适合于客户端和服务端都在同一台计算机上的情况,客户端从服务端读取并接收消息后,打印消息到客户端屏幕。并且该代码忽略了很多错误判断操作,仅供参考。
服务端程序:

$ cat unix_socket_server.pl
#!/usr/bin/perl -w
# unix_socket_server.pl
use Socket;
print "Server Started.\n";
$AF_UNIX=1;
$SOCK_STREAM=1;
$PROTOCOL=0;
socket(SERVERSOCKET,$AF_UNIX,$SOCK_STREAM,$PROTOCOL) ||
die "Socket $!\n";
print "Socket OK\n";

$name="./.mysock";
unlink "$name" || warn "$name: $!\n";

bind(SERVERSOCKET,sockaddr_un($name)) || die "Bind $!\n";
print "Bind OK\n";

listen(SERVERSOCKET,5) || die "Listen $!\n";
print "Listen OK\n";

while(1) {
accept(NEWSOCKET,SERVERSOCKET) || die "Accept $!\n";
defined($pid = fork) || die "Fork: $!\n";
if ($pid == 0) {
print(NEWSOCKET "Greetings from your server !!\n");
close(NEWSOCKET);
exit(0);
}
else {
close(NEWSOCKET);
}
}

客户端程序:

$ cat unix_socket_client.pl
#!/usr/bin/perl -w
# unix_socket_client.pl
use Socket;
print "Hi I'm the client\n";
$AF_UNIX=1;
$SOCK_STREAM=1;
$PROTOCOL=0;
socket(CLIENTSOCKET,$AF_UNIX,$SOCK_STREAM,$PROTOCOL);

$name="./.mysock";
do {
$result = connect(CLIENTSOCKET,sockaddr_un("$name"));
if ($result != 1) {
sleep(1);
}
} while ($result != 1);
read(CLIENTSOCKET,$buf,500);
print STDOUT "$buf\n";
close(CLIENTSOCKET);
exit(0);

结果:
首先,启动服务端进程:
引用
$ perl unix_socket_server.pl
Server Started.
Socket OK
Bind OK
Listen OK

然后,打开另外一个终端,运行客户端程序进行连接:
引用
$ perl unix_socket_client.pl
Hi I'm the client
Greetings from your server !!

源码: 点击这里下载文件
六、参考文档
《Perl 实例精解》通过网络发送部分的内容
Using open() for IPC
这个链接中的提供的示例更接近实际应用,我把其内容做了个拷贝: 点击这里下载文件

你可能感兴趣的:(perl,网络,编程,socket,unix,stream)