做一个具有高可用性的网络库(下)
网速检测
如果可以获取到当前手机的网速,就可以做很多额外的操作。 比如在图片场景中,可以基于当前的实时网速进行图片的质量的变换,在网速快的场景下,加载高质量的图片,在网速慢的场景下,加载低质量的图片。 我们如何去计算一个比较准确的网速呢,比如下面列举的几个场景
当前app没有发起网络请求,但是存在其他进程在使用网络,占用网速
当前app发起了一个网络请求,计算当前网络请求的速度
当前app并发多个网络请求,导致每个网络请求的速度都比较慢
可能还会存在一些其他的场景,那么在这么复杂的场景,我们通过两种不同的计算方式进行合并计算
基于当前网络接口的response读取的速度,进行网速的动态计算
基于流量和时间计算出网速
通过计算出来的两者,取最大值的网速作为当前的网速值。
基于当前接口动态计算
基于前面网络请求的全流程监控,我们可以在全局添加所有网络接口的监听,在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") nickname: String)
这个注解是如何能够透传到网络库中的内部拦截器呢。首先需要把在Interface中配置的注解获取出来。CallAdapter.Factory可以拿到网络请求中配置的注解。
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {}
我们可以在这里讲注解和同一个url的request的关联起来。 然后在拦截器中获取是否有对应的注解。
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (!NetAnnotationUtil.isAnntationExsit(request, Encryption::class)) {
return chain.proceed(request)
}
//do encrypt we want
...
}
调试工具
对于网络能力,我们经常会去针对网络接口进行调试。最简单的方式是通过charles抓包。 通过charles抓包我们可以做到哪些调试呢?
查看请求参数、查看网络返回值
mock网络数据 看着上面2个能力似乎已经满足我们了日常调试了,但是它还是有一些缺陷的:
必须要借助PC
在App关闭了可抓包能力之后,就不能再抓包了
无法针对于post请求参数区分
所以,我们需要有一个强大的网络调试能力,既满足了charles的能力, 也可以不借助PC,并且不论apk是否开启了抓包能力也能够允许抓包。 可以添加一个专门Debug网络拦截器,在拦截器中实现这个能力。
把网络的debug文件配置在本地Sdcard下(也可以配置在远端统一的地址中)
通过拦截器,进行url、参数匹配,如果命中,将本地json返回。否则,正常走网络请求。
data class GlobalDebugConfig(
@SeerializedName("printToConsole") var printData: Boolean = false,
@SeerializedName("printToPage") var printData: Boolean = false
)
data class NetDebugInfo(
@SerializedName("filter") var debugFilterInfo: NetDebugFilterInfo?,
@SerializedName("response") var responseString: Any?,
@SerializedName("code") var httpCode: Int,
@SerializedName("message") var httpMessage: String? = null,
@SeerializedName("printToConsole") var printData: Boolean = true,
@SeerializedName("printToPage") var printData: Boolean = true)
data class NetDebugFilterInfo(
@SerializedName("host") var host: String? = null,
@SerializedName("path") var path: String? = null,
@SerializedName("parameter") var paramMap: Map<String, String>? = 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(filterRequestFilterInfo, request)) {
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等参数。。在组件化中,针对这几个参数的配置有下面几个比较容易想到的解决方案:
在各个组件单独配置这几个Header
每个组件都需要但单独配置Header,会存在很多重复代码
通用信息很大概率在各个组件中获取不到
由主工程实现代理发起网络请求 这种实现方式也有下面几个缺陷
主工程需要关注所有组件,随着集成的组件越来越多,主工程需要初始化网络代理接口会越来越多
由于主工程并不知道组件什么时候会启动,只能App启动就初始化网络代理,导致组件初始化提前
所有直接和间接依赖的模块都需要由主工程来实现代理,很容易遗漏
通用信息拦截器自动注入
正因为上面两个实现方式或多或少都有问题,所以需要从网络库这一层来解决这个问题。 我们可以在网络层通过服务发现的能力,给外部提供一个通用网络信息拦截器注解, 一般由主工程实现, 完成默认信息的Header修改。创建网络Client实例时,自动查找app中被通用网络信息拦截器注解标注的拦截器。
线程池、连接池复用
各个组件都会有自己的网络Client实例,导致在同一个进程中,创建出来网络Client实例过多,同时线程池、连接池并没有复用。所以在网络库中,各个组件创建的网络Client默认会共享网络连接池和线程池,有特殊需要的模块,可以强制使用独立线程池和连接池。
作者:谢谢谢_xie
来源:juejin.cn/post/7074493841956405278