關於網路框架設計封裝的扯淡

qianby發表於2021-09-09

關於網路框架設計封裝的扯淡

本blog的程式碼庫:

[HttpUtil2] github.com/hss01248/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比較多的封裝庫:

圖片描述

圖片描述

圖片描述

結合日常開發經驗,總結一下,其實有如下可塞入框架中:

圖片描述

圖片描述

其實,再想想,一個完善的客戶端網路庫,應該像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使用:

直接看

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>(true,null) {

@Override

public void onSuccess(ResponseBean response) {

MyLog.json(MyJson.toJsonStr(response.data));

}

@Override

public void onError(String msgCanShow) {

MyLog.e(msgCanShow);

}

});

String url2 = “”;

HttpUtil.download(url2)

.setFileDownlodConfig(

FileDownlodConfig.newBuilder()

.verifyBySha1(“76DAB206AE43FB81A15E9E54CAC87EA94BB5B384”)

.isOpenAfterSuccess(true)

.build())

.callback(new MyNetCallback() {

@Override

public void onSuccess(ResponseBean 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(true,null) {

@Override

public void onSuccess(ResponseBean response) {

MyLog.i(response.data);

}

@Override

public void onError(String msgCanShow) {

MyLog.e(msgCanShow);

}

});

5.2 自動處理生命週期

原始時代:

本庫使用的方式.

用靜態map儲存activity/fragment物件和請求, activity/fragment destory時,從map中取出請求,判斷狀態,進行cancel.

public static void cancelByTag(Object obj) {

if (obj == null) {

return;

}

List 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 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,我還是把這個功能實現了–>

其實也不是實現,只是把服務端常用的功能遷移到移動端,並進行了適配. 做了一點微小的工作.

請看:

要移植到Android,需要考慮java8相容性問題,效能(方法耗時),以及對apk大小的影響,預設使用的是Apache BVal 1.1.2.

String errorMsg = BeanValidator.validate(bean);

//返回的errorMsg為空就說明校驗透過

if(!TextUtils.isEmpty(errorMsg)){

(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)

//快取策略,分類參考:

//不使用快取,該模式下,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;//預設是做持久化操作

public GlobalConfig setCookieMode(int cookieMode) {

this.cookieMode = cookieMode;

return this;

}

5.7 公共請求頭,請求引數/請求體引數

可初始化時用map儲存,每次請求時加入:

如果值初始化後就不變,那推薦使用這種方式.

如果會變化,就不能用這種.或者變化後更新快取的map.

也可以利用okhttp的攔截器,在攔截器里加入.

對於請求頭,get請求,很簡單就實現了

但對於post json或者multiPart,就需要將json再變成map,然後加入,將multiPart還原,再加入.

可參考:

如果涉及到請求體簽名,那麼務必將此攔截器加到簽名攔截器之前.

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() {

@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 void onError(MyNetCallback 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環境忽略證照

    或者直接網路框架在debug環境忽略證照

  • stetho-> flipper

    基於okhttp攔截器,抓包內容傳送到pc上的客戶端顯示. 顯示介面更高階大氣上檔次.

    改寫flipper內建的攔截器,有額外加密的,解密後發明文過去顯示.

    一行指令碼整合:

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 參考:

自定義加密

攔截器裡拿到請求體位元組陣列,加密,再構建新的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 uploadImgs(String type, final List filePaths){

final List infos = new ArrayList();

io.reactivex.Observable observable =

HttpUtil.requestAsJsonArray(getUploadTokenPath,S3Info.class)

.get()

.addParam(“type”, type)

.addParam(“contentType”, IMAGE_JPEG)

.addParam(“cnt”,filePaths.size())

.asObservable()

.flatMap(new Function, ObservableSource>() {

@Override

public ObservableSource apply(ResponseBean bean) throws Exception {

infos.addAll(bean.bean);

List> observables = new ArrayList();

for(int i = 0; i

S3Info info = bean.bean.get(i);

String filePath = filePaths.get(i);

io.reactivex.Observable 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實現通用的聚合介面請求.

每個介面可配置能否接受失敗

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3549/viewspace-2796691/,如需轉載,請註明出處,否則將追究法律責任。

相關文章