Perl 入门实战:JVM 监控脚本(下)

来源:互联网 发布:新手开淘宝店卖什么好 编辑:程序博客网 时间:2024/06/01 07:32

套接字

使用套接字(Socket)进行网络通信的基本流程是:

  • 服务端:监听端口、等待连接、接收请求、发送应答;
  • 客户端:连接服务端、发送请求、接收应答。
use IO::Socket::INET;my $server = IO::Socket::INET->new(    LocalPort => 10060,    Type => SOCK_STREAM,    Reuse => 1,    Listen => SOMAXCONN) || die "服务创建失败\n";while (my $client = $server->accept()) {    my $line = <$client>;    chomp($line);    if ($line =~ /^JVMPORT ([0-9]+)$/) {        print "RECV $1\n";        print $client "OK\n";    } else {        print "ERROR $line\n";        print $client "ERROR\n";    }    close($client);}close($server);
  • IO::Socket::INET是一个内置模块,::符号用来分隔命名空间。
  • ->new运算符是用来创建一个类的实例的,这涉及到面向对象编程,我们暂且忽略。
  • (key1 => value1, key2 => value2)是用来定义一个哈希表的,也就是键值对。这里是将哈系表作为参数传递给了new函数。请看以下示例。对于哈系表的进一步操作,我们这里暂不详述。
sub hello {    my %params = @_;    print "Hello, $params{'name'}!\n";}hello('name' => 'Jerry'); # 输出 Hello, Jerry!
  • while (...) {...}是另一种循环结构,当圆括号的表达式为真就会执行大括号中的语句。
  • $server->accept()表示调用$server对象的accept()函数,用来接受一个连接。执行这个函数时进程会阻塞(进入睡眠),当有连接过来时才会唤醒,并将该连接赋值给$client变量。
  • <...>运算符表示从文件中读取一行,如:
open my $fd, '<', '/proc/diskstats';while (my $line = <$fd>) {    print $line;}

由于套接字也可以作为文件来看待,所以就能使用<...>运算符。关于open函数和其他文件操作,读者可参考这篇文章。

  • chomp()函数用来将字符串末尾的换行符去掉。它的用法也比较奇特,不是$line = chomp($line),而是chomp($line),这里$line是一次引用传递。
  • 细心的读者会发现,第二句print增加了$client,可以猜到它是用来指定print的输出目标。默认情况下是标准输出。

我们打开两个终端,一个终端执行服务端,另一个终端直接用Bash去调用。

# 客户端$ echo 'JVMPORT 2181' | nc 127.0.0.1 10060OK$ echo 'hello' | nc 127.0.0.1 10060ERROR# 服务端$ ./socket-server.plRECV 2181ERROR hello

至于客户端,还请读者自行完成,可参考相关文档。

子进程

上述代码中有这样一个问题:当客户端建立了连接,但迟迟没有发送内容,那么服务端就会阻塞在$line = <$client>这条语句,无法接收其他请求。有三种解决方案:

  1. 服务端读取信息时采用一定的超时机制,如果3秒内还不能读到完整的一行就断开连接。可惜Perl中并没有提供边界的方法来实现这一机制,需要自行使用IO::Select这样的模块来编写,比较麻烦。
  2. 接受新的连接后打开一个子进程或线程来处理连接,这样就不会因为一个连接挂起而使整个服务不可用。
  3. 使用非阻塞事件机制,当有读写操作时才会去处理。

这里我们使用第二种方案,即打开子进程来处理请求。

use IO::Socket::INET;sub REAPER {    my $pid;    while (($pid = waitpid(-1, 'WNOHANG')) > 0) {        print "SIGCHLD $pid\n";    }}my $interrupted = 0;sub INTERRUPTER {    $interrupted = 1;}$SIG{CHLD} = \&REAPER;$SIG{TERM} = \&INTERRUPTER;$SIG{INT} = \&INTERRUPTER;my $server = ...;while (!$interrupted) {    if (my $client = $server->accept()) {        my $pid = fork();        if ($pid > 0) {            close($client);            print "PID $pid\n";        } elsif ($pid == 0) {            close($server);            my $line = <$client>;            ...            close($client);            exit;        } else {            print "fork()调用失败\n";        }    }}close($server);

我们先看下半部分的代码。系统执行fork()函数后,会将当前进程的所有内容拷贝一份,以新的进程号来运行,即子进程。通过fork()的返回值可以知道当前进程是父进程还是子进程:大于0的是父进程;等于0的是子进程。子进程中的代码做了省略,执行完后直接exit

上半部分的信号处理是做什么用的呢?这就是在多进程模型中需要特别注意的问题:僵尸进程。具体可以参考这篇文章。

$interrupted变量则是用来控制程序是否继续执行的。当进程收到SIGTERMSIGINT信号时,该变量就会置为真,使进程自然退出。

为何不直接使用while (my $client = $server->accept()) {...}呢?因为子进程退出时会向父进程发送SIGCHLD信号,而accept()函数在接收到任何信号后都会中断并返回空,使得while语句退出。

命令行参数

这个服务脚本所监听的端口后是固写在脚本中的,如果想通过命令行指定呢?我们可以使用Perl的内置模块Getopt::Long

use Getopt::Long;use Pod::Usage;my $help = 0;my $port = 10060;GetOptions(    'help|?' => \$help,    'port=i' => \$port) || pod2usage(2);pod2usage(1) if $help;print "PORT $port\n";__END__=head1 NAMEgetopt=head1 SYNOPSISgetopt.pl [options] Options:   -help brief help message   -port bind to tcp port=cut

使用方法是:

$ ./getopt.pl -hUsage:    getopt.pl [options]    ...$ ./getopt.plPORT 10060$ ./getopt.pl -p 12345PORT 12345

'port=i' => \$port表示从命令行中接收名为-port的参数,并将接收到的值转换为整数(i指整数)。\$又是一种引用传递了,这里暂不详述。

至于||运算符,之前在建立$server时也遇到过,它实际上是一种逻辑运算符,表示“或”的关系。这里的作用则是“如果GetOptions返回的值不为真,则程序退出”。

pod2usage(1) if $help表示如果$help为真则执行pod2usage(1)。你也可以写为$help && pod2usage(1)

我们再来看看__END__之后的代码,它是一种Pod文档(Plain Old Documentation),可以是单独的文件,也可以像这样直接附加到Perl脚本末尾。具体格式可以参考perlpod。pod2usage()函数顾名思义是将附加的Pod文档转化成帮助信息显示在控制台上。

小结

完整的脚本可以见这个链接jvm-service.pl。调用该服务的脚本可以见jvm-check.pl。

Perl语言历史悠久,语法丰富,还需多使用、多积累才行。

原创粉丝点击