python tornado框架中的 Happy Eyeballs 算法實現

Happy Eyeballs 算法

"Happy Eyeballs" 算法用於優化ipv4與ipv6的鏈接,許多應用程序在啓動的時候優先選擇ipv6鏈接,若是失敗,再嘗試 ipv4鏈接(fallback),和ipv4對比ipv6的網絡還沒有穩定,啓動ipv6的用戶會比只用ipv4(ipv4-only)有可能經歷更多 的鏈接延遲html

應用程序優化:python

  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函數將地址列表分紅兩類,列表第一個元素做爲第一類地址, 在第一個鏈接失敗以後若是還有額外的地址, 每一類地址都保持一個嘗試鏈接進行三路握手,直到可以成功鏈接爲止app

參考

Happy Eyeballs less

相關文章
相關標籤/搜索