Android 网络性能优化-复用连接池和弱网优化

1. 概述

复用连接池是一个优化连接的技术,在Android中,功能强大的OkHttp已经帮我们实现了这个技术,使我们不用再担心多请求时的性能低下。虽然已经帮咱实现了,但是我们可以学习学习(所以该篇比较简单),顺便可以重温下OkHttp的源码。

该篇承接 Android 网络性能优化DNS优化,在拿到服务器ip后,我们客户端和服务端需要建立Socket,走Tcp的三次握手,在请求完成后通过四次分手关闭Socket。下图为三次握手流程:

在这里插入图片描述

TCP“三次握手” 这个问题的本质是, 信道不可靠, 但是通信双发需要就某个问题达成一致. 而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值. 所以三次握手不是TCP本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的. 请注意这里的本质需求,信道不可靠, 数据传输要可靠. 三次达到了, 那后面你想接着握手也好, 发数据也好, 跟进行可靠信息传输的需求就没关系了. 因此,如果信道是可靠的, 即无论什么时候发出消息, 对方一定能收到, 或者你不关心是否要保证对方收到你的消息, 那就能像UDP那样直接发送消息就可以了.”。这可视为对“三次握手”目的的另一种解答思路。

这段话意思就是如果想确定双通道通畅,必须使用三个包发送接收,也就是三次握手

如果程序产生了频繁的、数量较多的 网络请求,大量的连接每次都要握手和分手,必然会造成性能低下。 

 Http有一种叫做keep-alive connections的机制,在我们Http的请求中,会看到有下面个meta-daya:

在这里插入图片描述
它的作用是可以在传输数据后仍然保持连接,当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手。

在这里插入图片描述

 这也是比较常规的实现长连接的做法。
在Http1.0中,该选项默认为False,而在Http1.1中,Keep-Alive 是默认开启的。OkHttp默认实现5个并发的Keepalive connection,默认的连接keep时长为5分钟。

2. 连接池

连接池,就是请求通过复用存在的连接,达到节省开辟新连接所需开销的结果。这也是一种设计模式,是一种浅学设计模式之享元模式。

因为连接的场景有多种(Spdy、SSL、WebSocket等),所以Socket的种类也有多种,连接池的分类可以参照下图:

在这里插入图片描述

 上图展示了连接池的多种不同类型,可以看到最根基的的TCPSocket连接,分别来看下每个连接池的作用:

1. SSL连接池
        管理SSLSocket,但SSLSocket又依赖于TCP连接池提供的TCPSocket
2. HTTP代理连接池
        如果走HTTP协议,那么就需要TCP连接池提供TCPSocket,如果走HTTPS协议,那么就需要SSL连接池提供SSLSocket;
3. SpdySession池
        依赖SSL连接池提供SSLSocket,这里需要说明下,虽然HTTP/2协议没有强制绑定HTTPS,但是在实际开发中确实都是绑定HTTPS
4. SOCKS连接池
        管理的SOCKSSocket和SOCKS5Socket都需要依赖TCP连接池提供的TCPSocket
5. WebSocket连接池
        依赖TCP连接池提供的TCPSocket,声明下这里没有说明WSS(Web Socket Secure)的情况
 

3. 源码实现

这里参考的是OkHttp4的代码

3.1 ConnectionPool类

连接池的类位于okhttp3.ConnectionPool。我们需要了解到如何在timeout时间内复用connection,并且有效的对其进行回收清理操作。我们先来看看该类的作用,因为有文档注释,我们来看看官方是如何描述该类的

在这里插入图片描述 翻译:该类管理 Http/Http2 的连接复用,用来减少网络的消耗。有着相同ip地址的Http请求可以共享一个连接通道。该类实现了一种长连接的策略。
构造函数创造了一个新的连接池和附带参数,这些参数可能会在未来的OkHttp版本中被更改(也就是说不建议我们直接使用)。目前这个连接池可以最多同时持有5个闲置的连接,如果有多的连接,将会被移除掉。

