From 5c9a3bb87167f774488ce92ab2f3dd2b4ad7d86a Mon Sep 17 00:00:00 2001 From: "Joshua M. Schmidlkofer" Date: Wed, 8 Apr 2020 19:10:29 -0700 Subject: [PATCH] Merge #25, fix close() calls --- README.md | 11 +++++--- demo.py | 61 +++++++++++++++++++++++++++++-------------- multiping/__init__.py | 57 ++++++++++++++++++++++++++++++---------- 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f92ce86..656520e 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ Here is an example of how to use MultiPing in your own code: responses, no_responses = mp.receive(1) The `receive()` function returns a tuple containing a results dictionary -(addresses and response times) as well as a list of addresses that did not +(addresses with response times and retry) as well as a list of addresses that did not respond in time. The results may be processed like this: ... - for addr, rtt in responses.items(): - print "%s responded in %f seconds" % (addr, rtt) + for addr, result in responses.items(): + print "%s responded in %f seconds (retry=%d)" % (addr, result['time'], result['retry']) if no_responses: print "These addresses did not respond: %s" % ", ".join(no_responses) @@ -102,3 +102,8 @@ surpressed if the `silent_lookup_errors` parameter flag is set. Either as named parameter for the `multi_ping` function or when a `MultiPing` object is created. +To avoid burst issues with packet loss on some networks, the `delay` parameter +can be used with the `multi_ping` function or when a `MultiPing` object is +created. This delay in seconds will be applied between every ICMP request. +For milliseconds delay simply use floating number, e.g.: `0.001` for 1 ms. + diff --git a/demo.py b/demo.py index 30faaef..0e940df 100644 --- a/demo.py +++ b/demo.py @@ -23,46 +23,69 @@ from multiping import multi_ping if __name__ == "__main__": - # A list of addresses and names, which should be pinged. addrs = ["8.8.8.8", "cnn.com", "127.0.0.1", "youtube.com", "2001:4860:4860::8888"] - - print("sending one round of pings and checking twice for responses") + #addrs = ["127.0.0.1", "10.11.1.5","10.12.1.1"] mp = MultiPing(addrs) + + ## + ## Demo Basic Usage + ## + print("Round: 0") + print(" [Ping]") mp.send() - # First attempt: We probably will only have received response from - # localhost so far. Note: 0.01 seconds is usually a very unrealistic - # timeout value. 1 second, or so, may be more useful in a real world - # example. - responses, no_responses = mp.receive(0.01) - print(" received responses: %s" % list(responses.keys())) - print(" no responses so far: %s" % no_responses) + # First attempt: keep recv time very, to show the staggered responses. + print(" Receive() Waiting Replies.......") + responses, no_responses = mp.receive(0.01) - # By now we should have received responses from the others as well - print(" --- trying another receive ---") - responses, no_responses = mp.receive(0.1) - print(" received responses: %s" % list(responses.keys())) - print(" still no responses: %s" % no_responses) - print("") + complete = len(responses.keys()) == len(addrs) + if complete: + print(" received responses: %s" % list(responses.keys())) + print('--- Round Complete ---') + print('') + else: + pending = list(responses.keys()) + print(" received responses: %s" % pending) + print(" no responses so far: %s" % no_responses) + print(" Try Receive() Again.......") + # By now we should have received responses from the others as well + responses, no_responses = mp.receive(0.1) + complete = (len(pending) + len(responses.keys())) == len(addrs) + if complete: + print(" received responses: %s" % list(responses.keys())) + print('--- Round Complete ---') + else: + print(" received responses: %s" % list(responses.keys())) + print(" still no responses: %s" % no_responses) + print("") + pass + print('') # Sometimes ICMP packets can get lost. It's easy to retry (re-send the # ping) to those hosts that haven't responded, yet. Send can be called # multiple times and will result in pings to be resent to those addresses # from which we have not heard back, yet. Here we try three pings max. - print("sending again, but waiting with retries if no response received") + print("Round 1: (3 Attempts)") mp = MultiPing(addrs) + pending = [] for i in range(3): + print(" [Ping]") mp.send() + + print(" Receive() Waiting Replies.......") responses, no_response = mp.receive(0.01) + pending += list(responses.keys()) + complete = len(pending) == len(addrs) print(" received responses: %s" % list(responses.keys())) - if not no_response: - print(" all done, received responses from everyone") + if complete: + print(' [100%]') break else: print(" %d. retry, resending to: %s" % (i + 1, no_response)) + if no_response: print(" no response received in time, even after 3 retries: %s" % no_response) diff --git a/multiping/__init__.py b/multiping/__init__.py index 0f13fb5..9b74d07 100644 --- a/multiping/__init__.py +++ b/multiping/__init__.py @@ -15,7 +15,7 @@ """ -__version__ = "1.1.0" +__version__ = "1.1.3" import os import socket @@ -71,7 +71,8 @@ class MultiPingSocketError(socket.gaierror): class MultiPing(object): - def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False): + def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False, + delay=0): """ Initialize a new multi ping object. This takes the configuration consisting of the list of destination addresses and an optional socket @@ -95,6 +96,7 @@ def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False): "65535 addresses at the same time.") self._ignore_lookup_errors = ignore_lookup_errors + self._xmit_delay = delay # delay between each ICMP request # Get the IP addresses for every specified target: We allow # specification of the ping targets by name, so a name lookup needs to @@ -145,6 +147,7 @@ def __init__(self, dest_addrs, sock=None, ignore_lookup_errors=False): self._unprocessed_targets.append(d) self._id_to_addr = {} + self._addr_retry = {} self._remaining_ids = None self._last_used_id = None self._time_stamp_size = struct.calcsize("d") @@ -242,7 +245,7 @@ def _send_ping(self, dest_addr, payload): # - ICMP code = 0 (unsigned byte) # - checksum = 0 (unsigned short) # - packet id (unsigned short) - # - sequence = 0 (unsigned short) This doesn't have to be 0. + # - sequence = pid (unsigned short) dummy_header = bytearray( struct.pack(_ICMP_HDR_PACK_FORMAT, icmp_echo_request, 0, 0, @@ -292,6 +295,7 @@ def send(self): # Collect all the addresses for which we have not seen responses yet. if not self._receive_has_been_called: all_addrs = self._dest_addrs + self._addr_retry = {addr: -1 for addr in all_addrs} else: all_addrs = [a for (i, a) in list(self._id_to_addr.items()) if i in self._remaining_ids] @@ -303,8 +307,13 @@ def send(self): # need to trim it down. self._last_used_id = int(time.time()) & 0xffff + # Reset the _id_to_addr, we are now retrying to send new request with + # new ID. Reply of a request that have been retried will be ignored. + self._id_to_addr = {} + # Send ICMPecho to all addresses... for addr in all_addrs: + self._addr_retry[addr] += 1 # Make a unique ID, wrapping around at 65535. self._last_used_id = (self._last_used_id + 1) & 0xffff # Remember the address for each ID so we can produce meaningful @@ -314,6 +323,14 @@ def send(self): # of the current time stamp. This is returned to us in the # response and allows us to calculate the 'ping time'. self._send_ping(addr, payload=struct.pack("d", time.time())) + # Some system/network doesn't support the bombarding of ICMP + # request and lead to a lot of packet loss and retry, therefore + # introcude a small delay between each request. + if self._xmit_delay > 0: + time.sleep(self._xmit_delay) + + # Keep track of the current request IDs to be used in the receive + self._remaining_ids = list(self._id_to_addr.keys()) def _read_all_from_socket(self, timeout): """ @@ -337,9 +354,9 @@ def _read_all_from_socket(self, timeout): try: self._sock.settimeout(timeout) while True: - p = self._sock.recv(64) + p, src_addr = self._sock.recvfrom(128) # Store the packet and the current time - pkts.append((bytearray(p), time.time())) + pkts.append((src_addr, bytearray(p), time.time())) # Continue the loop to receive any additional packets that # may have arrived at this point. Changing the socket to # non-blocking (by setting the timeout to 0), so that we'll @@ -366,8 +383,8 @@ def _read_all_from_socket(self, timeout): try: self._sock6.settimeout(timeout) while True: - p = self._sock6.recv(128) - pkts.append((bytearray(p), time.time())) + p, src_addr = self._sock6.recvfrom(128) + pkts.append((src_addr, bytearray(p), time.time())) self._sock6.settimeout(0) except socket.timeout: pass @@ -415,7 +432,7 @@ def receive(self, timeout): start_time = time.time() pkts = self._read_all_from_socket(remaining_time) - for pkt, resp_receive_time in pkts: + for src_addr, pkt, resp_receive_time in pkts: # Extract the ICMP ID of the response try: @@ -438,7 +455,8 @@ def receive(self, timeout): payload = pkt[_ICMP_PAYLOAD_OFFSET:] if pkt_ident == self.ident and \ - pkt_id in self._remaining_ids: + pkt_id in self._remaining_ids and \ + src_addr[0] == self._id_to_addr[pkt_id]: # The sending timestamp was encoded in the echo request # body and is now returned to us in the response. Note # that network byte order doesn't matter here, since we @@ -447,7 +465,8 @@ def receive(self, timeout): req_sent_time = struct.unpack( "d", payload[:self._time_stamp_size])[0] results[self._id_to_addr[pkt_id]] = \ - resp_receive_time - req_sent_time + {'time': resp_receive_time - req_sent_time, + 'retry': self._addr_retry[src_addr[0]]} self._remaining_ids.remove(pkt_id) except IndexError: @@ -470,11 +489,15 @@ def __del__(self): """ Close sockets descriptors. """ - self._sock.close() # TODO: probably need add some verifications. - self._sock6.close() + local = ( + getattr(self, '_sock', None), + getattr(self, '_sock6', None), + ) + [ item.close() for item in local if item != None ] + pass -def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False): +def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False, delay=0): """ Combine send and receive measurement into single function. @@ -492,11 +515,15 @@ def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False): names or looking up their address information will silently be ignored. Those targets simply appear in the 'no_results' return list. + The 'delay' parameter can be used to introduced a small delay between + each requests. + """ retry = int(retry) if retry < 0: retry = 0 + timeout = float(timeout) if timeout < 0.1: raise MultiPingError("Timeout < 0.1 seconds not allowed") @@ -505,7 +532,9 @@ def multi_ping(dest_addrs, timeout, retry=0, ignore_lookup_errors=False): if retry_timeout < 0.1: raise MultiPingError("Time between ping retries < 0.1 seconds") - mp = MultiPing(dest_addrs, ignore_lookup_errors=ignore_lookup_errors) + print(f"Dest: {dest_addrs}") + mp = MultiPing(dest_addrs, ignore_lookup_errors=ignore_lookup_errors, + delay=delay) results = {} retry_count = 0