"Happy Eyeballs" 算法用于优化ipv4与ipv6的链接,许多应用程序在启动的时候优先选择ipv6链接,若是失败,再尝试 ipv4链接(fallback),和ipv4对比ipv6的网络还没有稳定,启动ipv6的用户会比只用ipv4(ipv4-only)有可能经历更多 的链接延迟html
应用程序优化:python
下面是tornado中"Happy Eyeballs"实现:算法
class _Connector(object): """A stateless implementation of the "Happy Eyeballs" algorithm. "Happy Eyeballs" is documented in RFC6555 as the recommended practice for when both IPv4 and IPv6 addresses are available. In this implementation, we partition the addresses by family, and make the first connection attempt to whichever address was returned first by ``getaddrinfo``. If that connection fails or times out, we begin a connection in parallel to the first address of the other family. If there are additional failures we retry with other addresses, keeping one connection attempt per family in flight at a time. http://tools.ietf.org/html/rfc6555 """ def __init__(self, addrinfo, io_loop, connect): self.io_loop = io_loop self.connect = connect self.future = Future() self.timeout = None self.last_error = None self.remaining = len(addrinfo) self.primary_addrs, self.secondary_addrs = self.split(addrinfo) @staticmethod def split(addrinfo): """Partition the ``addrinfo`` list by address family. Returns two lists. The first list contains the first entry from ``addrinfo`` and all others with the same family, and the second list contains all other addresses (normally one list will be AF_INET and the other AF_INET6, although non-standard resolvers may return additional families). """ # 将ipv4和ipv6分两个结合 primary = [] secondary = [] primary_af = addrinfo[0][0] for af, addr in addrinfo: if af == primary_af: primary.append((af, addr)) else: secondary.append((af, addr)) return primary, secondary def start(self, timeout=_INITIAL_CONNECT_TIMEOUT): # 优先尝试primary地址,链接成功后经过返回future进行通知 self.try_connect(iter(self.primary_addrs)) # 这里设置超时,超时后会尝试链接sencond地址 self.set_timout(timeout) return self.future def try_connect(self, addrs): try: af, addr = next(addrs) except StopIteration: # We've reached the end of our queue, but the other queue # might still be working. Send a final error on the future # only when both queues are finished. if self.remaining == 0 and not self.future.done(): self.future.set_exception(self.last_error or IOError("connection failed")) return # connect为用户的回调 future = self.connect(af, addr) future.add_done_callback(functools.partial(self.on_connect_done, addrs, af, addr)) def on_connect_done(self, addrs, af, addr, future): self.remaining -= 1 try: stream = future.result() except Exception as e: if self.future.done(): return # Error: try again (but remember what happened so we have an # error to raise in the end) self.last_error = e # 尝试链接下一个地址 self.try_connect(addrs) if self.timeout is not None: # If the first attempt failed, don't wait for the # timeout to try an address from the secondary queue. self.io_loop.remove_timeout(self.timeout) self.on_timeout() return self.clear_timeout() if self.future.done(): # This is a late arrival; just drop it. stream.close() else: self.future.set_result((af, addr, stream)) def set_timout(self, timeout): self.timeout = self.io_loop.add_timeout(self.io_loop.time() + timeout, self.on_timeout) def on_timeout(self): self.timeout = None self.try_connect(iter(self.secondary_addrs)) def clear_timeout(self): if self.timeout is not None: self.io_loop.remove_timeout(self.timeout)
上面算法的策略是:网络
getaddrinfo
返回的地址列表中的第一个地址尝试链接,成功即返回socket(getaddrinfo经过DNS得到主机ip地址)getaddrinfo
返回的地址列表分为primary和second两类,第一个地址做为primary类, 同时开启两类地址的链接(可能为ipv4或者ipv6,由于不一样的操做系统实现会根据不一样策略,对getaddrinfo
返回的地址列表顺序进行优化)代码中经过self.split
函数将地址列表分红两类,列表第一个元素做为第一类地址, 在第一个链接失败以后若是还有额外的地址, 每一类地址都保持一个尝试链接进行三路握手,直到可以成功链接为止app
Happy Eyeballs less