// ConnectionPool.kt
class ConnectionPool internal constructor(
  internal val delegate: RealConnectionPool
) {
  constructor(
    maxIdleConnections: Int,
    keepAliveDuration: Long,
    timeUnit: TimeUnit
  ) : this(RealConnectionPool(
      taskRunner = TaskRunner.INSTANCE,
      maxIdleConnections = maxIdleConnections,
      keepAliveDuration = keepAliveDuration,
      timeUnit = timeUnit
  ))

  // 1
  constructor() : this(5, 5, TimeUnit.MINUTES)
  ..
}

这是构造函数,注释1中可以看出,默认就的最多闲置连接是5个,保持时间是5分钟,taskRunner是一个线程管理器,用来检测闲置socket并对其进行清理,在3.x版本中,它是一个Executor。然后这个类就没别的东西了,其他的都在它的父类RealConnectionPool里面了

3.2 RealConnectionPool的缓存操作

RealConnectionPool是真正的连接池,ConnectionPool是其子类,他除了刚刚那几个子类传来的参数之外,还有一个很重要的参数:

  /**
   * 使用线程安全的双向队列来管理所有的 [RealConnection]---Socket连接
   */
  private val connections = ConcurrentLinkedQueue<RealConnection>()

连接池可以通过 connections来管理连接的添加、删除、复用。

3.2.1 put操作
 

  fun put(connection: RealConnection) {
    connection.assertThreadHoldsLock()

    // 1
    connections.add(connection)
    // 2
    cleanupQueue.schedule(cleanupTask)
  }

注释1: 在连接池connections中添加一个连接。
注释2: 需要整理一遍connections里的连接,比如说多出来的连接需要删除掉,超过保持时长的连接要去掉。

3.2.2 判断连接是否可以复用

在3.x版本,该类提供了一个方法来返回一个可复用的连接,主要逻辑是遍历connections的所有连接,判断是否有连接可复用。而4.x的版本稍微的更改逻辑,先来看下这个方法:

  // 1
  fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?,
    requireMultiplexed: Boolean
  ): Boolean {
    for (connection in connections) {
      synchronized(connection) {
        // 2
        if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
        // 3
        if (!connection.isEligible(address, routes)) return@synchronized
        // 4
        call.acquireConnectionNoEvents(connection)
        return true
      }
    }
    return false
  }

注释1: 传入一个ip地址,该方法就是判断是否已经存在该ip打通的socket,如果有返回true,说明可以复用,否则返回false
注释2: 判断连接的多路复用,这个属性是给Http2用的
注释3: 检查ip地址和路由列表是否合法
注释4: 调用 RealCall.acquireConnectionNoEvents()方法,将RealCall的 connection指向该连接,表明存在可以复用的连接,并且返回true。那么调用者就可以通过它的RealCall来获取到复用的连接了。

可以看下 RealCall的方法:

// RealCall.kt
  fun acquireConnectionNoEvents(connection: RealConnection) {
    connection.assertThreadHoldsLock()

    check(this.connection == null)
    this.connection = connection
    connection.calls.add(CallReference(this, callStackTrace))
  }


3.2.3 清除和回收连接

在刚刚put方法里面,我们看到了该类会实现一个方法来check连接池里的连接,它的作用是清除和回收超时和多出来的连接,我们来看看这个方法,因为方法比较长,所以分成两个部分来看,下面是上半部分:

// RealConnectionPool.kt
  /**
   * 作用是维护连接池,删除那些超时的连接、或者超出最大数量限制的连接
   * 返回的值是睡眠到下次执行该方法的时间,
   * 如果不需要进一步清理,则返回-1
   */
  fun cleanup(now: Long): Long {
    var inUseConnectionCount = 0
    var idleConnectionCount = 0
    var longestIdleConnection: RealConnection? = null
    var longestIdleDurationNs = Long.MIN_VALUE

    // 1
    for (connection in connections) {
      synchronized(connection) {
        // 2
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++
        } else {
          idleConnectionCount++

          // 3
          val idleDurationNs = now - connection.idleAtNs
          if (idleDurationNs > longestIdleDurationNs) {
            longestIdleDurationNs = idleDurationNs
            longestIdleConnection = connection
          } else {
            Unit
          }
        }
      }
    }
    ...
  }

