作为网络工程师,日常工作大多数时间都是在用telnet同交换机、路由器等网络设备打交道。如果说perl中有什么好用的模块,当然得提Net::Telnet。Net::Telnet功能全面,对于日常管理写个针对几台、几十台交换机的小程序还是不错的选择。Net::Telnet是单线程、阻塞的,如果要处理数百台交换机,只能一台一台交换机的访问,程序运行在堵塞等待上花掉了大量的时间。使用Net::Telnet去探索一个二百多台交换机的网络拓扑,一台交换机耗时几秒,全部遍历完毕要花费30分钟,还要应对Net::Telnet模块中各种error(某台交换机CPU100%卡死、vty进程数太少连接占满等等),一个error die就得推倒重来。费时费力,效果不好。这几年perl没落了,多线程、并发的贴子国内很少,有些即便是提到多线程coro、anyevent之类的,也基本都是http方向的。由于我对TCP协议研究较少,短时间内要完成从socket层面重写telnet难度太高。昨天工作之余发现了简书用户飞天神猫(在此表示感谢)一年前的一篇笔记性文章《Coro-Telnet和Coro-Telnet+Golang-ssh-proxy性能测试》中提到“利用perl io::socket::telnet,封装AE::socket+Coro::handle实现异步telnet登录交互”。作者虽然在其他简书中列出了github地址,但很遗憾他的coro telnet模块代码并没有公布。幸运的是Coro::Socket模块让telnet异步成为了可能:点击(此处)折叠或打开use Coro::Socket; # listen on any other type of socketmy $socket = Coro::Socket->new_from_fh #利用该方法可以直接调用第三方模块                (IO::Socket::UNIX->new(                        Local => "/tmp/socket",                        Type => SOCK_STREAM,                    )                );Coro::Socket能够将IO::Socket模块非阻塞化。而飞天神猫提到的IO::Socket::Telnet恰好继承自IO::Socket::INET。理论上可以在Coro::Socket直接使用IO::Socket::Telnet,就可以实现多线程telnet,经过半天的修改调试结果喜人:对于一个二百多台网络设备的树形拓扑,用Net::Telnet获取邻居信息并根据邻居信息探索拓扑,遍历所有节点耗时30分钟以上;而经过算法重写,并使用多线程coro的telnet仅仅使用20秒时间(当时看到各节点IP飞速的显示到屏幕上,我的内心是相当激动的)。时间有限。先放一个小的异步脚本在这里,供大家参考,后续再完善博文。有几点要提下,IO::Socket::Telnet这个模块仅仅是在IO::Socket::INET模块基础上对telnet协议的字符进行了转义、处理,并不是像Net::Telnet的近似客户端的交互工具。比如CPAN上的例子就是个坑:点击(此处)折叠或打开use IO::Socket::Telnet;my $socket = IO::Socket::Telnet->new(PeerAddr => 'random.server.org');sleep(5); #我添加的defined $socket->recv(my $x, 4096) or die $!; #我添加的while (1) {    $socket->send(scalar >);    sleep(5); #我添加的    defined $socket->recv(my $x, 4096) or die $!;    print $x;}如果按照这个例子(注释掉我添加的语句),你会发现显示并不正常,服务器回传信息会比你的输入滞后一个回车,原因是recv方法接收数据的时间太短。而CPAN上它的分支IO::Socket::Telnet::HalfDuplex,它的作者为了解决这个问题,使用了一个不算可靠的小技巧,这位作者认为recv接收数据的时间不会比使用icmp协议对telnet服务端发送ping的响应时间长,他将recv方法通过“与”逻辑(and语句)与调用ping方法的代码块连接起来,当ping动作完成返回真时recv停止接收数据并显示在标准输出中。这样做对于一般交换机命令来说是可以的,但是当使用类似show int/show run/display current等需要返回长文本信息的命令时就不灵验了。根据这位作者的思路,我利用sleep(5)修改例子,延长接收数据时间5秒钟,发现程序终于像一个正常telnet客户端工具了,这就意味着recv是需要足够的时间接收数据的。如果单纯使用时间来决定telnet下一步操作的话,实现多线程就没有意义了。于是,我开始尝试摸索,替代sleep的方法。根据Net::Telnet源码的思路,作者通过sysread方法循环来实现完整的接收数据。于是,我将recv方法置于循环体中,将接收到的信息通过字符串连接符“.”重新组合,至于如何退出循环体,同样借鉴的Net::Telnet的prompt属性,通过正则表达式判断telnet的提示符,主要包括Username:、Password:、telnet提示符>或、enble(system-view)提示符#或[XXXX](在日常调试过程中,请注意其他需要结束接的收字符,比如提示密码错误的“Bad passwords”等,以防死循环发生),当正则条件满足last退出循环:点击(此处)折叠或打开sub read { #对recv封装,实现serve回传数据的正常交互    my $self = shift;    my $start;    while (1) {        $self->recv(my $x, 4096);            next if !$x;        $start .= $x;        last if $x =~ /(.+>|\[.+\]|.+>|.+#)$/;        $self->send(' ') if $x =~ /More/;        last if $x =~ /(Password: ?|Username: ?)/;    }    return $start;}你会看到有这样一条语句:点击(此处)折叠或打开$self->send(' ') if $x =~ /More/;下面放出一个完成Coro telnet多线程脚本,供参考。其中需要注意的一点是从标准输入中输入命令是自动带换行符的,如果使用字符串传送特定命令是没有换行符的(会发生服务端长时间等待回车符以执行命令,无任何返回信息——程序假死),即使用send方法向telnet服务端发送网络命令需要用双引号(确保换行符转义)结尾带换行符\n :点击(此处)折叠或打开use IO::Socket::Telnet;use strict;use AnyEvent;use Coro;use Coro::AnyEvent;use Coro::Socket;my $start = time;my @host = ("192.168.1.112","192.168.1.113","192.168.1.114","192.168.1.115","192.168.1.116",            "192.168.1.117","192.168.1.118",,"192.168.1.253","192.168.1.119","192.168.1.120","192.168.1.121",            "192.168.1.122","192.168.1.123","192.168.1.124","192.168.1.125","192.168.1.126",            "192.168.1.127","192.168.1.128","192.168.1.129","192.168.1.130","192.168.1.131");my @coro;for my $host (@host) {    push @coro, async {        my $socket = Coro::Socket->new_from_fh                        (IO::Socket::Telnet->new(                                 PeerAddr => $host,                                 Timeout => 5                            )                        );        &doit($host,$socket);        return;    }}foreach (@coro) {    print "joining\n";    $_->join;    print "joined\n";}sub doit {    my $result;    my $host = shift;    #my $socket = IO::Socket::Telnet->new(PeerAddr => $host);    my $socket = shift;    my $s;    eval {$s = &read($socket);}; #IO::Socket::Telnet模块本身缺少异常处理,此处                                 #eval的作用是检测服务器连接超时并报错。考虑到多                                 #线程,不建议调用die中断程序    return print "Can't connect $host\n" if $@;    $result .= $s;    if ($s =~ /Username:/) { #猜测交换机的telnet密码        $socket->send("user\n");        $s = &waitfor($socket,'Password: $');        $result .= $s;        $socket->send("password\n");    } else {        $socket->send("password\n");    }    $s = &waitfor($socket,'.+>$');=pod    $result .= $s;    $socket->send("sh int status\n");    $s = &read($socket);    $result .= $s;=cut       $socket->send("sh cdp nei de\n");    $s = &read($socket);    $result .= $s;     print $result."\n";    =podwhile (1) {    my $buffer;    $socket->send(scalar >);    print &read($socket);}=cut    $socket->close;}print time-$start;sub read { #对recv封装,实现serve回传数据的正常交互    my $self = shift;    my $start;    while (1) {        $self->recv(my $x, 4096);            next if !$x;        $start .= $x;        last if $x =~ /(.+>|\[.+\]|.+>|.+#)$/;        $self->send(' ') if $x =~ /More/;        last if $x =~ /(Password: ?|Username: ?)/;    }    return $start;}sub waitfor { #简单模仿Net::Telnet中的waitfor方法    my $self = shift;    my $regx = shift;    my $start;    while (1) {        $self->recv(my $x, 4096);            next if !$x;        $start .= $x;        last if $x =~ /$regx/;        $self->send(' ') if $x =~ /More/;    }    return $start;}
09-01 08:30