Skip to content

Perl Socket 编程

Perl - 套接字编程 (Socket Programming)

Section titled “Perl - 套接字编程 (Socket Programming)”

套接字编程(Socket programming)允许进程通过网络进行通信,无论是在同一台机器上还是跨不同机器。Perl 通过其内置的 Socket 模块和各种 CPAN 模块提供了强大的网络编程支持。

套接字是运行在网络上的两个程序之间双向通信链接的一个端点(endpoint)。它是一种抽象,结合了 IP 地址(标识一台机器)和端口号(标识该机器上的特定应用程序或服务)。例如,Web 服务器通常监听 80 端口 (HTTP) 或 443 端口 (HTTPS)。

Perl 的 Socket 模块提供了基于 Berkeley Sockets API 的函数和常量,用于创建和管理套接字。

客户端-服务器模型 (Client-Server Model)

Section titled “客户端-服务器模型 (Client-Server Model)”

套接字编程中常见的模式是客户端-服务器模型:

服务器端步骤:

  1. 创建套接字: 使用 socket()。
  2. 绑定套接字: 使用 bind() 将套接字与服务器机器上的特定 IP 地址和端口关联。
  3. 监听连接: 使用 listen() 将套接字置于监听状态,以接受传入的客户端连接。
  4. 接受连接: 当客户端尝试连接时,使用 accept() 接受连接。这通常会创建一个新的套接字,专门用于与该特定客户端通信。

客户端步骤:

  1. 创建套接字: 使用 socket()。
  2. 连接到服务器: 使用 connect() 建立与服务器 IP 地址和端口的连接。

连接建立后,客户端和服务器可以使用 send() 和 recv() 等函数交换数据,或通过套接字句柄使用标准的 Perl I/O 操作(例如,print SOCKET $data; 和 $line = <SOCKET>;)。

核心套接字函数(来自 Socket 模块)

Section titled “核心套接字函数(来自 Socket 模块)”

你的套接字程序总是以 use Socket; 开头,以导入必要的函数和常量。

1. socket(SOCKET_HANDLE, DOMAIN, TYPE, PROTOCOL)

Section titled “1. socket(SOCKET_HANDLE, DOMAIN, TYPE, PROTOCOL)”

创建一个套接字并将其与 SOCKET_HANDLE(一个文件句柄)关联。

  • DOMAIN:地址族。PF_INET (或 AF_INET) 用于 IPv4。PF_INET6 (或 AF_INET6) 用于 IPv6。
  • TYPE:套接字类型。SOCK_STREAM 用于 TCP(可靠、面向连接)。SOCK_DGRAM 用于 UDP(不可靠、无连接)。
  • PROTOCOL:协议号。通常是 0 让系统选择,或者 (getprotobyname('tcp'))[2] 用于 TCP,(getprotobyname('udp'))[2] 用于 UDP。
Example: `socket(my $socket_fh, PF_INET, SOCK_STREAM, (getprotobyname('tcp'))[2]) or die "socket: $!";`

2. bind(SOCKET_HANDLE, PACKED_ADDRESS) (服务器端)

Section titled “2. bind(SOCKET_HANDLE, PACKED_ADDRESS) (服务器端)”

为套接字分配一个本地地址(IP 和端口)。

PACKED_ADDRESS 通常使用 pack_sockaddr_in(PORT, IP_ADDRESS_BINARY) 为 IPv4 创建。IP_ADDRESS_BINARY 通常是 inet_aton('hostname_or_ip') 或 INADDR_ANY(绑定到所有可用网络接口)。

Example: `my $port = 7890;
my $packed_addr = pack_sockaddr_in($port, INADDR_ANY);
bind($server_fh, $packed_addr) or die "bind: $!";`

3. listen(SOCKET_HANDLE, QUEUE_SIZE) (服务器端)

Section titled “3. listen(SOCKET_HANDLE, QUEUE_SIZE) (服务器端)”

将套接字标记为用于接受传入连接请求的被动套接字。QUEUE_SIZE 是待处理连接的最大数量。

Example: `listen($server_fh, 5) or die "listen: $!";` (5 是常见的积压队列大小)

4. accept(NEW_SOCKET_HANDLE, LISTENING_SOCKET_HANDLE) (服务器端)

Section titled “4. accept(NEW_SOCKET_HANDLE, LISTENING_SOCKET_HANDLE) (服务器端)”

提取 LISTENING_SOCKET_HANDLE 待处理连接队列中的第一个连接请求,创建一个新的已连接套接字,并将其与 NEW_SOCKET_HANDLE 关联。此调用会阻塞,直到连接可用。

Example: `my $client_addr = accept(my $client_fh, $server_fh) or die "accept: $!";`
`$client_addr` 将包含已连接客户端的打包地址。

5. connect(SOCKET_HANDLE, PACKED_SERVER_ADDRESS) (客户端)

Section titled “5. connect(SOCKET_HANDLE, PACKED_SERVER_ADDRESS) (客户端)”

