WebSocket安卓客戶端實現詳解(二)–客戶端發送請求
本篇接着上一篇講解WebSocket客戶端發送請求和服務端主動通知消息,還沒看過第一篇的小夥伴,這裏附上第一篇鏈接WebSocket安卓客戶端實現詳解(一)–連接建立與重連.
本篇依舊乾貨十足內容很多所以我們還是先熱身下
客戶端發送請求
爲了方便我們協議使用json格式,當然你也可以替換成ProtoBuffer.
這裏先展示下發送請求的流程圖,大體流程在第一篇已經講過了這裏不再贅述,直接摟細節.
既然是請求,那麼得有一個請求協議.這裏我們不直接展示協議,而是通過http請求,引導到websocket請求協議.
一般http請求我們會需要如下幾個參數
url
請求參數
回調
而websocket是一個長連接,我們所有的請求都是通過這個連接發送的,也就是說我們沒法像http那樣通過一個url來區分不同請求,那麼我們請求協議中就必須要有一個類似url的東西去區分請求,這裏我用action字段來表示url,爲了方便協議的擴展,我們在加一個參數req_event,由action和req_event共同組成url,下面是一個簡單的例子.
{
"action": " login",
"req_event": 0
}
由於請求是異步的所以當我們請求的時候需要把回調加入集合保存起來,等到服務端響應的時候我們需要從集合中找到對應回調調用對應的方法,那麼爲了能夠找到對應回調我們需要給每個請求增加一個唯一標識,這裏我用的seq_id字段表示,當請求的時候我們把客戶端自己生成的seq_id傳給服務端,然後當服務端響應的時候在回傳給我們,這樣我們就能通過seq_id作爲key找到對應的回調,那麼現在協議將變成下面這樣.
{
"action": " login",
"req_event": 0,
"seq_id":0
}
在加上每個請求都有的請求參數,這裏我用req字段表示,最終協議如下.
{
"action": " login",
"req_event": 0,
"seq_id":0,
"req":{
"aaa":"aaa",
"bbb":0
}
}
協議有了,那我們談談代碼的實現,對於請求方法我們對外暴露如下參數.
Action
這裏我把action、req_event、響應實體統一用一個枚舉類Action來存儲,調用者只需根據不同請求傳入對應Action即可.
Req
請求參數
Callback
回調
timeout(可選參數,默認給的10秒)
請求超時時間.
show code
Action一個枚舉類,把action、req_event、響應實體統一封裝在一起,action和req_event用來組裝url,響應實體在反序列化的時候會用到.調用者只需根據不同請求傳入對應Action即可.
public enum Action {
LOGIN("login", 1, null);
private String action;
private int reqEvent;
private Class respClazz;
Action(String action, int reqEvent, Class respClazz) {
this.action = action;
this.reqEvent = reqEvent;
this.respClazz = respClazz;
}
public String getAction() {
return action;
}
public int getReqEvent() {
return reqEvent;
}
public Class getRespClazz() {
return respClazz;
}
}
ui層回調
public interface ICallback<T> {
void onSuccess(T t);
void onFail(String msg);
}
協議對應的請求實體類Request,這裏還額外添加了一個沒有參加序列化的參數reqCount來表示該請求的請求次數,在後面請求失敗情況下對該請求的處理會用上.
public class Request<T> {
@SerializedName("action")
private String action;
@SerializedName("req_event")
private int reqEvent;
@SerializedName("seq_id")
private long seqId;
@SerializedName("req")
private T req;
private transient int reqCount;
public Request(String action, int reqEvent, long seqId, T req, int reqCount) {
this.action = action;
this.reqEvent = reqEvent;
this.seqId = seqId;
this.req = req;
this.reqCount = reqCount;
}
....
//這裏還有各個參數對應get、set方法,爲節省篇幅省略了
public static class Builder<T> {
private String action;
private int reqEvent;
private long seqId;
private T req;
private int reqCount;
public Builder action(String action) {
this.action = action;
return this;
}
public Builder reqEvent(int reqEvent) {
this.reqEvent = reqEvent;
return this;
}
public Builder seqId(long seqId) {
this.seqId = seqId;
return this;
}
public Builder req(T req) {
this.req = req;
return this;
}
public Builder reqCount(int reqCount) {
this.reqCount = reqCount;
return this;
}
public Request build() {
return new Request<T>(action, reqEvent, seqId, req, reqCount);
}
}
}
WsManager中發送請求代碼
public class WsManager{
....跟之前相同代碼省略.....
private static final int REQUEST_TIMEOUT = 10000;//請求超時時間
private AtomicLong seqId = new AtomicLong(SystemClock.uptimeMillis());//每個請求的唯一標識
public void sendReq(Action action, Object req, ICallback callback) {
sendReq(action, req, callback, REQUEST_TIMEOUT);
}
public void sendReq(Action action, Object req, ICallback callback, long timeout) {
sendReq(action, req, callback, timeout, 1);
}
/**
* @param action Action
* @param req 請求參數
* @param callback 回調
* @param timeout 超時時間
* @param reqCount 請求次數
*/
@SuppressWarnings("unchecked")
private <T> void sendReq(Action action, T req, ICallback callback, long timeout, int reqCount) {
if (!isNetConnect()) {
callback.onFail("網絡不可用");
return;
}
Request request = new Request.Builder<T>()
.action(action.getAction())
.reqEvent(action.getReqEvent())
.seqId(seqId.getAndIncrement())
.reqCount(reqCount)
.req(req)
.build();
Logger.t(TAG).d("send text : %s", new Gson().toJson(request));
ws.sendText(new Gson().toJson(request));
}
....跟之前相同代碼省略.....
}
sendReq(Action action, T req, ICallback callback, long timeout, int reqCount)
中有reqCount參數是因爲在後面請求失敗情況下會根據該請求進行請求的次數執行不同的策略.
超時和回調的處理
發送請求ok了接下來需要處理回調的問題了.雖然在方法sendReq(Action action, T req, ICallback callback, long timeout, int reqCount)
中我們已經傳入了ui層的回調ICallback,但這裏還需要在封裝一層回調處理一些通用邏輯,然後再調用ICallback對應方法.
需要處理的通用邏輯有如下三個.
請求成功
將服務端返回數據從子線程傳到主線程,然後調用ui層回調.
請求失敗
將失敗信息從子線程傳到主線程,然後調用ui層回調.
請求超時
該請求的reqCount <= 3再次通過websocket發送請求,reqCount > 3通過http通道發送請求,根據結果直接調用對應回調.
中間層回調定義如下
public interface IWsCallback<T> {
void onSuccess(T t);
void onError(String msg, Request request, Action action);
void onTimeout(Request request, Action action);
}
onSuccess與普通的成功回調一樣,onError和onTimeout回調中有Request與Action是爲了方便後續再次請求操作.
接下來showcode.
....跟之前相同代碼省略.....
private final int SUCCESS_HANDLE = 0x01;
private final int ERROR_HANDLE = 0x02;
private Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SUCCESS_HANDLE:
CallbackDataWrapper successWrapper = (CallbackDataWrapper) msg.obj;
successWrapper.getCallback().onSuccess(successWrapper.getData());
break;
case ERROR_HANDLE:
CallbackDataWrapper errorWrapper = (CallbackDataWrapper) msg.obj;
errorWrapper.getCallback().onFail((String) errorWrapper.getData());
break;
}
}
};
private <T> void sendReq(Action action, T req, final ICallback callback, final long timeout, int reqCount) {
if (!isNetConnect()) {
callback.onFail("網絡不可用");
return;
}
Request request = new Request.Builder<T>()
.action(action.getAction())
.reqEvent(action.getReqEvent())
.seqId(seqId.getAndIncrement())
.reqCount(reqCount)
.req(req)
.build();
IWsCallback temp = new IWsCallback() {
@Override
public void onSuccess(Object o) {
mHandler.obtainMessage(SUCCESS_HANDLE, new CallbackDataWrapper(callback, o))
.sendToTarget();
}
@Override
public void onError(String msg, Request request, Action action) {
mHandler.obtainMessage(ERROR_HANDLE, new CallbackDataWrapper(callback, msg))
.sendToTarget();
}
@Override
public void onTimeout(Request request, Action action) {
timeoutHandle(request, action, callback, timeout);
}
};
Logger.t(TAG).d("send text : %s", new Gson().toJson(request));
ws.sendText(new Gson().toJson(request));
}
/**
* 超時處理
*/
private void timeoutHandle(Request request, Action action, ICallback callback, long timeout) {
if (request.getReqCount() > 3) {
Logger.t(TAG).d("(action:%s)連續3次請求超時 執行http請求", action.getAction());
//走http請求
} else {
sendReq(action, request.getReq(), callback, timeout, request.getReqCount() + 1);
Logger.t(TAG).d("(action:%s)發起第%d次請求", action.getAction(), request.getReqCount());
}
}
....跟之前相同代碼省略.....
Handler中獲取的Callback與Data包裝類如下
public class CallbackDataWrapper<T> {
private ICallback<T> callback;
private Object data;
public CallbackDataWrapper(ICallback<T> callback, Object data) {
this.callback = callback;
this.data = data;
}
public ICallback<T> getCallback() {
return callback;
}
public void setCallback(ICallback<T> callback) {
this.callback = callback;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
從上面代碼中可以看出對於需要處理的邏輯1和2在回調中通過CallbackDataWrapper包裝callback和data發送到主線程handler在調用對應的回調處理.
需要處理的邏輯3則在timeoutHandle()
方法中實現,具體http請求這裏我沒實現,大家只需要調用自己項目封裝的http請求api即可,當然爲了方便這裏建議還是以我們前面定義的websocket協議的形式執行http請求,這樣我們就不需要重新組裝請求參數了.
超時後的處理有了,接下來我們實現添加超時任務代碼.
....跟之前相同代碼省略.....
private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private Map<Long, CallbackWrapper> callbacks = new HashMap<>();
@SuppressWarnings("unchecked")
private <T> void sendReq(Action action, T req, final ICallback callback, final long timeout, int reqCount) {
if (!isNetConnect()) {
callback.onFail("網絡不可用");
return;
}
Request request = new Request.Builder<T>()
.action(action.getAction())
.reqEvent(action.getReqEvent())
.seqId(seqId.getAndIncrement())
.reqCount(reqCount)
.req(req)
.build();
ScheduledFuture timeoutTask = enqueueTimeout(request.getSeqId(), timeout);//添加超時任務
IWsCallback tempCallback = new IWsCallback() {
@Override
public void onSuccess(Object o) {
mHandler.obtainMessage(SUCCESS_HANDLE, new CallbackDataWrapper(callback, o))
.sendToTarget();
}
@Override
public void onError(String msg, Request request, Action action) {
mHandler.obtainMessage(ERROR_HANDLE, new CallbackDataWrapper(callback, msg))
.sendToTarget();
}
@Override
public void onTimeout(Request request, Action action) {
timeoutHandle(request, action, callback, timeout);
}
};
callbacks.put(request.getSeqId(),
new CallbackWrapper(tempCallback, timeoutTask, action, request));
Logger.t(TAG).d("send text : %s", new Gson().toJson(request));
ws.sendText(new Gson().toJson(request));
}
/**
* 添加超時任務
*/
private ScheduledFuture enqueueTimeout(final long seqId, long timeout) {
return executor.schedule(new Runnable() {
@Override
public void run() {
CallbackWrapper wrapper = callbacks.remove(seqId);
if (wrapper != null) {
Logger.t(TAG).d("(action:%s)第%d次請求超時", wrapper.getAction().getAction(), wrapper.getRequest().getReqCount());
wrapper.getTempCallback().onTimeout(wrapper.getRequest(), wrapper.getAction());
}
}
}, timeout, TimeUnit.MILLISECONDS);
}
....跟之前相同代碼省略.....
這裏我們用的ScheduledThreadPoolExecutor
來處理超時任務,通過ScheduledThreadPoolExecutor.schedule()
方法間隔一定時長執行超時的處理.當然爲了能進行超時的處理我們需要將callback保存起來,又由於超時的時候我們需要進行再次請求所以不僅需要callback還需要request與action,至於執行超時任務返回的ScheduledFuture這個我們會在服務端響應的時候取消超時任務用上這邊我提前添加了.最終通過CallbackWrapper將他們包裝了起來.
callback包裝類
public class CallbackWrapper {
private final IWsCallback tempCallback;
private final ScheduledFuture timeoutTask;
private final Action action;
private final Request request;
public CallbackWrapper(IWsCallback tempCallback, ScheduledFuture timeoutTask, Action action, Request request) {
this.tempCallback = tempCallback;
this.timeoutTask = timeoutTask;
this.action = action;
this.request = request;
}
public IWsCallback getTempCallback() {
return tempCallback;
}
public ScheduledFuture getTimeoutTask() {
return timeoutTask;
}
public Action getAction() {
return action;
}
public Request getRequest() {
return request;
}
}
接下來是請求的最後一步了服務端響應的處理.這裏我們還是先從協議入手.
首先說明下對於長連接無論是我們請求的響應還是服務端的主動通知都是通過同一個回調方法回調給客戶端我們沒法區分,所以服務端給我們數據中需要帶一個標誌來區分普通請求的響應與服務端主動通知.這裏我們用resp_event來表示,當resp_event爲10的時候是請求的響應,爲20的時候是服務端的主動通知.
對於請求的響應我們需要通過seq_id找到對應的回調,對於主動通知我們需要添加一個action來區分這個通知執行什麼操作,在加上響應數據體resp協議就定製完成了.當然seq_id應該在客戶端請求的響應的時候纔有,action應該在服務端主動通知的時候纔有.這裏爲了解析的方便最外層通用協議如下.
{
"resp_event": 10,
"action": "",
"seq_id": 11111111,
"resp": {}
}
對於我們客戶端主動請求的響應數據可以按照http請求的格式來定義code,msg,data那麼最終數據結構如下
{
"resp_event": 10,
"action": "",
"seq_id": 11111111,
"resp": {
"code":0,
"msg":"ok",
"data":{
}
}
}
對於服務端主動的推送不需要code和msg那麼數據結構改爲如下
{
"resp_event": 20,
"action": "",
"seq_id": 11111111,
"resp": {
}
}
}
可以看出最外層json數據結構一樣,從resp開始不同.這裏對於客戶端主動請求的響應的情況我已經封裝好了Codec工具類去解析,服務端主動推送的話後面再說。
最外層bean如下
public class Response {
@SerializedName("resp_event")
private int respEvent;
@SerializedName("seq_id")
private String seqId;
private String action;
private String resp;
//省略get set方法
}
第二層bean如下
public class ChildResponse {
private int code;
private String msg;
private String data;
public boolean isOK(){
return code == 0;
}
//省略get set方法
}
對應的解析工具類
public class Codec {
public static Response decoder(String text) {
Response response = new Response();
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(text);
if (element.isJsonObject()) {
JsonObject obj = (JsonObject) element;
response.setRespEvent(decoderInt(obj, "resp_event"));
response.setAction(decoderStr(obj, "action"));
response.setSeqId(decoderStr(obj, "seq_id"));
response.setResp(decoderStr(obj, "resp"));
return response;
}
return response;
}
private static int decoderInt(JsonObject obj, String name) {
int result = -1;
JsonElement element = obj.get(name);
if (null != element) {
try {
result = element.getAsInt();
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
private static String decoderStr(JsonObject obj, String name) {
String result = "";
try {
JsonElement element = obj.get(name);
if (null != element && element.isJsonPrimitive()) {
result = element.getAsString();
} else if (null != element && element.isJsonObject()) {
result = element.getAsJsonObject().toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
public static ChildResponse decoderChildResp(String jsonStr) {
ChildResponse childResponse = new ChildResponse();
JsonParser parser = new JsonParser();
JsonElement element = parser.parse(jsonStr);
if (element.isJsonObject()) {
JsonObject jsonObject = (JsonObject) element;
childResponse.setCode(decoderInt(jsonObject, "code"));
childResponse.setMsg(decoderStr(jsonObject, "msg"));
childResponse.setData(decoderStr(jsonObject, "data"));
}
return childResponse;
}
}
接下來就是響應的處理了
....跟之前相同代碼省略.....
class WsListener extends WebSocketAdapter {
@Override
public void onTextMessage(WebSocket websocket, String text) throws Exception {
super.onTextMessage(websocket, text);
Logger.t(TAG).d("receiverMsg:%s", text);
Response response = Codec.decoder(text);//解析出第一層bean
if (response.getRespEvent() == 10) {//響應
CallbackWrapper wrapper = callbacks.remove(Long.parseLong(response.getSeqId()));//找到對應callback
if (wrapper == null) {
Logger.t(TAG).d("(action:%s) not found callback", response.getAction());
return;
}
try {
wrapper.getTimeoutTask().cancel(true);//取消超時任務
ChildResponse childResponse = Codec.decoderChildResp(response.getResp());//解析第二層bean
if (childResponse.isOK()) {
Object o = new Gson().fromJson(childResponse.getData(),
wrapper.getAction().getRespClazz());
wrapper.getTempCallback().onSuccess(o);
} else {
wrapper.getTempCallback()
.onError(ErrorCode.BUSINESS_EXCEPTION.getMsg(), wrapper.getRequest(),
wrapper.getAction());
}
} catch (JsonSyntaxException e) {
e.printStackTrace();
wrapper.getTempCallback()
.onError(ErrorCode.PARSE_EXCEPTION.getMsg(), wrapper.getRequest(),
wrapper.getAction());
}
} else if (response.getRespEvent() == 20) {//通知
}
}
}
....跟之前相同代碼省略.....
先解析出第一層bean然後根據respEvent判斷響應類型,當respEvent爲10的時候通過seqId找對應的callback如果找到了則取消超時任務,解析第二層bean判斷code是否爲成功,失敗調用失敗回調,成功用gson解析對應的bean然後調用成功回調.
錯誤信息的封裝ErrorCode如下
public enum ErrorCode {
BUSINESS_EXCEPTION("業務異常"),
PARSE_EXCEPTION("數據格式異常"),
DISCONNECT_EXCEPTION("連接斷開");
private String msg;
ErrorCode(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
授權和心跳
發送請求封裝好了接下來我們可以開始授權和心跳了,這裏我們先回顧下用戶登錄流程.
可以看到我們連接成功後進行授權,授權就是發送一個攜帶用戶信息的請求然後服務端通過這個請求驗證用戶信息,驗證成功後服務端就知道了當前長連接屬於哪個用戶,由於我們是先通過http請求進行的登錄然後在通過websocket進行的授權,既然http登錄能成功所以正常情況下通過websocket進行授權不會有問題,所以這裏錯誤回調沒有進行處理.
這裏我們模擬這個過程,首先是定義請求的action,在action中添加請求對應的action,reqEvent,respClazz這個比較簡單我就省略了,然後在連接成功回調進行授權請求doAuth()
public class WsManager {
....跟之前相同代碼省略.....
private void doAuth() {
sendReq(Action.LOGIN, null, new ICallback() {
@Override
public void onSuccess(Object o) {
Logger.t(TAG).d("授權成功");
setStatus(WsStatus.AUTH_SUCCESS);
}
@Override
public void onFail(String msg) {
}
});
}
/**
* 繼承默認的監聽空實現WebSocketAdapter,重寫我們需要的方法
* onTextMessage 收到文字信息
* onConnected 連接成功
* onConnectError 連接失敗
* onDisconnected 連接關閉
*/
class WsListener extends WebSocketAdapter {
@Override
public void onConnected(WebSocket websocket, Map<String, List<String>> headers)
throws Exception {
super.onConnected(websocket, headers);
Logger.t(TAG).d("連接成功");
setStatus(WsStatus.CONNECT_SUCCESS);
cancelReconnect();//連接成功的時候取消重連,初始化連接次數
doAuth();
}
}
....跟之前相同代碼省略.....
}
授權成功後接下來是心跳和同步數據,本質跟授權一樣也是發送請求.
這裏單獨說下心跳,心跳其實就是每隔一段時間我們請求服務端然後服務端有響應我們就認爲這條連接是穩定的,對於websocket其實已經定義了心跳的格式ping和pong,爲了方便這裏我就直接使用我們自己定義的協議發送的心跳請求,本質上跟ping和pong是一樣的只是協議不同罷了.對於心跳重連策略我這裏做的比較簡單按一個固定時間執行心跳請求,當心跳連續失敗3次就進行重連.
首先還是定義action.這裏只是模擬所以respClazz都是null
public enum Action {
LOGIN("login", 1, null),
HEARTBEAT("heartbeat", 1, null),
SYNC("sync", 1, null);
private String action;
private int reqEvent;
private Class respClazz;
Action(String action, int reqEvent, Class respClazz) {
this.action = action;
this.reqEvent = reqEvent;
this.respClazz = respClazz;
}
public String getAction() {
return action;
}
public int getReqEvent() {
return reqEvent;
}
public Class getRespClazz() {
return respClazz;
}
}
心跳和同步
public class WsManager {
....跟之前相同代碼省略.....
private static final long HEARTBEAT_INTERVAL = 30000;//心跳間隔
private Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SUCCESS_HANDLE:
CallbackDataWrapper successWrapper = (CallbackDataWrapper) msg.obj;
successWrapper.getCallback().onSuccess(successWrapper.getData());
break;
case ERROR_HANDLE:
CallbackDataWrapper errorWrapper = (CallbackDataWrapper) msg.obj;
errorWrapper.getCallback().onFail((String) errorWrapper.getData());
break;
}
}
};
private void doAuth() {
sendReq(Action.LOGIN, null, new ICallback() {
@Override
public void onSuccess(Object o) {
Logger.t(TAG).d("授權成功");
setStatus(WsStatus.AUTH_SUCCESS);
startHeartbeat();
delaySyncData();
}
@Override
public void onFail(String msg) {
}
});
}
//同步數據
private void delaySyncData() {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
sendReq(Action.SYNC, null, new ICallback() {
@Override
public void onSuccess(Object o) {
}
@Override
public void onFail(String msg) {
}
});
}
}, 300);
}
private void startHeartbeat() {
mHandler.postDelayed(heartbeatTask, HEARTBEAT_INTERVAL);
}
private void cancelHeartbeat() {
heartbeatFailCount = 0;
mHandler.removeCallbacks(heartbeatTask);
}
private int heartbeatFailCount = 0;
private Runnable heartbeatTask = new Runnable() {
@Override
public void run() {
sendReq(Action.HEARTBEAT, null, new ICallback() {
@Override
public void onSuccess(Object o) {
heartbeatFailCount = 0;
}
@Override
public void onFail(String msg) {
heartbeatFailCount++;
if (heartbeatFailCount >= 3) {
reconnect();
}
}
});
mHandler.postDelayed(this, HEARTBEAT_INTERVAL);
}
};
private int reconnectCount = 0;//重連次數
private long minInterval = 3000;//重連最小時間間隔
private long maxInterval = 60000;//重連最大時間間隔
public void reconnect() {
if (!isNetConnect()) {
reconnectCount = 0;
Logger.t(TAG).d("重連失敗網絡不可用");
return;
}
//這裏其實應該還有個用戶是否登錄了的判斷 因爲當連接成功後我們需要發送用戶信息到服務端進行校驗
//由於我們這裏是個demo所以省略了
if (ws != null &&
!ws.isOpen() &&//當前連接斷開了
getStatus() != WsStatus.CONNECTING) {//不是正在重連狀態
reconnectCount++;
setStatus(WsStatus.CONNECTING);
cancelHeartbeat();//取消心跳
long reconnectTime = minInterval;
if (reconnectCount > 3) {
url = DEF_URL;
long temp = minInterval * (reconnectCount - 2);
reconnectTime = temp > maxInterval ? maxInterval : temp;
}
Logger.t(TAG).d("準備開始第%d次重連,重連間隔%d -- url:%s", reconnectCount, reconnectTime, url);
mHandler.postDelayed(mReconnectTask, reconnectTime);
}
}
....跟之前相同代碼省略.....
}
授權成功後開始心跳和同步數據,心跳連續失敗三次開始重連,在重連時候會取消心跳.當重連成功的時候在連接成功回調會再次進行授權然後授權成功後會再次開啓心跳就形成了一個循環.
感覺篇幅又有些長了..爲了避免大夥疲勞將服務端主動通知放第三篇WebSocket安卓客戶端實現詳解(三)–服務端主動通知好了,並且WebSocket整個模塊我覺得我寫的最好的就是服務端主動通知這部分還請各位看官一定要捧場啊.