--- AnyEvent-FastPing/FastPing.pm 2011/02/01 04:06:24 1.12 +++ AnyEvent-FastPing/FastPing.pm 2011/02/02 19:26:45 1.13 @@ -47,213 +47,278 @@ =item AnyEvent::FastPing::ipv4_supported -Returns true if IPv4 is supported in this module and on this system. +Returns true iff IPv4 is supported in this module and on this system. =item AnyEvent::FastPing::ipv6_supported -Returns true if IPv6 is supported in this module and on this system. +Returns true iff IPv6 is supported in this module and on this system. =item AnyEvent::FastPing::icmp4_pktsize -Returns the number of bytes each IPv4 ping packet has. +Returns the number of octets per IPv4 ping packet (the whole IP packet +including headers, excluding lower-level headers or trailers such as +Ethernet). -=item AnyEvent::FastPing::icmp6_pktsize - -Returns the number of bytes each IPv4 ping packet has. +Can be used to calculate e.g. octets/s from rate ... -=cut + my $octets_per_second = $packets_per_second * AnyEvent::FastPing::icmp4_pktsize; -sub new { - my ($klass) = @_; +... or convert kilobit/second to packet rate ... - _new $klass, (rand 65536), (rand 65536), (rand 65536) -} + my $packets_per_second = $kilobit_per_second + * (1000 / 8 / AnyEvent::FastPing::icmp4_pktsize); -our @IDLE_CB; +etc. -sub DESTROY { - undef $IDLE_CB[ &id ]; - &_free; -} +=item AnyEvent::FastPing::icmp6_pktsize -sub on_idle { - $IDLE_CB[ &id ] = $_[1]; -} +Like AnyEvent::FastPing::icmp4_pktsize, but for IPv6. -our $THR_RES_W = AE::io $THR_RES_FH, 0, sub { - sysread $THR_RES_FH, my $buf, 8; +=back - for my $id (unpack "S*", $buf) { - _stop_id $id; - ($IDLE_CB[$id] || sub { })->(); - } -}; +=head1 THE AnyEvent::FastPing CLASS -for(1..10) { -my $p = new AnyEvent::FastPing;#d# -$p->interval (0); -$p->max_rtt (0.5); -#$p->add_range (v127.0.0.1, v127.255.255.254, 0); -$p->add_range (v127.0.0.1, v127.0.0.1, 0); -#$p->add_range (v1.0.0.1, v1.255.255.254, 0); -$p->on_idle (my $cv = AE::cv); -my $cnt; -$p->on_recv (sub { - $cnt++; -}); -$p->start; - -{ - my $p = new AnyEvent::FastPing;#d# - $p->interval (0); - $p->max_rtt (0.5); - $p->add_hosts ([v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2, (v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1)x8, v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.3], 0); - my $cnt; - $p->on_recv (sub { - use Data::Dump; ddx \@_; - }); - $p->on_idle (sub { - undef $p; - }); - $p->start; -} +The AnyEvent::FastPing class represents a single "pinger". A "pinger" +comes with its own thread to send packets in the background, a rate-limit +machinery and separate idle/receive callbacks. -$cv->recv; -warn $cnt; -} +The recommended workflow (there are others) is this: 1. create a new +AnyEvent::FastPing object 2. configure the address lists and ranges to +ping, also configure an idle callback and optionally a receive callback +3. C the pinger. -=item AnyEvent::FastPing::icmp_ping [ranges...], $send_interval, $payload, \&callback +When the pinger has finished pinging all the configured addresses it will +call the idle callback. -Ping the given IPv4 address ranges. Each range is an arrayref of the -form C<[lo, hi, interval]>, where C and C are octet strings with -either 4 octets (for IPv4 addresses) or 16 octets (for IPV6 addresses), -representing the lowest and highest address to ping (you can convert a -dotted-quad IPv4 address to this format by using C. The -range C is the minimum time in seconds between pings to the -given range. If omitted, defaults to C<$send_interval>. - -The C<$send_interval> is the minimum interval between sending any two -packets and is a way to make an overall rate limit. If omitted, pings will -be sent as fast as possible. - -The C<$payload> is a 32 bit unsigned integer given as the ICMP ECHO -REQUEST ident and sequence numbers (in unspecified order :). - -The request will be queued and all requests will be served by a background -thread in order. When all ranges have been pinged, the C will be -called. - -Algorithm: Each range has an associated "next time to send packet" -time. The algorithm loops as long as there are ranges with hosts to be -pinged and always serves the range with the most urgent packet send -time. It will at most send one packet every C<$send_interval> seconds. - -This will ensure that pings to the same range are nicely interleaved with -other ranges - this can help reduce per-subnet bandwidth while maintaining -an overall high packet rate. +The pinging process works like this: every range has a minimum interval +between sends, which is used to limit the rate at which hosts in that +range are being pinged. Distinct ranges are independent of each other, +which is why there is a per-pinger "global" minimum interval as well. -The algorithm to send each packet is O(log n) on the number of ranges, so -even a large number of ranges (many thousands) is managable. +The pinger sends pings as fats as possible, while both obeying the pinger +rate limit as well as range limits. -No storage is allocated per address. +When a range is exhausted, it is removed. When all ranges are exhausted, +the pinger waits another C seconds and then exits, causing the +idle callback to trigger. Performance: On my 2 GHz Opteron system with a pretty average nvidia gigabit network card I can ping around 60k to 200k adresses per second, depending on routing decisions. Example: ping 10.0.0.1-10.0.0.15 with at most 100 packets/s, and -11.0.0.1-11.0.255.255 with at most 1000 packets/s. Do not, however, exceed -1000 packets/s overall: +11.0.0.1-11.0.255.255 with at most 1000 packets/s. Also ping the IPv6 +loopback address 5 times as fast as possible. Do not, however, exceed 1000 +packets/s overall. Dump each received reply: + + use AnyEvent::Socket; + use AnyEvent::FastPing; my $done = AnyEvent->condvar; - AnyEvent::FastPing::icmp_ping - [ - [v10.0.0.1, v10.0.0.15, .01], - [v11.0.0.1, v11.0.255.255, .001], - ], - .001, 0x12345678, - sub { - warn "all ranges pinged\n"; - $done->broadcast; + my $pinger = new AnyEvent::FastPing; + + $pinger->interval (1/1000); + $pinger->max_rtt (0.1); # reasonably fast/reliable network + + $pinger->add_range (v10.0.0.1, v10.0.0.15, 1/100); + $pinger->add_range (v11.0.0.1, v11.0.255.255, 1/1000); + $pinger->add_hosts ([ (v0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1) x 5 ]); + + $pinger->on_recv (sub { + for (@{ $_[0] }) { + printf "%s %g\n", (AnyEvent::Socket::format_address $_->[0]), $_->[1]; } - ; + }); + $pinger->on_idle (sub { + print "done\n"; + undef $pinger; + }); + + $pinger->start; $done->wait; +=head2 METHODS + +=over 4 + +=item $pinger = new AnyEvent::FastPing + +Creates a new pinger - right now there can be at most C<65536> pingers in +a process, although that limit might change to somethind drastically lower +- you should be stringy with your pinger objects. + =cut -sub icmp_ping($$$&) { -# _send_req _req_icmp_ping @_; +sub new { + my ($klass) = @_; + + _new $klass, (rand 65536), (rand 65536), (rand 65536) +} + +our @IDLE_CB; + +sub DESTROY { + undef $IDLE_CB[ &id ]; + &_free; } -=item AnyEvent::FastPing::register_cb \&cb +=item $pinger->on_recv ($callback->([[$host, $rtt], ...])) -Register a callback that is called for every received ping reply -(regardless of whether a ping is still in process or not and regardless of -whether the reply is actually a reply to a ping sent earlier). - -The code reference gets a single parameter - an arrayref with an -entry for each received packet (replies are being batched for greater -efficiency). Each packet is represented by an arrayref with three members: -the source address (an octet string of either 4 (IPv4) or 16 (IPv6) octets -length), the payload as passed to C and the round trip time in -seconds. - -Example: register a callback which simply dumps the received data. Since -the coderef is created on the fly via sub, it would be hard to unregister -this callback again :) - - AnyEvent::FastPing::register_cb sub { - for (@{$_[0]}) { - printf "%s %d %g\n", - (4 == length $_->[0] ? inet_ntoa $_->[0] : Socket6::inet_ntop (&AF_INET6, $_->[0])), - $_->[2], - $_->[1]; +Registeres a callback to be called for ping replies. If no callback has +been registered than ping replies will be ignored, otherwise this module +calculates the round trip time, in seconds, for each reply and calls this +callback. + +The callback receives a single argument, which is an array reference +with an entry for each reply packet (the replies will be batched for +efficiency). Each member in the array reference is again an array +reference with exactly two members: the binary host addresss (4 octets for +IPv4, 16 for IPv6) and the approximate round trip time, in seconds. + +The replies will be passed to the callback as soon as they arrive, and +this callback can be called many times with batches of replies. + +The receive callback will be called whenever a suitable reply arrives, +whether generated by this pinger or not, whether this pinger is started +or not. The packets will have a unique 64 bit ID to distinguish them from +other pinger objects and other generators, but this doesn't help against +malicious replies. + +Note that very high packet rates can overwhelm your process, causing +replies to be dropped (configure your kernel with long receive queues for +raw sockets if this is a problem). + +Example: register a callback which simply dumps the received data. + + use AnyEvent::Socket; + + $pinger->on_recv (sub { + for (@{ $_[0] }) { + printf "%s %g\n", (AnyEvent::Socket::format_address $_->[0]), $_->[1]; } }; Example: a single ping reply with payload of 1 from C<::1> gets passed like this: - [ [ - "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1", - "0.000280141830444336", - 1 - ] ] + [ + [ "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\1", 0.000280141830444336 ] + ] -Example: ping replies for C<127.0.0.1> and C<127.0.0.2>, with a payload of -C<0x12345678>: +Example: ping replies for C<127.0.0.1> and C<127.0.0.2>: [ - [ - "\177\0\0\1", - "0.00015711784362793", - 305419896 - ], - [ - "\177\0\0\2", - "0.00090184211731", - 305419896 - ] + [ "\177\0\0\1", 0.00015711784362793 ], + [ "\177\0\0\2", 0.00090184211731 ] ] -=item AnyEvent::FastPing::unregister_cb \&cb +=item $pinger->on_idle ($callback->()) -Unregister the callback again (make sure you pass the same codereference -as to C). +Registers a callback to be called when the pinger becomes I, that +is, it has been started, has exhausted all ping ranges and waited for +the C time. An idle pinger is also stopped, so the callback can +instantly add new ranges, if it so desires. =cut -our @CB; - -sub register_cb($) { - push @CB, $_[0]; +sub on_idle { + $IDLE_CB[ &id ] = $_[1]; } -sub unregister_cb($) { - @CB = grep $_ != $_[0], @CB; -} +our $THR_RES_W = AE::io $THR_RES_FH, 0, sub { + sysread $THR_RES_FH, my $buf, 8; + + for my $id (unpack "S*", $buf) { + _stop_id $id; + ($IDLE_CB[$id] || sub { })->(); + } +}; + +=item $pinger->interval ($seconds) + +Configures the minimum interval between packet sends for this pinger - the +pinger will not send packets faster than this rate (or atcually 1 / rate), +even if individual ranges have a lower interval. + +A value of C<0> selects the fastests possible speed (currently no faster +than 1_000_000 packets/s). + +=item $pinger->max_rtt ($seconds) + +If your idle callback were called instantly after all ranges were +exhausted and you destroyed the object inside (which is common), then +there would be no chance to receive some replies, as there would be no +time fo the packet to travel over the network. + +This can be fixed by starting a timer in the idle callback, or more simply +by selecting a suitable C value, which should be the maximum time +you allow a ping packet to travel to its destinazion and back. + +The pinger thread automatically waits for this amount of time before becoming idle. + +The default is currently C<0.5> seconds, which is usually plenty. + +=item $pinger->add_range ($lo, $hi[, $interval]) + +Ping the IPv4 (or IPv6, but see below) address range, starting at binary +address C<$lo> and ending at C<$hi> (both C<$lo> and C<$hi> will be +pinged), generating no more than one ping per C<$interval> seconds (or as +fast as possible if omitted). + +You can convert IP addresses from text to binary form by +using C, C, +C or any other method that you like :) + +The algorithm to select the next address is O(log n) on the number of +ranges, so even a large number of ranges (many thousands) is managable. + +No storage is allocated per address. + +Note that, while IPv6 addresses are currently supported, the usefulness of +this option is extremely limited and might be gone in future versions - if +you want to ping a number of IPv6 hosts, better specify them individually +using the C method. + +=item $pinger->add_hosts ([$host...], $interval, $interleave) + +Similar to C, but uses a list of single addresses instead. The +list is specified as an array reference as first argument. Each entry in +the array should be a binary host address, either IPv4 or IPv6. Currently, +all entries in the list must be either IPv4 B IPv6, so you have to +create two host ranges if you have mixed addresses. + +Minimum C<$interval> is the same as for C and can be left out. + +C<$interlave> specifies an increment between addresses: often address +lists are generated in a way that results in clustering - first all +addresses from one subnet, then from the next, and so on. To avoid this, +you can specify an interleave factor. If it is C<1> (the default), then +every address is pinged in the order specified. If it is C<2>, then only +every second address will be pinged in the first round, followed by a +second round with the others. Higher factors will create C<$interleave> +runs of addresses spaced C<$interleave> indices in the list. + +The special value C<0> selects a (hopefully) suitable interleave factor +automatically - currently C<256> for lists with less than 65536 addresses, +and the square root of the list length otherwise. + +=item $pinger->start + +Start the pinger, unless it is running already. While a pinger is running +you must not modify the pinger. If you want to change a parameter, you +have to C the pinger first. + +The pinger will automatically stop when destroyed. + +=item $pinger->stop + +Stop the pinger, if it is running. A pinger can be stopped at any time, +after which it's current state is preserved - starting it again will +continue where it left off. + +=cut 1;