1

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).

pepoluan
  • 6,132
  • 4
  • 46
  • 76
  • There exists approaches that make use of [non-blocking sockets](https://stackoverflow.com/questions/20476555/non-blocking-connect) which makes the use of threading irrelevant. If this is the case please update the question so it may be reopened (or be closed with an actual alternative question thread that implement this). – metatoaster Feb 13 '19 at 05:48
  • @metatoaster I've edited my question to better descibe the need and why the linked answer to "non-blocking sockets" might not be applicable. – pepoluan Feb 13 '19 at 05:59
  • One way is to use the async programming facilities of Python 3, I've seen a nice demo with `trio`, with incremental backoff – Antti Haapala -- Слава Україні Feb 13 '19 at 07:57

1 Answers1

2

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.

metatoaster
  • 17,419
  • 5
  • 55
  • 66
  • Awesome answer, thanks! I didn't know that `socket.setblocking` is actually applicable to `connect` as well... whoa, I'm learning something new everyday! – pepoluan Feb 23 '19 at 06:04
  • Also, in my case I have to add some more `errno.*` values there as the 'acceptable' values, because my program basically needs to wait for some endpoints to be ready, so a simple "cannot connect to that IP:port" is expected... that's why in my question I added the `timeout` parameter. But your answer sent me down the right path. Again, thank you very much! – pepoluan Feb 23 '19 at 06:06