Chinaunix首页 | 论坛 | 博客
  • 博客访问: 46931
  • 博文数量: 10
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 10
  • 用 户 组: 普通用户
  • 注册时间: 2019-06-30 11:36
文章分类

全部博文(10)

文章存档

2019年(10)

我的朋友

分类: PERL

2019-06-30 11:51:42

作为网络工程师,日常工作大多数时间都是在用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异步成为了可能:



点击(此处)折叠或打开

  1. use Coro::Socket;
  2.  
  3. # listen on any other type of socket
  4. my $socket = Coro::Socket->new_from_fh #利用该方法可以直接调用第三方模块
  5.                 (IO::Socket::UNIX->new(
  6.                         Local => "/tmp/socket",
  7.                         Type => SOCK_STREAM,
  8.                     )
  9.                 );

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上的例子就是个坑:



点击(此处)折叠或打开

  1. use IO::Socket::Telnet;
  2. my $socket = IO::Socket::Telnet->new(PeerAddr => 'random.server.org');

  3. sleep(5); #我添加的
  4. defined $socket->recv(my $x, 4096) or die $!; #我添加的

  5. while (1) {
  6.     $socket->send(scalar <>);

  7.     sleep(5); #我添加的
  8.     defined $socket->recv(my $x, 4096) or die $!;
  9.     print $x;
  10. }

如果按照这个例子(注释掉我添加的语句),你会发现显示并不正常,服务器回传信息会比你的输入滞后一个回车,原因是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退出循环:


点击(此处)折叠或打开

  1. sub read { #对recv封装,实现serve回传数据的正常交互
  2.     my $self = shift;
  3.     my $start;
  4.     while (1) {
  5.         $self->recv(my $x, 4096);
  6.     
  7.         next if !$x;

  8.         $start .= $x;

  9.         last if $x =~ /(<.+>|\[.+\]|.+>|.+#)$/;
  10.         $self->send(' ') if $x =~ /More/;
  11.         last if $x =~ /(Password: ?|Username: ?)/;
  12.     }

  13.     return $start;
  14. }
你会看到有这样一条语句:

点击(此处)折叠或打开

  1. $self->send(' ') if $x =~ /More/;
下面放出一个完成Coro telnet多线程脚本,供参考。其中需要注意的一点是从标准输入中输入命令是自动带换行符的,如果使用字符串传送特定命令是没有换行符的(会发生服务端长时间等待回车符以执行命令,无任何返回信息——程序假死),即使用send方法向telnet服务端发送网络命令需要用双引号(确保换行符转义)结尾带换行符\n :

点击(此处)折叠或打开

  1. use IO::Socket::Telnet;
  2. use strict;
  3. use AnyEvent;
  4. use Coro;
  5. use Coro::AnyEvent;
  6. use Coro::Socket;

  7. my $start = time;

  8. my @host = ("192.168.1.112","192.168.1.113","192.168.1.114","192.168.1.115","192.168.1.116",
  9.             "192.168.1.117","192.168.1.118",,"192.168.1.253","192.168.1.119","192.168.1.120","192.168.1.121",
  10.             "192.168.1.122","192.168.1.123","192.168.1.124","192.168.1.125","192.168.1.126",
  11.             "192.168.1.127","192.168.1.128","192.168.1.129","192.168.1.130","192.168.1.131");
  12. my @coro;


  13. for my $host (@host) {
  14.     push @coro, async {
  15.         my $socket = Coro::Socket->new_from_fh
  16.                         (IO::Socket::Telnet->new(
  17.                                  PeerAddr => $host,
  18.                                  Timeout => 5
  19.                             )
  20.                         );
  21.         &doit($host,$socket);
  22.         return;
  23.     }
  24. }

  25. foreach (@coro) {
  26.     print "joining\n";
  27.     $_->join;
  28.     print "joined\n";
  29. }

  30. sub doit {
  31.     my $result;
  32.     my $host = shift;
  33.     #my $socket = IO::Socket::Telnet->new(PeerAddr => $host);
  34.     my $socket = shift;
  35.     my $s;
  36.     eval {$s = &read($socket);}; #IO::Socket::Telnet模块本身缺少异常处理,此处
  37.                                  #eval的作用是检测服务器连接超时并报错。考虑到多
  38.                                  #线程,不建议调用die中断程序
  39.     return print "Can't connect $host\n" if $@;
  40.     $result .= $s;
  41.     if ($s =~ /Username:/) { #猜测交换机的telnet密码
  42.         $socket->send("user\n");
  43.         $s = &waitfor($socket,'Password: $');
  44.         $result .= $s;
  45.         $socket->send("password\n");
  46.     } else {
  47.         $socket->send("password\n");
  48.     }
  49.     $s = &waitfor($socket,'.+>$');
  50. =pod
  51.     $result .= $s;
  52.     $socket->send("sh int status\n");
  53.     $s = &read($socket);
  54.     $result .= $s;
  55. =cut
  56.    
  57.     $socket->send("sh cdp nei de\n");
  58.     $s = &read($socket);
  59.     $result .= $s;
  60.  
  61.     print $result."\n";
  62.     
  63. =pod
  64. while (1) {
  65.     my $buffer;
  66.     $socket->send(scalar <>);
  67.     print &read($socket);
  68. }
  69. =cut

  70.     $socket->close;
  71. }

  72. print time-$start;

  73. sub read { #对recv封装,实现serve回传数据的正常交互
  74.     my $self = shift;
  75.     my $start;
  76.     while (1) {
  77.         $self->recv(my $x, 4096);
  78.     
  79.         next if !$x;

  80.         $start .= $x;

  81.         last if $x =~ /(<.+>|\[.+\]|.+>|.+#)$/;
  82.         $self->send(' ') if $x =~ /More/;
  83.         last if $x =~ /(Password: ?|Username: ?)/;
  84.     }

  85.     return $start;
  86. }

  87. sub waitfor { #简单模仿Net::Telnet中的waitfor方法
  88.     my $self = shift;
  89.     my $regx = shift;
  90.     my $start;
  91.     while (1) {
  92.         $self->recv(my $x, 4096);
  93.     
  94.         next if !$x;

  95.         $start .= $x;

  96.         last if $x =~ /$regx/;
  97.         $self->send(' ') if $x =~ /More/;
  98.     }

  99.     return $start;
  100. }




阅读(2076) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~