本篇文章主要介紹自己使用WebSocket實現Android端即時通訊聊天功能的過程,最終我們使用WebSocket實現了兩個客戶端之間的即時通訊聊天功能和直播中的聊天室功能,當然整個WebSocket還是比較複雜的,特別是長鏈接的穩定性方面自己還需加強(感嘆微信的長鏈接真是穩定啊),所以也希望大家共同探討。
關於Socket和WebSocket的區別以及詳細介紹在此就不贅述了,這方面的介紹網上還是比較多的。
1、使用Java-WebSocket框架
首先,本地使用Java-WebSocket框架實現WebSocket客戶端,地址:Java-WebSocket地址,添加依賴:
compile 'org.java-websocket:Java-WebSocket:1.3.8'
Java-WebSocket是一個純java寫的WebSocket客戶端和服務端實現,在客戶端我們需要自己寫一個類繼承Java-WebSocket中的客戶端 WebSocketClient ,實現四個抽象方法和一個構造方法,如下:
public class MyWebSocketClient extends WebSocketClient {
public MyWebSocketClient(URI serverUri) {
super(serverUri);
}
//長鏈接開啓
@Override
public void onOpen(ServerHandshake handshakedata) {
}
//消息通道收到消息
@Override
public void onMessage(String message) {
}
//長鏈接關閉
@Override
public void onClose(int code, String reason, boolean remote) {
}
//鏈接發生錯誤
@Override
public void onError(Exception ex) {
}
}
構造方法中需要傳一個serverUri,需要說明的是WebSocket的鏈接是ws協議的,所以應該是這樣的:
ws:// [ip地址] : [端口號]
點擊super看源碼可以看見如下構造,由此可見Java-WebSocket使用的WebSocket協議版本是RFC 6455(作者在項目主頁也說明了),當然也提供了其他構造更改協議版本。
//源碼中的構造方法
public WebSocketClient( URI serverUri ) {
this( serverUri, new Draft_6455());
}
由於WebSocketClient對象是不能重複使用的,所以我將MyWebSocketClient寫爲單例模式:
private Context mContext;
//選擇懶漢模式
private static MyWebSocketClient mInstance;
//1. 私有構造方法
private MyWebSocketClient(Context context) {
//開啓WebSocket客戶端
super(URI.create("webSocket鏈接"));
this.mContext = context;
}
//2.公開方法,返回單例對象
public static MyWebSocketClient getInstance(Context context) {
//懶漢: 考慮線程安全問題, 兩種方式: 1. 給方法加同步鎖 synchronized, 效率低; 2. 給創建對象的代碼塊加同步鎖
if (mInstance == null) {
synchronized (MyWebSocketClient.class) {
if (mInstance == null) {
mInstance = new MyWebSocketClient(context);
}
}
}
return mInstance;
}
這樣我們就可以從外部對MyWebSocketClient進行初始化並開啓鏈接。
2、開啓WebSocket鏈接
開啓WebSocket鏈接時要特別注意!!!WebSocket有五種狀態,分別是NOT_YET_CONNECTED(還沒有連接), CONNECTING(正在連接), OPEN(打開狀態), CLOSING(正在關閉), CLOSED(已關閉)。由於WebSocketClient對象是不能重複使用的,所以當WebSocket處於CONNECTING、OPEN、CLOSING、 CLOSED這四種狀態時,說明已經被初始化過了,所以此時再次初始化鏈接時會報異常: WebSocketClient objects are not reuseable ; (這裏我剛開始沒有弄清楚,使用的是isConnecting()、isOpen()、isClosing()、isClosed()這四個方法返回的boolean值來判斷狀態,判斷不出來NOT_YET_CONNECTED狀態,然後各種混亂)
//源碼中五種狀態的枚舉
enum READYSTATE {
NOT_YET_CONNECTED, CONNECTING, OPEN, CLOSING, CLOSED
}
//源碼中初始化鏈接的方法,如果狀態不對會報異常
public void connect() {
if( writeThread != null )
throw new IllegalStateException( "WebSocketClient objects are not reuseable" );
writeThread = new Thread(this);
writeThread.setName( "WebSocketConnectReadThread-" + writeThread.getId() );
writeThread.start();
}
上面也可以看到執行connect()時底層會創建一個線程並對其命名,所以並不需要我們自己創建線程。
好了,現在只需要在合適的地方對MyWebSocketClient判斷狀態並使用connect()方法進行初始化開啓鏈接:
//初始化開啓WebSocket鏈接
WebSocket.READYSTATE readyState = MyWebSocketClient.getInstance(this).getReadyState();
Log.i("WebSocket", "getReadyState() = " + readyState);
//當WebSocket的狀態是NOT_YET_CONNECTED時使用connect()方法進行初始化開啓鏈接:
if (readyState.equals(WebSocket.READYSTATE.NOT_YET_CONNECTED)) {
Log.i("WebSocket", "---初始化WebSocket客戶端---");
MyWebSocketClient.getInstance(this).connect();
}
開啓鏈接時會回調WebSocketClient中的onOpen(ServerHandshake handshakedata)、onMessage(String message)兩個方法。在一對一聊天時,還需要服務器針對每一臺設備生成一個唯一的客戶端設備ID,可以通過onMessage(String message)將其返回到客戶端,然後客戶端需要將客戶端設備ID和用戶UserID進行綁定。
後續所有通過消息通道推送到客戶端的消息會通過onMessage(String message)方法發送到客戶端。所以收到消息後的操作需要在onMessage(String message)方法中完成,比如收到聊天消息,首先將消息保存到本地消息數據庫,然後使用EventBus將消息發送到聊天頁面用以展示。
3、WebSocket重新連接
WebSocket的重新連接是建立一個穩定的WebSocket長鏈接非常重要的一部分,因爲WebSocket的長鏈接通道隨時可能因爲手機的網絡變化、WiFi切換等因素而斷開,從而影響聊天等功能的穩定性。
首先,我們應該在什麼時候發起重新連接?在各種因素導致WebSocket的長鏈接斷開時,會回調WebSocketClient中的onClose()方法,所以我們可以在此發起重新連接;還有將客戶端設備ID和用戶UserID進行綁定時,如果失敗也需要發起重新連接然後再次進行綁定。在需要多次重連時,我設計了一個簡單的時間間隔機制:第一次斷開時延時500毫秒發起重連,如果重連失敗則第二次延時1000毫秒發起重連,如果再次失敗則第三次延時2000毫秒發起重連,以此類推每次時間間隔翻倍直至重連10次以後如果還未成功則宣告重連失敗(最大的時間間隔可達17分鐘,用戶從弱網環境回到正常網絡環境時也可以重連)。當某一次重連成功後則將重連時間間隔重置爲500毫秒,將重連次數重置爲0,以待下次執行同樣的時間間隔機制進行重新連接。
//長鏈接關閉
//在各種因素導致WebSocket的長鏈接斷開時會回調onClose()方法,所以可以在此發起重新連接;(客戶端設備ID和用戶UserID綁定失敗時也需要發起重新連接然後再次進行綁定)
@Override
public void onClose(int code, String reason, boolean remote) {
Log.i("WebSocket", "...MyyWebSocketClient...onClose...");
mHandler.removeMessages(MSG_EMPTY);
mHandler.sendEmptyMessageDelayed(MSG_EMPTY, GlobalConstants.RECONNECT_DELAYED_TIME);
//將時間間隔翻倍
GlobalConstants.RECONNECT_DELAYED_TIME = GlobalConstants.RECONNECT_DELAYED_TIME * 2;
}
//長鏈接開啓
//重連成功後則將重連時間間隔重置爲500毫秒,將重連次數重置爲0,以待下次執行同樣的時間間隔機制進行重新連接
@Override
public void onOpen(ServerHandshake handshakedata) {
Log.i("WebSocket", "...MyyWebSocketClient...onOpen...");
GlobalConstants.webSocketConnectNumber = 0;
GlobalConstants.RECONNECT_DELAYED_TIME = 500;
mHandler.removeMessages(MSG_EMPTY);
}
在我們自己的項目做即時通訊聊天和直播聊天室功能時,Java-WebSocket框架的版本還是1.3.5;當我寫這篇文章時Java-WebSocket框架的版本已經更新至1.3.8,在1.3.8版本中新增了兩個重新連接的方法reconnect()和reconnectBlocking()。在1.3.5版本時沒有直接提供重新連接的方法,我採取的方法是:先將原來的鏈接徹底關閉,再重新創建一個MyWebSocketClient對象(因爲WebSocketClient對象是不能重複使用的),然後執行connect()方法重新連接。當然,在1.3.8及以後版本中建議使用reconnect()或reconnectBlocking()方法進行重新連接。
//在Handler消息隊列中執行重新連接,也便於重連時間間隔控制
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i("WebSocket", "webSocketConnectNumber = " + GlobalConstants.webSocketConnectNumber);
//未超過設置次數時執行重連操作
if (GlobalConstants.webSocketConnectNumber <= 10) {
if (mInstance != null) {
WebSocket.READYSTATE readyState = mInstance.getReadyState();
if (readyState.equals(WebSocket.READYSTATE.NOT_YET_CONNECTED)) {
mInstance.connect();
} else if (readyState.equals(WebSocket.READYSTATE.CLOSED) || readyState.equals(WebSocket.READYSTATE.CLOSING)) {
//先將原來的鏈接關閉,再重新創建一個MyWebSocketClient對象,然後執行connect()方法重新連接。
//(此爲1.3.5版本重連的方法,建議使用1.3.8版本提供的重連方法)
mInstance.closeBlocking();
mInstance = new MyWebSocketClient(mContext);
mInstance.connect();
//將連接次數自增
GlobalConstants.webSocketConnectNumber++;
}
}
} else {
//超過設置次數則清空重連消息隊列,並將mInstance置爲null(待外部初始化連接)
mHandler.removeMessages(MSG_EMPTY);
mInstance = null;
}
}
};
//在Handler消息隊列中執行重新連接,也便於重連時間間隔控制
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
Log.i("WebSocket", "webSocketConnectNumber = " + GlobalConstants.webSocketConnectNumber);
//未超過設置次數時執行重連操作
if (GlobalConstants.webSocketConnectNumber <= 10) {
if (mInstance != null) {
WebSocket.READYSTATE readyState = mInstance.getReadyState();
if (readyState.equals(WebSocket.READYSTATE.NOT_YET_CONNECTED)) {
mInstance.connect();
} else if (readyState.equals(WebSocket.READYSTATE.CLOSED) || readyState.equals(WebSocket.READYSTATE.CLOSING)) {
//使用1.3.8版本提供的reconnect()方法重連
mInstance.reconnect();
//將連接次數自增
GlobalConstants.webSocketConnectNumber++;
}
}
} else {
//超過設置次數則清空重連消息隊列,並將mInstance置爲null(待外部初始化連接)
mHandler.removeMessages(MSG_EMPTY);
mInstance = null;
}
}
};
到這裏我實現的功能就基本完畢了,當然整個WebSocket還是比較複雜的,我自己實現的穩定性也還需要加強,上面如有不到之處還請指出,也希望大家共同探討。