对于网络框架设计封装的扯淡
本 blog 的代码库:
HttpUtil2
1. 前后端交互协定设计
惯例是 data-code-msg 三字段设计
也有 data-code-msg-isSuccess. 其中 isSuccess 和 code 其实互为冗余.
但看了 Facebook,google 等大公司的接口交互协定, 发现其实最全的是:
data-code-msg-errorData.
申请正确时:
{
"data": {"uid": "898997899788997"},
"code": "0",
"msg": "success!",
"success": true,
"errorData": null
}
申请谬误时
谬误起因千奇百怪, 应应用 map 来解析 errorData, 防止解析异样. 或间接应用 optJSONObject(“errorData”)
{
"data": null,
"code": "user.login.401",
"msg": "unlogin",
"success": false,
"errorData": {
"reason":"kickout",
"time":1689799989
}
}
为了 debug 不便, 在开发 / 测试环境, 后盾 500 时, 应将异样栈信息间接塞在 msg 里返回给前端.
2. 应该蕴含哪些性能
底层
从 urlconnection 到 httpclient 到 okhttp
封装层
从 volley/asyncHttpClient 到 retrofit
现在基本上是 okhttp 一统底层, 下层 retrofit+rxjava.
即应用 retrofit, 依然有很多反复代码要写, 须要更进一层的封装, 不便日常 crtl+c ,ctrl+v.
即便是 crtl+c, 也心愿代码能少一点是一点.
那么一个封装欠缺的网络框架, 还须要哪些性能?
先看看几个 star 比拟多的封装库:
OkGo:
大强子的 NET
rxHttp
联合日常开发教训, 总结一下, 其实有如下可塞入框架中:
其实, 再想想, 一个欠缺的客户端网络库, 应该像 postman 一样基于配置, 傻瓜易用.
封装网络框架, 无非是吧这些个 gui 变成 api 而已.
3. 几个设计上的思维
开箱即用
跟 spring boot 一样, 约定大于配置. 外面的配置项大多都有默认值.
初始化即便只是调用最简略的 init 办法, 也可能应用框架大部分性能.
全量信息可拜访
回调里要能拿到本次申请和响应的全量信息.
比方 okhttp 在他的 callback 里就能拿到整个 call 对象, 以及整个 response 信息.
很多框架 callback 里只有解析后的 data. 须要用到其余信息时就懵逼了.
全方位适应页面生命周期
管你传 view,fragment,activity,lifecycleowner,viewmodel, 统统主动解决.
你说 view 怎么拿到页面生命周期? context 里层层剥开, 总能拿到 activity.
生命周期完结后主动勾销申请.
勾销申请有两种做法:
(在期待队列里没有区别, 都是移出队列 –> 只是 … okhttp-rxjava 的线程模型下, 根本都是立即收回, 没有期待)
- 间接 socket.close() 关掉连贯
- 不干涉底层, 只是在回调里通过 boolean 值切断回调
retrofit 和 rxjava 的 takeutil, 都是用的第一种. 简略粗犷易实现, 只是后端接口监控里多了一些 0 或者 499 的谬误.
不必 kotlin 协程
kotlin 协程很牛逼? 道歉, 只是假协程, 底层还是线程池切换. 只是用同步形式写异步代码而已 (跟 js 的 async,await 差不多).
当然这并非 kotlin 不行, 而是 jvm 自身并未反对协程.
要真能实现像 go 一样的真协程, 或者跳出 jvm, 本人调用 epoll 实现多路复用, 那就牛逼了, 我必定抢着用 kotlin 来改写这个框架.
上面开始讲讲每个关键点的实现和应用
4. api 应用:
间接看 readme
HttpUtil.requestAsJsonArray("article/getArticleCommentList/v1.json",PostStandardJsonArray.class)
.addParam("pageSize","30")
.addParam("articleId","1738")
.addParam("pageIndex","1")
.post()
.setCacheMode(CacheMode.FIRST_CACHE_THEN_REQUEST)
// .setCacheMode(CacheStrategy.REQUEST_FAILED_READ_CACHE)
.callback(new MyNetCallback<ResponseBean<List<PostStandardJsonArray>>>(true,null) {
@Override
public void onSuccess(ResponseBean<List<PostStandardJsonArray>> response) {MyLog.json(MyJson.toJsonStr(response.data));
}
@Override
public void onError(String msgCanShow) {MyLog.e(msgCanShow);
}
});
String url2 = "https://kiwivm.64clouds.com/dist/openvpn-install-2.4.5-I601.exe";
HttpUtil.download(url2)
.setFileDownlodConfig(FileDownlodConfig.newBuilder()
.verifyBySha1("76DAB206AE43FB81A15E9E54CAC87EA94BB5B384")
.isOpenAfterSuccess(true)
.build())
.callback(new MyNetCallback<ResponseBean<FileDownlodConfig>>() {
@Override
public void onSuccess(ResponseBean<FileDownlodConfig> response) {MyLog.i("path:"+response.data.filePath);
}
@Override
public void onError(String msgCanShow) {MyLog.e(msgCanShow);
}
});
5. 关键点
5.1 同步异步的反对
其实 okhttp 自身就有同步和异步的写法.
同步间接 return, 用 try-catch 包裹.
异步就应用 callback.
但咱们这里外部应用 retrofit, 基于 rxjava. 全副变成了回调的模式.
那么, 就不谋求同步的写法, 间接以异步的模式写同步执行.
rxjava 怎么同步执行?
不进行线程切换, 就同步执行了. so easy
HttpUtil.requestString("article/getArticleCommentList/v1.json")
.post()
.setSync(true)// 同步执行
.addParam("pageSize","30")
.addParam("articleId","1738")
.addParam("pageIndex","1")
.callback(new MyNetCallback<ResponseBean<String>>(true,null) {
@Override
public void onSuccess(ResponseBean<String> response) {MyLog.i(response.data);
}
@Override
public void onError(String msgCanShow) {MyLog.e(msgCanShow);
}
});
5.2 主动解决生命周期
原始时代:
本库应用的形式.
用动态 map 存储 activity/fragment 对象和申请, activity/fragment destory 时, 从 map 中取出申请, 判断状态, 进行 cancel.
/**
* 勾销申请, 常在 activity ondestory 处调用. 间接传入 activity 即可, 不会保留援用, 间接辨认其名字作为 tag
*
* @param obj
*/
public static void cancelByTag(Object obj) {if (obj == null) {return;}
List<retrofit2.Call> calls = callMap.remove(obj);// 从 gc root 援用中删除
if (calls != null && calls.size() > 0) {for (retrofit2.Call call : calls) {
try {if (call.isCanceled()) {return;}
call.cancel();} catch (Exception e) {ExceptionReporterHelper.reportException(e);
}
}
}
}
RxLifecycle + rxjava
onDestory 时构建 transformer, 传给 rxjava 的 takeUtil 操作符.
本库未实现
livedata
observable 转 livedata, 间接跟 lifecyclerOwner 挂钩.
本库已实现.
5.3 通用 UI 反对
loadingDialog
内置, 默认不显示. 可配置开关,UI 款式
谬误 msg 的 toast
比拟不便的做法是在 onError 里对立解决, 默认敞开, 能够通过链式 api 开启.
测试环境应 toast: code+”\n”+msg. 且测试环境的 msg 应尽量带栈信息.
错误码转文案
个别, 应在框架内对立解决.
分三个类型:
底层框架抛出的 exception, 应转为敌对文案
http 申请自身的错误码, 比方 400,500 之类的, 应提供对立文案
业务 data-code-msg 内, 如果 msg 局部后盾不做国际化, 那么客户端应配置对应的翻译文案.
框架应主动解决前两个, 并提供第三种业务谬误文案的配置接口:
ExceptionFriendlyMsg.init(context, new IFriendlyMsg() {Map<String,Integer> errorMsgs = new HashMap<>();
{errorMsgs.put("user.login.89899",R.string.httputl_unlogin_error);
}
@Override
public String toMsg(String code) {Integer res = errorMsgs.get(code);
if(res != null && res != 0){return context.getResources().getString(res);
}
return "";
}
});
外部已配置文案:(中文 + 英文)
5.4 响应体格式校验
bean validator 这件事件在服务端接管客户端 / 浏览器申请时比拟罕用. 曾经倒退成为了一项 java 标准.
其实这个需要在客户端并不强烈. 服务端的返回大多数状况还是比较稳定的, 呈现丢字段, 字段谬误等状况比拟少.
不过, 为了小装一个 X, 我还是把这个性能实现了 –>
其实也不是实现, 只是把服务端罕用的性能迁徙到挪动端, 并进行了适配. 做了一点渺小的工作.
请看:
AndroidBeanValidator
要移植到 Android, 须要思考 java8 兼容性问题, 性能 (办法耗时), 以及对 apk 大小的影响, 默认应用的是 Apache BVal 1.1.2.
String errorMsg = BeanValidator.validate(bean);
// 返回的 errorMsg 为空就阐明校验通过
if(!TextUtils.isEmpty(errorMsg)){//Toast.makeText(this,errorMsg,Toast.LENGTH_LONG).show();
Observable.error(xxx)// 把 errorMsg 和指定 errorCode 往外抛
}else {// 拿到合格的 bean}
这个操作, 放到 bean 刚被解析进去的时候做就行.
5.5 缓存管制: 丰盛的缓存模式
超过 http 协定自身的缓存管制模式
http 协定自身缓存管制有哪些局限:
- 只能缓存 get 申请
- 老简单的申请头
本人写的客户端, 能受这点气? 必须得改, 大改!
- 要能缓存任何申请
- 要能一键反对罕用业务模式
.setCacheMode(CacheMode.FIRST_CACHE_THEN_REQUEST)
// 缓存策略, 分类参考:https://github.com/jeasonlzy/okhttp-OkGo
// 不应用缓存, 该模式下,cacheKey,cacheMaxAge 参数均有效
public static final int NO_CACHE = 1;
// 齐全依照 HTTP 协定的默认缓存规定,例如有 304 响应头时缓存。public static final int DEFAULT = 2;
// 先申请网络,如果申请网络失败,则读取缓存,如果读取缓存失败,本次申请失败。胜利或失败的回调只有一次
public static final int REQUEST_FAILED_READ_CACHE = 3;
// 优先应用缓存, 如果缓存不存在才申请网络, 胜利或失败的回调只有一次
public static final int IF_NONE_CACHE_REQUEST = 4;
// 先应用缓存,不论是否存在,依然申请网络, 可能导致两次胜利的回调或一次失败的回调.
// 胜利回调里, 有标识辨认本次是缓存还是网络返回.
public static final int FIRST_CACHE_THEN_REQUEST = 5;
// 只读取缓存, 不申请网络
public static final int ONLY_CACHE = 6;
5.6 cookie
okhttp 底层默认没有存 cookie, 但提供了接口, 咱们基于他的接口 cookiejar 实现.
个别有:
- 不存储 cookie
- 只在内存存储 cookie
- cookie 序列化到 shareprefences/ 文件:
第三种跟浏览器行为比拟像了. 只不过没有浏览器恶心的各种跨域, 平安限度, 轻易玩.
你说 httpOnly?sameSite? 不存在的, 在我这就是几个 key-value, 想怎么搞就怎么搞.
不过作为一个框架, 还是遵循一下最根本的, 响应一下 host 和 path 还是要的. 其余的, 提供接口给他人自定义吧. 松或者严都随便.
public static final int COOKIE_NONE = 1;
public static final int COOKIE_MEMORY = 2;
public static final int COOKIE_DISK = 3;
private int cookieMode = COOKIE_DISK;// 默认是做长久化操作
/**
* 设置 cookie 管理策略
*/
public GlobalConfig setCookieMode(int cookieMode) {
this.cookieMode = cookieMode;
return this;
}
5.7 公共申请头, 申请参数 / 申请体参数
可初始化时用 map 存储, 每次申请时退出:
如果值初始化后就不变, 那举荐应用这种形式.
如果会变动, 就不能用这种. 或者变动后更新缓存的 map.
也能够利用 okhttp 的拦截器, 在拦截器里退出.
对于申请头,get 申请, 很简略就实现了
但对于 post json 或者 multiPart, 就须要将 json 再变成 map, 而后退出, 将 multiPart 还原, 再退出.
可参考:
AddCommonHeaderAndParamInterceptor
如果波及到申请体签名, 那么务必将此拦截器加到签名拦截器之前.
5.8 申请超时
okhttp 不是有超时设置么?
之前只有 connecTimeout,read,write 三个超时工夫, 当初看, 已新增 callTimeout, 涵盖了 okhttp 层面的整个申请过程.
对于当初没有 calltimeout 的时代, 单纯设置上面三个是不够的, 因为 dns 解析过程并不能被这三者笼罩.
能够应用 rxjava 的 timeout 来管制整个流程的耗时.
现在仍然优先应用 rxjava 来管制. 因为 okhttp 的 calltimeout 无奈笼罩自定义缓存读写的超时.
这种个别提供全局配置和单个申请配置
5.9 申请重试
okhttp 自身有个重试 api:
builder.retryOnConnectionFailure(boolean)
但只是 tcp 连贯失败的重试. 且只能重试一次
要不论什么谬误都重试, 且可指定重试次数, 还是得靠 rxjava 的 api. 这就不说了, 间接用就行.
5.10 异样捕捉和上报
别管 okhttp/retrofit 崩不崩, 你作为一个封装框架, 必定不能崩.
任何状况都不能崩, 得做到 100% crash free.
有几个要害的中央:
拦截器内
作为利用拦截器第一个, 对 chain.proceed(request) 加上 try-catch, 降级为 ioException, 能够被 okhttp 的 error 回调解决.
@Override
public Response intercept(Chain chain) throws IOException {
try {Response response = chain.proceed(chain.request());
} catch (Throwable e) {if (e instanceof IOException) {throw e;} else {
// 降级, 让 okhttp 框架能处理错误, 而不是 crash
throw new IOException(e);
}
}
}
rxjava 全局异样捕捉:
这个个别在主工程做. 框架内不参加.
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(Throwable e) throws Exception {report(e);
}
});
本人框架层的回调里
回调的 onSuccess 和 onError 是使用者实现的, 如果也呈现了解体怎么办? 也给你兜住!
onSuccess 抛异样, 降级给 onError
onError 还抛异样, 模拟 rxjava, 降级给全局错误处理
if(bean.success){
try {onSuccess(callback,t);
}catch (Throwable throwable){onError(callback,throwable);
}
}else {onError(callback,bean.errorInfo);
}
private static <T> void onError(MyNetCallback<T> callback, Throwable e) {
try {Tool.logd("-->http is onError:"+callback.getUrl() );
Tool.dismissLoadingDialog(callback.dialogConfig, callback.tagForCancel);
ErrorCallbackDispatcher.dispatchException(callback, e);
}catch (Throwable e2){if(GlobalConfig.get().getErrorHandler() != null){
try {GlobalConfig.get().getErrorHandler().accept(e2);
} catch (Exception exception) {exception.printStackTrace();
}
}else {if(!GlobalConfig.get().isDebug()){e2.printStackTrace();
}
}
// 测试环境, 都解体, 揭示一下
if(GlobalConfig.get().isDebug()){throw e2;}
}
}
5.11 debug 性能
网络嘛,debug 次要模式还是抓包
提供丰富多彩的看包的模式:
-
logcat
革新 okhttpLoggingInterceptor, 申请体响应体间接一行打印, 不便拷贝. 大于 4000 个字符切割分行.
-
手机内抓包
革新的 chuck, 基于 okhttp 拦截器, 告诉栏显示抓包内容. 提供过滤过于频繁的刷屏申请, 比方各种行为日志上报之类的.
-
pc 代理抓包
通常用 fiddler 或者 chales.
须要配置: 7.0 以上 debugable 环境疏忽证书
<network-security-config> <debug-overrides> <trust-anchors> <certificates src="system"/> <certificates src="user"/> </trust-anchors> </debug-overrides> <base-config cleartextTrafficPermitted="true"> <trust-anchors> <certificates src="system" /> </trust-anchors> </base-config> </network-security-config>
或者间接网络框架在 debug 环境疏忽证书
-
stetho-> flipper
基于 okhttp 拦截器, 抓包内容发送到 pc 上的客户端显示. 显示界面更高端大气上档次.
改写 flipper 内置的拦截器, 有额定加密的, 解密后创造文过来显示.
一行脚本集成: flipperUtil
5.12 线上监测
上报不麻烦, 要害是统计分析怎么搞? 有哪些现成的, 本人搭又要怎么搭.
在下面的拦截器里增加上报即可. 要害是上报到哪里
构建 exception, 上报到 sentry.
或者本人搭一条 flume+elk 的剖析零碎.
或者猥琐一点, 构建 event 上报到事件统计平台, 蹭他们的流量.
哪些参数
-
错误信息:
在下面拦截器 / 对立的谬误回调里拿到并上报即可. 个别上报到统计平台看谬误趋势, 依据趋势看某时段前后台服务是否有异样. 这通常只是后盾自身申请监控的补充.
前几年利用谷歌剖析的事件实时剖析性能, 将错误信息变成 event 上报, 能实时看 1min 内,30min 内的网络谬误趋势, 自带排序, 爽得一逼, 惋惜前面谷歌剖析挪动端下线了,firebase 上这个性能被经营占用了.
-
申请分时信息:
比方 dns 耗时,tcp 耗时,tls,http 申请响应, 这些都能够通过 okhttp 的 eventListener 接口来获取.
5.13 平安
伎俩根本是:
- https 上玩的一些操作
- 自定义加密
- 申请头, 申请体签名 - 防篡改
https
基本上就是这几个问题
什么是中间人攻打
如何防备中间人攻打
什么是单向证书校验, 框架层如何实现
什么是双向证书校验, 框架层如何实现
如何反抗证书校验? root 手机 +frida+okhttplogging 的 dex 参考: frida 应用
自定义加密
拦截器里拿到申请体字节数组, 加密, 再构建新的 requestBody, 持续走即可.
final Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
final long size = buffer.size();
final byte[] bytes = new byte[(int) size];
buffer.readFully(bytes);
final byte[] bytesEncrypted = encrypt(bytes);
// 加密胜利 / 失败, 最好在申请头加一个标识
return new RequestBody() {
@Override
public MediaType contentType() {return MediaType.parse(type);
}
@Override
public long contentLength() {return bytesEncrypted.length;}
@Override
public void writeTo(BufferedSink sink) throws IOException {sink.write(bytesEncrypted);
}
};
申请头申请体签名
无非是加盐来生成 sha1,sha256 什么的, 没什么好讲的.
5.14 gzip
okhttp 已内置对响应体的 gzip 解决, 这个不必再说.
如果申请体是比拟大的字符串, 那么用 gzip 压缩, 流量收益方面还是能够的.
须要前后端反对.
咱们在拦截器里进行 gzip 压缩.
gzip 前无奈指定 gzip 后的大小, 能够再包裹一层, 以设定申请体的 contentLength
private RequestBody gzip(final RequestBody body, String type) {return new RequestBody() {
@Override
public MediaType contentType() {return body.contentType();
}
@Override
public long contentLength() {return -1; // We don't know the compressed length in advance!}
@Override
public void writeTo(BufferedSink sink) throws IOException {BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
body.writeTo(gzipSink);
gzipSink.close();}
};
}
后端 nginx 上用 lua 脚本进行解压缩后再转发即可.
5.15 断点上传 / 下载
利用的是 http 头的 range 和 content-range, 加上 java 的 randomAccessFile api.
次要还是工程问题比拟难解决. 写得好的框架不多. 我这个没有做这个断点续传性能.
5.16 下载后处理
抄了些迅雷等下载软件的性能, 用 api 的模式提供进去
比方:
- 下载后校验 md5/sha1
- 下载后主动关上: 须要解决 Android7 的 File uri permission
- 下载后告诉 mediastore 扫描
- 是否暗藏文件: 下载一些隐衷文件时用, 你懂的. 利用.nomedia 空文件暗藏, 防小人不防君子.
- 告诉栏显示下载进度 / 对话框显示下载进度
5.17 回调模式
- callback
- livedata
- 返回 observable
5.18 接口聚合
场景 1 多图异步上传
public static io.reactivex.Observable<ResponseBean<S3Info>> uploadImgs(String type, final List<String> filePaths){final List<S3Info> infos = new ArrayList<>();
io.reactivex.Observable<ResponseBean<S3Info>> observable =
HttpUtil.requestAsJsonArray(getUploadTokenPath,S3Info.class)
.get()
.addParam("type", type)
.addParam("contentType", IMAGE_JPEG)
.addParam("cnt",filePaths.size())
.asObservable()
.flatMap(new Function<ResponseBean<List<S3Info>>, ObservableSource<ResponseBean<S3Info>>>() {
@Override
public ObservableSource<ResponseBean<S3Info>> apply(ResponseBean<List<S3Info>> bean) throws Exception {infos.addAll(bean.bean);
List<io.reactivex.ObservableSource<ResponseBean<S3Info>>> observables = new ArrayList<>();
for(int i = 0; i< bean.bean.size(); i++){S3Info info = bean.bean.get(i);
String filePath = filePaths.get(i);
io.reactivex.Observable<ResponseBean<S3Info>> observable =
HttpUtil.request(info.getUrl(),S3Info.class)
.uploadBinary(filePath)
.put()
.setExtraFromOut(info)
.responseAsString()
.treatEmptyDataAsSuccess()
.asObservable();
observables.add(observable);
}
return io.reactivex.Observable.merge(observables);
}
});
return observable;
}
场景 2: 多接口异步申请, 对立回调一次
后盾微服务拆得太细, 又不愿做聚合, 只能客户端本人做.
在客户端, 基于 Rxjava 实现通用的聚合接口申请.
每个接口可配置是否承受失败
代码
HttpUtil2
flipperUtil
AndroidBeanValidator
AddCommonHeaderAndParamInterceptor
frida 应用