注释1: 遍历连接池内的所有连接
注释2: 调用 pruneAndGetAllocationCount()方法,查看该连接是否正在被使用。如果正在使用,则工作连接+1,否则 闲置连接+1
注释3: 计算该连接的闲置时间。遍历一圈,记录下闲置时间最久的连接。

再来看下cleanup()的下半部分:

// RealConnectionPool.kt
....
when {
      // 1
      longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections -> {
        val connection = longestIdleConnection!!
        synchronized(connection) {
          if (connection.calls.isNotEmpty()) return 0L // No longer idle.
          if (connection.idleAtNs + longestIdleDurationNs != now) return 0L // No longer oldest.
          connection.noNewExchanges = true
          (longestIdleConnection)
        }

        connection.socket().closeQuietly()
        if (connections.isEmpty()) cleanupQueue.cancelAll()

        // Clean up again immediately.
        return 0L
      }

      // 2
      idleConnectionCount > 0 -> {
        return keepAliveDurationNs - longestIdleDurationNs
      }
    
      // 3
      inUseConnectionCount > 0 -> {
        return keepAliveDurationNs
      }
   
      // 4
      else -> {
        return -1
      }
    }

这里是根据上半部分的统计结果进行处理:
注释1:闲置最久的连接时间已经超过5分钟或者当前空闲的连接数超过了5个,则通过 connections.remove() 和 connection.socket().closeQuietly() 移除掉闲置最久的连接,
注释2:当前存在闲置连接,则返回 闲置最久的连接还需要等待多少时间就到5分钟 的时间间隔
注释3:当前没有闲置连接,有工作连接, 则返回 5分钟
注释4:既没有工作连接又没有闲置连接,返回-1

这个方法主要就是通过计算有无超时的限制连接或则超过容量的连接进行删除,其中它使用了一个方法 pruneAndGetAllocationCount()来查看一个连接是否正在被使用,我们可以看看这个方法的逻辑。

3.2.4 查看连接是否闲置

// RealConnectionPool.kt
 /**
  * 删除所有的发生泄漏的回调,然后返回[Connection]剩余的实时的被调用的数量 
  * 
  * 如果一个回调正在被引用但是实际上已经被代码不使用他们了,这个回调就是泄漏的,
  * 这种泄漏检测是不靠谱的,而且依赖于 GC回收
  */
  private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
    connection.assertThreadHoldsLock()
    // 1
    val references = connection.calls
    var i = 0
    
    // 2
    while (i < references.size) {
      val reference = references[i]
      // 3
      if (reference.get() != null) {
        i++
        continue
      }

      // 4
      val callReference = reference as CallReference
      val message = "A connection to ${connection.route().address.url} was leaked. " +
          "Did you forget to close a response body?"
      Platform.get().logCloseableLeak(message, callReference.callStackTrace)
      // 5
      references.removeAt(i)
      connection.noNewExchanges = true
      
      if (references.isEmpty()) {
        connection.idleAtNs = now - keepAliveDurationNs
        return 0
      }
    }

    return references.size
  }

注释1:获取 connection的所有存储的 Reference<RealCall>,也就是Call引用
注释2:遍历这些Call
注释3:如果Call通过 .get()获取不为null,那么说明它正在被使用,则记录并continue,否则说明这个call已经被清除了,但是由于在列表中所有没有回收掉。
注释4: 抛出一个泄漏的Log,提醒开发者有没有遗漏close一个连接。(所以这里提示我们在使用完一个socket后,需要关闭到RealCall,否则我们就复用不了连接池)
注释5:Call列表移除掉这个泄漏的Call。

