python tornado框架中的 Happy Eyeballs 算法实现

Happy Eyeballs 算法

"Happy Eyeballs" 算法用于优化ipv4与ipv6的连接,许多应用程序在启动的时候优先选择ipv6连接,如果失败,再尝试 ipv4连接(fallback),和ipv4对比ipv6的网络尚未稳定,启动ipv6的用户会比只用ipv4(ipv4-only)有可能经历更多 的连接延迟

应用程序优化:

  1. 优先尝试ipv6连线,若短时间内不陈功,转用ipv4
  2. 避免总是同时尝试ipv6与ipv4
  3. 客户端要避免多余的连接,以免增加网络负载

代码实现

下面是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)

上面算法的策略是:

  1. 首先选用系统getaddrinfo返回的地址列表中的第一个地址尝试连接,成功即返回socket(getaddrinfo通过DNS获得主机ip地址)
  2. 如果第一个地址连接超时或者失败,那么将getaddrinfo返回的地址列表分为primary和second两类,第一个地址作为primary类, 同时开启两类地址的连接(可能为ipv4或者ipv6,因为不同的操作系统实现会根据不同策略,对getaddrinfo返回的地址列表顺序进行优化)
  3. 只要有一个连接成功即停止,返回对应的socket给用户

代码中通过self.split函数将地址列表分成两类,列表第一个元素作为第一类地址, 在第一个连接失败之后如果还有额外的地址, 每一类地址都保持一个尝试连接进行三路握手,直到能够成功连接为止

参考

Happy Eyeballs

转载于:https://my.oschina.net/twistfate33/blog/743501

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值