注册

做一个具有高可用性的网络库(下)

续 做一个具有高可用性的网络库(上)

网速检测

如果可以获取到当前手机的网速,就可以做很多额外的操作。 比如在图片场景中,可以基于当前的实时网速进行图片的质量的变换,在网速快的场景下,加载高质量的图片,在网速慢的场景下,加载低质量的图片。 我们如何去计算一个比较准确的网速呢,比如下面列举的几个场景

  • 当前app没有发起网络请求,但是存在其他进程在使用网络,占用网速

  • 当前app发起了一个网络请求,计算当前网络请求的速度

  • 当前app并发多个网络请求,导致每个网络请求的速度都比较慢

可能还会存在一些其他的场景,那么在这么复杂的场景,我们通过两种不同的计算方式进行合并计算

  1. 基于当前网络接口的response读取的速度,进行网速的动态计算

  2. 基于流量和时间计算出网速

通过计算出来的两者,取最大值的网速作为当前的网速值。

基于当前接口动态计算

基于前面网络请求的全流程监控,我们可以在全局添加所有网络接口的监听,在ResponseBody这个周期内,基于response的byte数和时间,可以计算每一个网络body读取速度。之所以要选取body读取的时间来计算网速,主要是为了防止把网络建连的耗时影响了最终的网速计算。 不过接口网速的动态计算需要针对不同场景去做不同的计算。

  • 当前只有一个网络请求 在当前只有一个网络请求的场景下, 当前body计算出来请求速度就是当前的网速。

  • 当前同时存在多个网络请求发起时 每一个请求都会瓜分网速,所以在这个场景下,每个网络请求的网速都有其对应的网速占比。比如当前有6个网络请求,每个网络请求的网速近似为1/6。

当然,为了防止网速的短时间的波动,每个网络请求对于当前的网速的影响是有固定的占比的, 比如我们可以设置的占比为5%。

currentSpeed = (requestSpeed * concurrentRequestCount - preSpeed) * ratePercent + preSpeed

其中

  • requestSpeed:表示为当前网络请求计算出来的网速。

  • concurrentRequestCount:表示当前网络请求的总数

  • preSpeed:表示先前计算出来的网速

  • ratePercent:表示当前计算出来网速对于真正的网速影响占比

为了防止body过小导致的计算出来网速不对的场景,我们选取当前body大小超过20K的请求参与进行计算。

基于流量动态计算

基于流量的计算,可以参照TrafficState进行计算。可以参照facebook的network-connection-class 。可以通过每秒获取系统当前进程的流量变化,网速 = 流量总量 / 计算时间。 它内部也有一个计算公式:

public void addMeasurement(double measurement) {
  double keepConstant = 1 - mDecayConstant;
  if (mCount > mCutover) {
    mValue = Math.exp(keepConstant * Math.log(mValue) + mDecayConstant * Math.log(measurement));
  } else if (mCount > 0) {
    double retained = keepConstant * mCount / (mCount + 1.0);
    double newcomer = 1.0 - retained;
    mValue = Math.exp(retained * Math.log(mValue) + newcomer * Math.log(measurement));
  } else {
    mValue = measurement;
  }
  mCount++;
}

自定义注解处理

假如我们现在有一个需求,在我们的网络库中有一套内置的接口加密算法,现在我们期望针对某几个网络请求做单独的配置,我们有什么样的解决方案呢? 比较容易能够想到的方案是添加给网络添加一个全局拦截器,在拦截器中进行接口加密,然后在拦截器中对符合要求的请求URL的进行加密。但是这个拦截器可能是一个网络库内部的拦截器,在这里面去过滤不同的url可能不太合适。 那么,有什么方式可以让这个配置通用并且简洁呢? 其中一种方式是通过接口配置的地方,添加一个Header,然后在网络库内部拦截器中获取Header中有没有这个key,但是这个这个使用起来并且没有那么方便。首先业务方并不知道header里面key的值是什么,其次在添加到header之后,内部还需要在拦截器中把这个header的key给移除掉。

最后我们决定对于网络库给单接口提供的能力都通过注解来提供。 就拿接口加密为例子,我们期望加密的配置方式如下所示