总的来说,pruneAndGetAllocationCount()这个方法就是通过检查 Reference来查看引用数,判断一个连接是否被call引用,如果引用,就说明这个连接是一个正在工作中的连接,否则就是一个闲置的连接。

3.3 OkHttp中的使用

我们已经知道 RealConnectionPool、ConnectionPool() 是如何复用连接池了,那么我们来看看它是在什么时候运用在代码中的吧。

在 OkHttp的大管家 OkHttpClient的构造方法中,就发现了连接池的实例化:

// OkHttpClient
 class Builder constructor() {
    internal var dispatcher: Dispatcher = Dispatcher()
    internal var connectionPool: ConnectionPool = ConnectionPool()
    ....


所以我们平时使用OkHttp的时候,就默认使用了这个 ConnectionPool了。

4. 总结
复用连接池可以减少多个网络请求下的连接建立的消耗,而且OkHttp已经默认帮我们实现了这些功能,我们只需要注意几点:

复用连接的前提是同ip通道,如果每个请求都发送给不同的ip,那么连接池也复用不了
最好养成习惯,在非Http2的使用完连接后(就是Http请求完成后),我们需要手动 关闭RealCall,否则下层代码就要通过触发GC回收来帮我们检查和关闭。

参考文章

Android网络编程(八)源码解析OkHttp后篇[复用连接池]
百度APP移动端网络深度优化实践分享(二):网络连接优化篇
okhttp连接池复用机制


弱网优化

1. 背景

移动端时段,手机网络的连接形态是无线的,即通过Wifi连接,在前面章节有提高过,无线连接的优点就是便捷,只要有信号就能上网。而它的缺点是不稳定,它没有像导线那样的抗干扰手段,而且离信号越远网络越差。手机用户几乎都能体验到网络不好的情况的。

而对于移动端开发来说,在网络不好的情况下进行交互,如果处理不好,会消耗宽带、浪费电量等资源问题,所以我们有必要解决弱网环境下会出现的问题。弱网优化需要解决的核心问题有两点:

  1. 移动网络环境如此复杂,我们如何确定当下就是弱网环境?
  2. 如果是弱网环境,我们应该如何提升弱网环境下的业务成功率?

2. 弱网

2.1 什么是弱网

弱网并没有明确的定义,所以这需要我们根据实际场景来建立一套标准。一般来说有两个角度,即大指标:

  1. 丢包率
  2. 网络延时

上述两个都好理解,网络太复杂,造成丢包率高和网络延时高的原因太多了,比如:

  • 网络拥塞状况下, 服务器处理的策略是丢包,来缓解服务器压力(但是有的服务器策略就是不丢包,排队处理,这是两种完全不同的情况,客户端需要处理的方式完全相反,所以这个需要客户端和服务端来协商)
  • 信号弱
  • 2g网3g网
    传输的数据量较大,但是带宽小,就会导致大量数据丢失,表现形式就是网络延时
  • 中间的网络节点挂了

既然没有明确的定义,那我们只能通过各种工具和手段来判断当前是否为弱网环境了。对于丢包率和网络延时,都是可以使用数据来进行衡量的,下面来介绍更细化的指标。

2.2 弱网指标

如下图所示:
弱网指标

  1. httprtt(Http Round-Tip Time)
    又称TTFB(Time to first byte),指从客户端请求的第一个字节开始发送到接收到http header的第一个字节的时间差。即 httprtt = 接收第一个字节的时间 - 发送第一个字节的时间。如果httprtt的时间较长,则说明出现了网络延时,也可能是接入的网络质量的问题。
  2. tcprtt (tcp Round-Tip Time)
    tcprtt = 通过tcp通道接收到第一个字节的时间 - 发送第一个字节的时间。因为Http是基于Tcp的,所以在复用同一条tcp通道的情况下,httprtt的时间是包含tcprtt时间的。大部分情况下,httprtt已经可以说明问题的原因。
  3. throughput
    吞吐量,它是用来衡量单位时间内成功传送数据的数量,是比较权威的、官方的衡量网络质量的指标。最简单的公式: 吞吐量 = C / T ,C为完成的任务总量,T为完成这些任务的时间。
  4. singal strength
    指的是无线信号强度,也就是我们的wifi信号格。Android上SDK就可以支持获取了,而iOS则需要通过黑科技来获取。
  5. bandwidth-delay product(带宽时延乘积)
    带宽时延乘积,指的是一个数据链路的能力(bit/s)与来回通信延迟(s)的乘积。其结果是以比特(或字节)为单位的一个数据总量,反映的是当前网络通道的最大容量。Tcp中有滑动窗口的概念,会限制发送和接收数据的大小,所以Tcp窗口的大小是直接受带宽时延乘积的影响,来设置接收缓冲区大小和发送缓冲区大小。

2.3 建立弱网标准 / 弱网检测

在 百度APP移动端网络深度优化实践分享(三):移动端弱网优化篇中,百度使用的是一个循序渐进的过程,通过三个阶段来建立一个弱网标准:

  1. 第一阶段,线下测试
    获取预期的阈值,在测试App网络切换、DNS故障时,抓取这些数据
  2. 第二阶段,线上进行验证
    通过线下获取到的阈值,在线上可以获取到弱网的比例,找到会出现弱网的场景,进行诊断优化。(比如网络体验最好的微信,也只是在信令的传输上优化到极致)
  3. 第三阶段,线上反复试验
    通过反复试验来调整阈值,使得优化更接近业务层。

腾讯的做法也大同小异,都是根据2.2的指标进行数据收集,整理一套达到弱网时的数据标准。但是我们该如何去探测这些数据呢?这需要我们实现一套完整的网络探测架构。

3. 弱网检测

这部分基本照抄百度的实现了。网络探测是弱网检测的基础,目标是能够即时、正确的检测出网络质量,我们把网络探测分成两个部分,主动网络探测被动网络采集

3.1 主动网络探测

主动检测,就是在触发了某些特定条件后,主动的进行网络检测,并按照一定的条件检查出是否为弱网状态。架构图如下所示:
在这里插入图片描述

3.1.1 策略层

策略层,通过多种策略,使主动探测的即时性和准确性得到提高。主要在网络请求成功和失败的时候触发弱网检测的逻辑。大致分成下面三个逻辑:

  1. 成功时,如何判断进入弱网状态?
    检测 httprtt,是否达到弱网的阈值(即设定的 weak-httprtt),大于这个值就会进入弱网检测,为了防止频繁触发探测,加了时间间隔,一般为5~15min
  2. 成功时,如何判断退出弱网状态?
    检测 httprtt,是否没有达到阈值(即设定的 good-httprtt),如果小于这个值则切回到正常的网络状态。防止频繁触发探测,加了时间间隔,一般为30s~60s。
    在多次实验中,发现如果小于阈值,就一定是正常状态,而大于或等于阈值,并不能证明一定不是正常网络,所以也需要发起网络探测。但是由于这个动作是发生在成功的回调里,所以频次会很高,所以需要加上时间间隔。可能还需要限制次数。
  3. 失败时,如何判断进入弱网状态?
    直接进行网络探测,但是由于可能会频繁触发,其检测的条件需要比成功时检测要更加严苛,所以不会在时间上加限制,而是从次数上加限制。

进入网络探测状态后,就轮到基础能力层来实现。

3.1.2 基础能力层

基础能力层就是提供网络探测的手段,一般有两个:

  1. dns query
    使用Android系统的默认DNS服务器来查询目标服务器的(核心)域名,DNS查询的超时时间为3s
  2. ping
    正所谓“想测网络通不通,不ping百度ping京东”。因为他们的服务器都比较稳定,很少宕机,通过ping这些域名来了解当前网络状态、丢包率等信息。ping的次数为两次,超时时间默认是1s。

为什么要使用这两种手段呢?这是因为网络请求的过程中,主要有 DNS查询、TLS握手、TCP握手。而 DNS Query可以用来获取dns是否出现问题,ping可以用来检测TCP连接的连通性。

除了上面两种手段,应该也有其他网络探测的方式。在执行完这些手段后,将探测数据交给接口层。

3.1.3 接口层

接口层提供网络状态,大致为下面几个:

  • GOOD
    DNS查询成功、Ping成功,且没有超时、无丢包
  • BAD
    ping失败则为BAD
  • UNKNOWN
    无法识别状态
  • OFFLINE
    dns查询错误、网关错误,发送dns错误、ping读写错误、接收dns错误、ping地址错误等

3.2 被动网络采集

被动采集,就是每一次网络请求的所有细节都进行记录,并按照一定的条件将原始信息进行上报,上层再根据条件判断是否是弱网状态。cronet(Chromium提供给Android的网络堆栈库)可以提供这个能力,OkHttp应该没有这种能力,所以可以考虑接入 cronet。

被动采集的信息主要有以下三个:

  1. tcprtt
  2. httprtt
  3. throughput

架构图如下所示:
在这里插入图片描述

3.2.1 基础能力层

基础能力层就是采集网络请求的三个参数tcprtt、httprtt、throughput。

(1)tcprtt采集
基于posix和windows的socket编程接口来获取tcprtt。获取时机在连接完成,读完成和写完成;

(2)httprtt采集
基于HTTP协议栈实现,通过计算接收response数据开始和开始发送的时间差,来获取httprtt。获取时机在读首包完成时;

(3)throughput采集
通过计算公式获取bytes总量和时间,基于posix和windows的socket编程接口来获取bytes。获取时机在读完成时记录接收的bytes,在写完成时记录发送的bytes。时间的获取在吞吐量管理模块里完成,获取时机在请求完成和请求销毁时。

3.2.2 策略层

通过获取到的数据进行上报,三种数据对应三种模块,对于上报的时机需要做处理。

(1) 套接字管理模块
使用cronet的话,可以通过 getsockopt()函数获取 tcp_info结构体中的 trcpi_rtt值。其次由于tcprtt的上报比较频繁,所以需要做时间限制,最少间隔 1~3s

(2)吞吐量管理模块
负责吞吐量计算,之前介绍了公式,从网络活动监控器模块获取bytes,但是吞吐量的计算单位是bits,所以将bytes乘以8,。只有GET请求会被列入统计计算,并且至少要累计5个请求才开始统计计算。

排除精准度的干扰,比如 localhost,私有子网上的主机,特定用途的子网主机,可以参考 rfc1918 的规范

(3)网络质量管理模块
从套接字管理模块获取tcprtt,从吞吐量管理模块获取吞吐量,并且在Http协议栈读到首包完成时获取httprtt。
获取到这三个值后,需要经过一些策略限制上报的频次,10s的间隔限制;网络类型不能是UNKNOWN;网络不能频繁切换;rtt和总吞吐量大小各300。

这些限制都是为了排除误差、降低性能损耗。

3.2.3 接口层

接口层提供的状态是对标主动采集的网络状态的,所以也包括 GOOD、BAD、UNKNOWN、OFFLINE

  1. GOOD
    3G网、4G网、5G网,任一条件满足即标记为GOOD状态。
    通过阈值标记3G和广义的4G,httprtt大于等于273ms,tcprtt大于等于204ms,即标记为3G状态。
    小于这两个值则被标记为4G、5G网。
  2. BAD
    2G网、httprtt大于1.31s,任一条件满足则标记为BAD状态。阈值需要在网上查询资料,根据业务线调整。
  3. UNKNOWN
    非法的httprtt、tcprtt、吞吐量,任一条件满足则标记为UNKNOWN状态。比如初始化的时候,有的值会标为-1
  4. OFFLINE
    依赖平台能力进行判断,Android可以利用 ConnectivityManager得到 NetworkInfo,判断网络信息(没有连接wifi、或者wifi无网络等)

4. 弱网状态下的优化

4.1 使用QUIC进行弱网优化

QUIC是一种udp协议,由google发明,实现了TCP+TLS+HTTP/2,它还有一个更加响亮的名称: Http/3,Cronet库对其进行了支持,Android、iOS平台都可以使用。因为我自身对QUIC并不了解,所以这里并不会给出过多的使用介绍,接下来的一个月内,我会专门学习并写一篇有关 QUIC的文章。

总的来说,QUIC协议本身高效节流的特性,使得它在弱网络环境下可以大施拳脚。那么如何使用它呢?

4.1.1 网络库切换

在进行网络探测,并根据探测结果诊断当前网络状态为弱网时,网络请求进行切换,即将请求库切换到QUIC。
而当网络探测恢复为正常状态时,使用正常的网络库,例如OkHttp。

4.1.2 QUIC预连接

所谓QUIC的预连接,就是在进入弱网状态前提前建立QUIC连接。QUIC有引以为傲的0RTT,但第一次建立连接的时候是需要1RTT的,客户端首先会向服务器发送一个client hello消息,服务器会回复一个server reject消息,这个消息中包括了server config,有了server config后客户端就可以直接计算出密钥,完成0RTT。
所以我们在App启动的过程中会做一次QUIC预连接,将server config拉取下来,这样等进入弱网后走QUIC协议。

4.2 复合连接

复合连接,即多条连接。它解决的场景是为了多个IP地址的连接选取问题。无论是HttpDNS还是LocalDNS中,我们都能获取到一个域名下的多个ip。我们可以找到最优ip进行连接:

比如我们通过HttpDNS获取到两个ip,那我们将走下面步骤:

  1. 如果结果中存在IPv6的地址,那么会优先选用IPv6的地址,这个规则follow HappyEyeBall机制(可参考系列一对于HappyEyeBall的介绍)
  2. 接下来这两个IP会按照顺序尝试建立连接,如果第一个IP返回失败,将立即开始连接第二个IP
  3. 如果第一个IP率先成功返回,那么第二个IP将被加入连接尝试列表并停止所有尝试连接
  4. 如果第一个IP失败,会立刻开始第二个IP的连接
  5. 如果第一个IP处于pending状态,那么会启动一个定时器,默认延迟2s会发起第二个IP的连接,如果是多个IP将会递归连接,需要特别说明下,不同的网络制式延迟时间会不一样,这样体验也会更好。

复合连接的好处是提供最优的ip选取机制,但是也会带来服务端的高负载,所以使用的时候需要进行综合评估。

4.3 其他

  • 减少资源传送
  • 重连
  • 合并资源请求
  • 使用缓存兜底
  • 心理博弈,在ui层面上让用户耐心等待

5. 弱网测试

弱网测试的本质是模拟弱网环境。通过 Fiddler可以模拟。这里我介绍一种更便捷的方式: QNET
QNET弱网测试由腾讯开发,无需ROOT手机,无需连接数据线,以独立app的方式,为用户提供给快捷、可靠、功能完善的弱网络模拟服务,下载地址:QNET弱网测试

先选择需要测试的App:
在这里插入图片描述
然后设置参数,QNET提供了多个参数调整:
在这里插入图片描述
最后可以查看弱网模板:
在这里插入图片描述

6. 参考文章

百度APP移动端网络深度优化实践分享(三):移动端弱网优化篇
弱网的概念以及弱网测试
QUIC成为了HTTP/3的标准传输协议!
技术扫盲:新一代基于UDP的低延时网络传输层协议——QUIC详解
弱网的概念以及弱网测试

猜你喜欢

转载自blog.csdn.net/jdsjlzx/article/details/123778432