建立与 PACKED_SERVER_ADDRESS 处服务器的连接。

Example: `my $server_host = 'localhost';
my $server_port = 7890;
my $server_ip_bin = inet_aton($server_host) or die "Cannot resolve $server_host";
my $packed_server_addr = pack_sockaddr_in($server_port, $server_ip_bin);
connect($client_fh, $packed_server_addr) or die "Can't connect to $host:$port: $!";`

这是一个服务器向客户端发送问候消息的基本示例。

#!/usr/bin/perl
use strict;
use warnings;
use Socket;
use POSIX 'WNOHANG'; # 用于非阻塞检查子进程
my $port = shift || 7890;
my $protocol = getprotobyname('tcp');
# 创建套接字
socket(my $listen_fh, PF_INET, SOCK_STREAM, $protocol)
or die "Can't create socket: $!";
# 允许地址重用
setsockopt($listen_fh, SOL_SOCKET, SO_REUSEADDR, 1)
or die "Can't set SO_REUSEADDR: $!";
# 绑定端口
my $server_addr = pack_sockaddr_in($port, INADDR_ANY);
bind($listen_fh, $server_addr)
or die "Can't bind to port $port: $!";
# 监听连接
listen($listen_fh, SOMAXCONN) # SOMAXCONN 是系统的最大队列大小
or die "Can't listen on port $port: $!";
print "Server started on port $port, waiting for connections...\n";
# 处理子进程终止以防止僵尸进程
$SIG{CHLD} = sub {
while (waitpid(-1, WNOHANG) > 0) {
# 子进程已回收
}
};
while (1) {
my $client_packed_addr = accept(my $client_fh, $listen_fh);
unless (defined $client_packed_addr) {
warn "accept failed: $!";
next;
}
# 获取客户端信息用于日志记录
my ($client_port, $client_ip_bin) = unpack_sockaddr_in($client_packed_addr);
my $client_ip_str = inet_ntoa($client_ip_bin);
print "Connection from $client_ip_str:$client_port\n";
# fork 一个子进程来处理这个客户端
my $pid = fork();
die "Cannot fork: $!" unless defined $pid;
if ($pid == 0) { # 子进程
close $listen_fh; # 子进程不需要监听套接字
# 发送问候消息
print $client_fh "Hello from Perl Server!\n";
print "Sent greeting to $client_ip_str:$client_port\n";
# 示例:从客户端读取一行
my $client_message = <$client_fh>;
if (defined $client_message) {
chomp $client_message;
print "Received from $client_ip_str: '$client_message'\n";
}
close $client_fh;
exit 0; # 子进程退出
} else { # 父进程
close $client_fh; # 父进程关闭客户端连接句柄
# 父进程继续监听新的连接
}
}
close $listen_fh;
#!/usr/bin/perl
use strict;
use warnings;
use Socket;
my $host = shift || 'localhost';
my $port = shift || 7890;
my $protocol = getprotobyname('tcp');
# 解析服务器地址
my $server_ip_bin = inet_aton($host)
or die "Cannot resolve hostname '$host': $!";
my $server_addr = pack_sockaddr_in($port, $server_ip_bin);
# 创建套接字
socket(my $client_fh, PF_INET, SOCK_STREAM, $protocol)
or die "Can't create socket: $!";
# 连接到服务器
connect($client_fh, $server_addr)
or die "Can't connect to $host:$port: $!";
print "Connected to server $host:$port\n";
# 从服务器读取问候消息
my $server_response = <$client_fh>;
if (defined $server_response) {
print "Server says: $server_response";
} else {
print "No response from server or connection closed.\n";
}
# 示例:向服务器发送消息
print $client_fh "Hi Server, this is Client!\n";
close $client_fh or warn "close failed: $!";
print "Connection closed.\n";

如何运行:

  1. 将服务器代码保存为 server.pl,将客户端代码保存为 client.pl。
  2. 打开一个终端并运行服务器:perl server.pl
  3. 打开另一个终端并运行客户端:perl client.pl

您应该会看到服务器确认连接,客户端打印服务器的问候消息。

此服务器使用 fork() 并发处理多个客户端。对于更高级的场景(非阻塞 I/O、多线程),通常使用 IO::Async、Mojo::IOLoop (来自 Mojolicious) 或 AnyEvent 等模块。

套接字编程可能很复杂。进一步学习的关键领域包括:

  • 错误处理 (Error Handling): 健壮的错误检查至关重要。
  • UDP 套接字 (UDP Sockets): 用于无连接通信。
  • 非阻塞 I/O (Non-blocking I/O): 处理多个连接而无需为每个客户端 fork/创建线程。
  • 序列化 (Serialization): 将复杂数据结构转换为用于网络传输的格式(例如,使用 JSON、Sereal、Storable)。
  • 安全性 (Security): 使用 SSL/TLS 进行加密通信(例如,使用 IO::Socket::SSL)。

查阅 perldoc Socket、perldoc perlipc(用于进程间通信,包括套接字)和 perldoc perlfunc,获取相关函数的详细信息。