pepoluan
pepoluan

Reputation: 6780

Attempt to connect to several IP addresses and get the first one that succeeds

Is there a way using socket to attempt connection to several IP addresses and get the first one that succeeds?

A simplifed concept is this:

targets = ['1.2.3.4', '2.3.4.5', '3.4.5.6']
try:
    s = socket_try_connect_one(targets, port=80, timeout=300)
    # at this point, s contains the socket connection to one of the targets
except TimeoutError:
    print('Cannot connect to any servers!')
    sys.exit(1)

How should I implement socket_try_connect_one so I get the expected behavior? Is there a built-in method (in the standard libs) to do this?


Edited to add:

I'm avoiding multithreading/multiprocessing because that might be overkill for this simple need.

Rather, I'm looking for something similar to .setblocking(0), but applied during connection establishment (setblocking applies to send() and recv()). And something similar to select.select but applicable to connection establishment (instead of triggering on I/O).

Upvotes: 1

Views: 511

Answers (1)

metatoaster
metatoaster

Reputation: 18908

So borrowing roughly from the standard library socket.create_connection function, which does connection to multiple address/port pairs for each IP resolved for any given hostname, with the connection done using blocking sockets following the sequence of IP addresses as returned by DNS. Adapting that to accept multiple raw IP addresses and make use of non-blocking sockets can be done roughly with the following function:

import socket
import errno


def make_socket_from_addresses(addresses, port, *args, **kwargs):
    sockets = {}  # mapping of the actively checked sockets
    dests = []   # the list of all destination pairs
    for address in addresses:
        dest = (address, port)
        sock = socket.socket(*args, **kwargs)
        sock.setblocking(0)
        sockets[dest] = sock
        dests.append(dest)

    result = None
    while sockets and result is None:
        for dest in dests:
            sock = sockets.get(dest)
            if not sock:
                continue
            code = sock.connect_ex(dest)
            if code == 0:
                # success, track the result.
                result = sock
                sockets.pop(dest)
                break
            elif code not in (errno.EALREADY, errno.EINPROGRESS):
                # assume any not already/in-progress are invalid/dead and so
                # prune them.
                sockets.pop(dest)
        # should insert some very minute timeout here

    for _, sock in sockets.items():
        # close any remaining sockets
        sock.close()

    return result                                                               

To make use of this function, a list of addresses to check and a port must be supplied, along with the relevant arguments to the socket.socket constructor. For example:

# various google.com addresses
addresses = ['216.58.203.110', '172.217.25.46', '172.217.6.78']
port = 80
sock = make_socket_from_addresses(
    addresses, port, socket.AF_INET, socket.SOCK_STREAM)
print(sock)

Running that:

<socket.socket fd=3, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('10.0.0.1', 59454), raddr=('172.217.25.46', 80)>                            

Usage of select may be beneficial, the example function provided serves only as an illustration on how the non-blocking loop might look like and how it should work.

Upvotes: 2

Related Questions