1# happy.py 2# An implementation of RFC 6555 (Happy Eyeballs). 3# See: https://tools.ietf.org/html/rfc6555 4 5from curio import socket, TaskGroup, ignore_after, run 6import itertools 7 8async def open_tcp_stream(hostname, port, delay=0.3): 9 # Get all of the possible targets for a given host/port 10 targets = await socket.getaddrinfo(hostname, port, type=socket.SOCK_STREAM) 11 if not targets: 12 raise OSError(f'nothing known about {hostname}:{port}') 13 14 # Cluster the targets into unique address families (e.g., AF_INET, AF_INET6, etc.) 15 # and make sure the first entries are from a different family. 16 families = [ list(g) for _, g in itertools.groupby(targets, key=lambda t: t[0]) ] 17 targets = [ fam.pop(0) for fam in families ] 18 targets.extend(itertools.chain(*families)) 19 20 # List of accumulated errors to report in case of total failure 21 errors = [] 22 23 # Task group to manage a collection concurrent tasks. 24 # Cancels all remaining once an interesting result is returned. 25 async with TaskGroup(wait=object) as group: 26 27 # Attempt to make a connection request 28 async def try_connect(sockargs, addr, errors): 29 sock = socket.socket(*sockargs) 30 try: 31 await sock.connect(addr) 32 return sock 33 except Exception as e: 34 await sock.close() 35 errors.append(e) 36 37 # Walk the list of targets and try connections with a staggered delay 38 for *sockargs, _, addr in targets: 39 await group.spawn(try_connect, sockargs, addr, errors) 40 async with ignore_after(delay): 41 task = await group.next_done() 42 if not task.exception: 43 group.completed = task 44 break 45 46 if group.completed: 47 return group.completed.result 48 else: 49 raise OSError(errors) 50 51 52async def main(): 53 result = await open_tcp_stream('www.python.org', 80) 54 print(result) 55 56if __name__ == '__main__': 57 run(main) 58 59 60 61 62 63 64 65 66 67