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