WebSocket 安卓客戶端實現及程式碼封裝
介紹
關於 Socket/">WebSocket Android 端的使用封裝之前已經做過一次了,但在使用了一段時間之後逐漸發現了一些問題,一直想改也沒時間,正好最近公司業務比較少,就趁著這段時間有空閒把程式碼優化了一下,其實差不多是重新做一套了。
這個版本的使用方式上比之前簡化了很多,整合起來也更容易,並且程式碼邏輯更加清晰,模組與模組之間的耦合降到最低,執行效率更高,更健壯,好了廢話不說了,先介紹一下使用方式。
如何使用
先放上 Github 地址:
ofollow,noindex">https://github.com/0xZhangKe/WebSocketDemo
好了,首先將程式碼整合到自己的專案中,這裡有兩種整合方式,第一種是使用 Gradle 依賴這個專案既可,第二種把程式碼拷貝到自己專案中,我建議使用第二種方式,這樣你覺得有什麼問題自己改起來比較方便,當然了也可以直接給我 提 issue 我來改。
整合
Gradle 方式整合
在對應 model 的 build.gradle 中新增依賴:
implementation 'com.github.0xZhangKe:WebSocketDemo:2.0'
然後編譯一下,如果出現類似的錯誤:
Failed to resolve: com.github.0xZhangKe:WebSocketDemo:2.0
那意味著你還沒新增 Github 的倉庫,到專案根目錄中的 build.gradle 中新增如下程式碼:
maven { url = 'https://jitpack.io' }
然後 sync 一下即可。
第二種整合方式
這個就很簡單了,直接把 websocketlib 中的程式碼拷貝到自己的專案中就行,具體怎麼做就看你的個人喜好。
相關配置
按照上面的步驟整合進來之後再做一些簡單的配置可以使用了。
配置 WebSocket 連線地址
首先,最重要的一點,配置 WebSocket 連線地址:
WebSocketSetting.setConnectUrl("Your WebSocket connect url");
這一步必須在啟動 WebSocketService 使用前呼叫,我是在 Application 中配置的,建議你們也這麼做,可以看一下 demo 的使用方式。
這一步配置完成後一個簡單的 WebSocketService 就可以使用了。
配置統一的訊息處理器
在我們實際開發中可能需要考慮更多的問題,比如資料格式的統一規劃,後臺返回資料的統一處理,處理完成後再發送到下游等等。
機智的我早就想到了解決方案,本專案中使用 IResponseDispatcher 來分發資料,可以看到這是個介面,預設會使用 DefaultResponseDispatcher 來當做訊息分發器,如果不進行設定 WebSocket 接收到資料後會直接傳送給下游。
那麼我們先來看一下 IResponseDispatcher:
public interface IResponseDispatcher { //省略掉其他程式碼 /** * 接收到訊息 * * @param message 接收到的訊息 * @param delivery 訊息發射器 */ void onMessageResponse(Response message, ResponseDelivery delivery); //省略掉其他程式碼 }
IResponseDispatcher 共中有五個方法需要實現,大體上都類似的,我們只看其中一個就行。
onMessageResponse 方法中的兩個引數,Response 後面會介紹,這裡說一下 ResponseDelivery,我管它叫訊息發射器,其實很簡單,他內部就是維護了一個監聽器的 List,當呼叫其中某個方法時會遍歷呼叫所有的 Listener 中對應的方法。
當我們處理完資料之後通過這個就可以將資料傳送到下游的 Activity/Fragment 中,很簡單的吧,當然也可以對訊息進行攔截,或者將資料包裝成統一的格式再發送出去。
舉個栗子,我們要將資料轉成統一的一個實體在傳送到下游,那麼在實現類中可以這麼做:
@Override public void onMessageResponse(Response message, ResponseDelivery delivery) { delivery.onMessageResponse(new CommonResponse(message.getResponseText(), JSON.parseObject(message.getResponseText(), new TypeReference<CommonResponseEntity>() { }))); }
上面是把 Response 中的訊息資料轉成我們根據後臺資料統一格式自定義的 CommonResponseEntity 物件再包裝成一個自定義的 CommonResponse 物件傳送出去。
除此之外,更重要的一點是,當我們將訊息資料轉成 CommonResponseEntity 之後可以根據業務邏輯來進行統一的處理,例如後臺規定返回資料中的 code 欄位等於 1000 時才代表介面呼叫成功,那麼我們就可以直接在這裡做判斷了,而不是每個地方都要判斷一次:
@Override public void onMessageResponse(Response message, ResponseDelivery delivery) { try { CommonResponse commonResponse = new CommonResponse(message.getResponseText(), JSON.parseObject(message.getResponseText(), new TypeReference<CommonResponseEntity>() { })); if (commonResponse.getResponseEntity().getCode() >= 1000) { delivery.onMessageResponse(commonResponse); } else { ErrorResponse errorResponse = new ErrorResponse(); errorResponse.setErrorCode(12); errorResponse.setDescription(commonResponse.getResponseEntity().getMsg()); errorResponse.setResponseText(message.getResponseText()); //將已經解析好的 CommonResponseEntity 物件儲存起來以便後面使用 errorResponse.setReserved(responseEntity); //IResponseDispatcher內的一個方法,表示接收到錯誤訊息,通過errorCode指定錯誤型別 onSendMessageError(errorResponse, delivery); } } catch (JSONException e) { ErrorResponse errorResponse = new ErrorResponse(); errorResponse.setResponseText(message.getResponseText()); errorResponse.setErrorCode(11); errorResponse.setCause(e); onSendMessageError(errorResponse, delivery); } }
onSendMessageError 方法後面會介紹
大概就是按照上面來實現,更詳細的用法可以看 demo 中是怎麼做的。
配置統一的訊息資料型別
一般來說,後臺介面返回的資料是有個固定的格式的,通過上面的介紹我們已經瞭解到如何把資料轉換成統一的型別傳送到下游,下面我們先來簡單的瞭解一下 Response ,我這裡將所有後臺返回的資料統一包裝成一個 Response 物件,這是一個介面,你可以根據自己的需要來實現它:
/** * WebSocket 響應資料介面 * Created by ZhangKe on 2018/6/26. */ public interface Response<T> { /** * 獲取響應的文字資料 */ String getResponseText(); /** * 設定響應的文字資料 */ void setResponseText(String responseText); /** * 獲取該資料的實體,可能為空,具體看實現類 */ T getResponseEntity(); /** * 設定資料實體 */ void setResponseEntity(T responseEntity); }
WebSocket 接收到資料後會首先包裝成 TextResponse 物件傳送出去,我們看一下 TextResponse 的程式碼:
/** * 預設的訊息響應事件包裝類, * 只包含文字,不包含資料實體 * Created by ZhangKe on 2018/6/27. */ public class TextResponse implements Response<String> { private String responseText; public TextResponse(String responseText) { this.responseText = responseText; } public String getResponseText() { return responseText; } public void setResponseText(String responseText) { this.responseText = responseText; } public String getResponseEntity() { return null; } public void setResponseEntity(String responseEntity) { } }
可以看到其中只包含了 String 型別的響應資料,沒有對資料做其他操作,接收到什麼就返回什麼,其中的 responseText 表示 WebSocket 接收到的文字資料,除此之外我還提供了兩個用於操作 ResponseEntity 的方法,我們可以將接收到的文字按照統一的格式轉換成一個實體存入這個欄位,然後再發送到下游。
比如後臺介面的資料格式如下:
{ "message": "登陸成功", "data": { "name": "zhangke", "sex": "男", "nationality": "中國" }, "code": 1000, "path": "app_user_login" }
那麼我們可以將資料轉換成一個統一的泛型資料實體:
/** * 後臺介面返回的資料格式 * Created by ZhangKe on 2018/6/27. */ public class CommonResponseEntity { private String message; private String data; private int code; private String path; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getData() { return data; } public void setData(String data) { this.data = data; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
data 欄位中的資料交給對應模組解析,這裡直接轉成 String,然後包裝成一個 CommonResponse 傳送出去:
public class CommonResponse implements Response<CommonResponseEntity> { private String responseText; private CommonResponseEntity responseEntity; public CommonResponse(String responseText, CommonResponseEntity responseEntity) { this.responseText = responseText; this.responseEntity = responseEntity; } @Override public String getResponseText() { return responseText; } @Override public void setResponseText(String responseText) { this.responseText = responseText; } @Override public CommonResponseEntity getResponseEntity() { return this.responseEntity; } @Override public void setResponseEntity(CommonResponseEntity responseEntity) { this.responseEntity = responseEntity; } }
錯誤資訊的處理
剛剛已經介紹瞭如何統一處理訊息及將訊息轉換成對應的實體,下面再說一下如何統一的處理錯誤資訊。
所有的錯誤訊息將統一包裝成 ErrorResponse 物件傳送出去,看一下其中的程式碼:
/** * 出現錯誤時的響應 * Created by ZhangKe on 2018/6/25. */ public class ErrorResponse { /** * 1-WebSocket 未連線或已斷開 * 2-WebSocketService 服務未繫結到當前 Activity/Fragment,或繫結失敗 * 3-WebSocket 初始化未完成 * 11-資料獲取成功,但是解析 JSON 失敗 * 12-資料獲取成功,但是伺服器返回資料中的code值不正確 */ private int errorCode; /** * 錯誤原因 */ private Throwable cause; /** * 傳送的資料,可能為空 */ private String requestText; /** * 響應的資料,可能為空 */ private String responseText; /** * 錯誤描述,客戶端可以通過這個欄位來設定統一的錯誤提示等等 */ private String description; /** * 保留欄位,可以自定義存放任意資料 */ private Object reserved; public ErrorResponse() { } /** * 1-WebSocket 未連線或已斷開 * 2-WebSocketService 服務未繫結到當前 Activity/Fragment,或繫結失敗 * 3-WebSocket 初始化未完成 * 11-資料獲取成功,但是解析 JSON 失敗 * 12-資料獲取成功,但是伺服器返回資料中的code值不正確 */ public int getErrorCode() { return errorCode; } /** * 1-WebSocket 未連線或已斷開 * 2-WebSocketService 服務未繫結到當前 Activity/Fragment,或繫結失敗 * 3-WebSocket 初始化未完成 * 11-資料獲取成功,但是解析 JSON 失敗 * 12-資料獲取成功,但是伺服器返回資料中的code值不正確 */ public void setErrorCode(int errorCode) { this.errorCode = errorCode; } public Throwable getCause() { return cause; } public void setCause(Throwable cause) { this.cause = cause; } public String getRequestText() { return requestText; } public void setRequestText(String requestText) { this.requestText = requestText; } public String getResponseText() { return responseText; } public void setResponseText(String responseText) { this.responseText = responseText; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Object getReserved() { return reserved; } public void setReserved(Object reserved) { this.reserved = reserved; } }
其中包括了五種錯誤型別,處理錯誤訊息時就按照錯誤碼來判斷既可,另外還提供了一個 reserved 保留欄位,這個用法可以看上面的 配置統一的訊息處理器 那一節。
錯誤資訊的處理同樣也在 IResponseDispatcher 中處理,上面已經介紹了其中的 onMessageResponse ,現在再來說一下 onSendMessageError 方法:
/** * 統一處理錯誤資訊, * 介面上可使用 ErrorResponse#getDescription() 來當做提示語 */ @Override public void onSendMessageError(ErrorResponse error, ResponseDelivery delivery) { switch (error.getErrorCode()) { case 1: error.setDescription("網路錯誤"); break; case 2: error.setDescription("網路錯誤"); break; case 3: error.setDescription("網路錯誤"); break; case 11: error.setDescription("資料格式異常"); Log.e(LOGTAG, "資料格式異常", error.getCause()); break; } delivery.onSendMessageError(error); }
其實這裡主要就是用來通過錯誤碼給出不同的錯誤提示,其它的也沒做什麼,也可以在這裡列印一下 Log 啊等等,code==12 時這裡沒有設定提示語,因為 12 表示介面已經請求成功了,但是後臺後臺介面給了錯誤的提示,比如密碼錯誤等等,這時候錯誤資訊應該是介面中給出,當然我們也可以自己來根據業務調整。
關於配置的就是這麼多了,下面在介紹一下如何使用。
使用
我提供了一個 AbsWebSocketActivity 和一個 AbsWebSocketFragment 抽象基類,需要使用 WebSocket 的介面只需要繼承這兩個中的某一個就行,看一下 AbsWebSocketActivity 的程式碼:
/** * 已經綁定了 WebSocketService 服務的 Activity, * <p> * Created by ZhangKe on 2018/6/25. */ public abstract class AbsWebSocketActivity extends AppCompatActivity implements IWebSocketPage { protected final String LOGTAG = this.getClass().getSimpleName(); private WebSocketServiceConnectManager mConnectManager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mConnectManager = new WebSocketServiceConnectManager(this, this); mConnectManager.onCreate(); } @Override public void sendText(String text) { mConnectManager.sendText(text); } /** * 服務繫結成功時的回撥,可以在此初始化資料 */ @Override public void onServiceBindSuccess() { } /** * WebSocket 連線成功事件 */ @Override public void onConnected() { } /** * WebSocket 連接出錯事件 * * @param cause 出錯原因 */ @Override public void onConnectError(Throwable cause) { } /** * WebSocket 連線斷開事件 */ @Override public void onDisconnected() { } @Override protected void onPause() { if (isFinishing()) { mConnectManager.onDestroy(); } super.onPause(); } }
程式碼很簡潔的吧,有關於對 WebSocketService 的繫結、監聽等操作全部放在了 WebSocketServiceConnectManager 類中,這樣規避了程式碼重複問題,如果你想做一下自己的 BaseWebSocketActivity/BaseWebSocketFragment 直接按照這裡面的程式碼實現既可。
AbsWebSocketActivity/AbsWebSocketFragment 中提供了一系列的方法以供使用,大部分方法一般都不需要用的,主要有三個方法要說一下:
public void onServiceBindSuccess();//WebSocketService 服務繫結成功回撥事件,可以在這個回撥方法中初始化一下資料 public void onMessageResponse(Response message);//接收到訊息回撥事件 public void onSendMessageError(ErrorResponse error);//訊息傳送失敗或接收到錯誤訊息事件
onMessageResponse 及 onSendMessageError 方法中的 Response 和 ErrorResponse 引數上面已經介紹過了,另外還有一個 onServiceBindSuccess 方法,表示服務繫結成功,可以開始傳送資料了。
重連機制
連線斷開後會自動重連 20 次,每次間隔 500 毫秒。也可以通過監聽網路連線變化自動重連,這部分我已經寫好了,配置一下既可開啟。
WebSocketSetting.setReconnectWithNetworkChanged(true);
跟上面說的一樣,這個也要在啟動 WebSocketService 之前呼叫。
別忘了在 AndroidManifest.xml 配置廣播和許可權:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <application> <!--省略程式碼--> <receiver android:name="com.zhangke.websocket.NetworkChangedReceiver" /> </application>
好了關於如何配置及使用差不多就這樣了,如果還有哪裡不清楚的隨時可以問我哦,下面在介紹的是其中的原理,不想看的可以直接跳過。
原理
關於原理我就大概的介紹一下,也沒有太多的程式碼,細節部分我就不說了,先說一下設計。
在整個框架中的核心就是 WebSocketThread 執行緒,其內部採用的是訊息驅動型的設計,使用 Looper.loop() 開啟訊息迴圈,其他模組將 WebSocket 的所有操作(訊息傳送、連線、斷開等等)封裝成訊息的形式傳送到該執行緒。
我們來看一下流程圖:

流程圖
Service 在建立一個 WebSocketThread 物件後通過獲取該執行緒的 Handler 來向其傳送控制資訊。
關於重連模組使用的是一個單獨的類 ReconnectManager 來管理,其內部也持有一個 WebSocketThread 物件,當觸發重連事件時通過 Handler 傳送連線訊息既可。
WebSocket 中的各種事件(連線成功、接收到訊息等等)通過監聽器 SocketListener 通知 Service。
WebSocketThread 講完了我在講一下 WebSocketService ,也是比較重要,先看圖:

WebSocketService
上圖描述了 WebSocket 事件從 WebSocketThread 到 WebSocketService 再到 Activity/Fragment 的事件流向,WebSocketService 中通過一個 IResponseDispatcher 介面來分發事件,預設實現為 DefaultResponseDispatcher ,不做任何處理,直接傳送到下游,也可以自己實現從而實現資料攔截、轉換等操作。
好了就說到這裡了,具體的一些細節直接看程式碼就行,還是很清晰的,要是有什麼疑問直接問我也行。
我的微信:

微信二維碼