对于网络框架设计封装的扯淡

本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应用