@Encryption
@POST("xxUrl)
fun testRequest(@Field("xxUrl"nicknameString)

这个注解是如何能够透传到网络库中的内部拦截器呢。首先需要把在Interface中配置的注解获取出来。CallAdapter.Factory可以拿到网络请求中配置的注解。

override fun get(returnTypeTypeannotationsArray<Annotation>retrofitRetrofit): CallAdapter<**> {}

我们可以在这里讲注解和同一个url的request的关联起来。 然后在拦截器中获取是否有对应的注解。

override fun intercept(chainInterceptor.Chain): Response {
       val request = chain.request()
       if (!NetAnnotationUtil.isAnntationExsit(requestEncryption::class)) {
           return chain.proceed(request)
      }
       //do encrypt we want
      ...
}

调试工具

对于网络能力,我们经常会去针对网络接口进行调试。最简单的方式是通过charles抓包。 通过charles抓包我们可以做到哪些调试呢?

  1. 查看请求参数、查看网络返回值

  2. mock网络数据 看着上面2个能力似乎已经满足我们了日常调试了,但是它还是有一些缺陷的:

  3. 必须要借助PC

  4. 在App关闭了可抓包能力之后,就不能再抓包了

  5. 无法针对于post请求参数区分

所以,我们需要有一个强大的网络调试能力,既满足了charles的能力, 也可以不借助PC,并且不论apk是否开启了抓包能力也能够允许抓包。 可以添加一个专门Debug网络拦截器,在拦截器中实现这个能力。

  1. 把网络的debug文件配置在本地Sdcard下(也可以配置在远端统一的地址中)

  2. 通过拦截器,进行url、参数匹配,如果命中,将本地json返回。否则,正常走网络请求。

data class GlobalDebugConfig(
    @SeerializedName("printToConsole"var printDataBoolean = false,
   @SeerializedName("printToPage"var printDataBoolean = false
)
data class NetDebugInfo(
       @SerializedName("filter"var debugFilterInfoNetDebugFilterInfo?,
       @SerializedName("response"var responseStringAny?,
       @SerializedName("code"var httpCodeInt,
       @SerializedName("message"var httpMessageString? = null,
       @SeerializedName("printToConsole"var printDataBoolean = true,
       @SeerializedName("printToPage"var printDataBoolean = true)

data class NetDebugFilterInfo(
       @SerializedName("host"var hostString? = null,
       @SerializedName("path"var pathString? = null,
       @SerializedName("parameter"var paramMapMap<StringString>? = null)

首先日志输出有个全局配置和单个接口的配置,单接口配置优于全局配置。

  • printToConsole表示输出到控制台

  • printToPage表示将接口记录到本地中,可以在本地页面查看请求数据

其次filterInfo就是我们针对接口请求的匹配规则。

  • host表示域名

  • path表示接口请求地址

  • parameter表示请求参数的值,如果是post请求,会自动匹配post请求的body参数。如果是get请求,会自动匹配get请求的query参数。

       val host = netDebugInfo.debugFilterInfo?.host
       if (!TextUtils.isEmpty(host) && UriUtils.getHost(request.url().toString()) !host) {
           return chain.proceed(request)
      }
       val filterPath = netDebugInfo.debugFilterInfo?.path
       if (!TextUtils.isEmpty(filterPath) && path !filterPath) {
           return chain.proceed(request)
      }
       val filterRequestFilterInfo = netDebugInfo.debugFilterInfo?.paramMap
       if (!filterRequestFilterInfo.isNullOrEmpty() && !checkParam(filterRequestFilterInforequest)) {
           return chain.proceed(request)
      }
       val resultResponseJsonObj = netDebugInfo.responseString
       if (resultResponseJsonObj == null) {
           return chain.proceed(request)
      }
       return Response.Builder()
               .code(200)
               .message("ok")
               .protocol(Protocol.HTTP_2)
               .request(request)
               .body(NetResponseHelper.createResponseBody(GsonUtil.toJson(resultResponseJsonObj)))
               .build()

对于配置文件,最好能够共同维护mock数据。 本地可以提供mock数据展示列表。

组件化上网络库的能力支持

在组件化中,各个组件都需要使用网络请求。 但是在一个App内,都会有一套统一的网络请求的Header,例如AppInfo,UA,Cookie等参数。。在组件化中,针对这几个参数的配置有下面几个比较容易想到的解决方案:

  1. 在各个组件单独配置这几个Header

  • 每个组件都需要但单独配置Header,会存在很多重复代码

  • 通用信息很大概率在各个组件中获取不到

  1. 由主工程实现代理发起网络请求 这种实现方式也有下面几个缺陷

  • 主工程需要关注所有组件,随着集成的组件越来越多,主工程需要初始化网络代理接口会越来越多

  • 由于主工程并不知道组件什么时候会启动,只能App启动就初始化网络代理,导致组件初始化提前

  • 所有直接和间接依赖的模块都需要由主工程来实现代理,很容易遗漏

通用信息拦截器自动注入

正因为上面两个实现方式或多或少都有问题,所以需要从网络库这一层来解决这个问题。 我们可以在网络层通过服务发现的能力,给外部提供一个通用网络信息拦截器注解, 一般由主工程实现, 完成默认信息的Header修改。创建网络Client实例时,自动查找app中被通用网络信息拦截器注解标注的拦截器。

线程池、连接池复用

各个组件都会有自己的网络Client实例,导致在同一个进程中,创建出来网络Client实例过多,同时线程池、连接池并没有复用。所以在网络库中,各个组件创建的网络Client默认会共享网络连接池和线程池,有特殊需要的模块,可以强制使用独立线程池和连接池。

作者:谢谢谢_xie
来源:juejin.cn/post/7074493841956405278

0 个评论

要回复文章请先登录注册