作为网络工程师,日常工作大多数时间都是在用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 socket
-
my $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";
-
-
=pod
-
while (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;
-
}
阅读(33434) | 评论(0) | 转发(1) |