關於網路框架設計封裝的扯淡
關於網路框架設計封裝的扯淡
本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
@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
@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
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.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
final List infos = new ArrayList();
io.reactivex.Observable
HttpUtil.requestAsJsonArray(getUploadTokenPath,S3Info.class)
.get()
.addParam(“type”, type)
.addParam(“contentType”, IMAGE_JPEG)
.addParam(“cnt”,filePaths.size())
.asObservable()
.flatMap(new Function
@Override
public ObservableSource bean) throws Exception {
infos.addAll(bean.bean);
List
for(int i = 0; i
S3Info info = bean.bean.get(i);
String filePath = filePaths.get(i);
io.reactivex.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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 基於retrofit的網路框架的終極封裝(一):第一層引數組裝層的API設計框架封裝API
- Flutter 網路請求框架封裝Flutter框架封裝
- 最簡易的網路框架封裝(新手可看)框架封裝
- 關於對小程式網路請求的封裝(詳盡版)封裝
- 【JavaScript框架封裝】公共框架的封裝JavaScript框架封裝
- Flutter 基於Bloc框架的封裝FlutterBloC框架封裝
- 基於vue-resource的網路層封裝Vue封裝
- 【JavaScript框架封裝】實現一個類似於JQuery的動畫框架的封裝JavaScript框架封裝jQuery動畫
- 基於Tencent封裝的通用UI框架封裝UI框架
- 網路封裝APi封裝API
- Flutter 熱門網路請求框架Dio的簡單封裝Flutter框架封裝
- 【JavaScript框架封裝】實現一個類似於JQuery的CSS樣式框架的封裝JavaScript框架封裝jQueryCSS
- Retrofit + Kotlin + MVVM 的網路請求框架的封裝嘗試KotlinMVVM框架封裝
- javascript物件導向程式設計關於封裝簡單介紹JavaScript物件程式設計封裝
- 基於FutureBuilder通用網路請求介面封裝Rebuild封裝
- 開源一個封裝AFNetworking的網路框架 - SJNetwork封裝框架
- 開源一個封裝AFNetworking的網路框架 – SJNetwork封裝框架
- 關於php面向的特性之封裝PHP封裝
- 關於程式設計師寫文件(網路轉載)程式設計師
- 關於cp網站建設BC搭建制作app封裝的一些分享網站APP封裝
- vue專案的網路模組封裝Vue封裝
- 網路圖片載入的封裝封裝
- OC:封裝網路請求封裝
- 關於緩動動畫函式的封裝動畫函式封裝
- 封裝框架的實踐封裝框架
- 一步步封裝實現自己的網路請求框架封裝框架
- 專案重構之網路篇 根據OkHttp封裝簡單的框架HTTP封裝框架
- 基於retrofit的網路框架的終極封裝(二)-與retrofit的對接與解耦,以及遇到的坑框架封裝解耦
- 基於gin框架封裝的web專案骨架goskeleton框架封裝WebGo
- 網路擁塞裝置的設計
- 關於《.NET 框架設計》書中 Demo 的更正 (二)框架
- LXNetwork – 基於AF3.0封裝的iOS網路請求庫封裝iOS
- 關於計算機網路的 Wireshark 實驗計算機網路
- uni-app網路請求的封裝APP封裝
- iOS網路層封裝(基於AFNetworking3 0)iOS封裝
- 關於Vue中常用的工具函式封裝Vue函式封裝
- 關於資料庫操作的封裝程式碼資料庫封裝
- 小程式-網路請求封裝封裝