使用WebSocket實現Android端即時通訊聊天功能

        本篇文章主要介紹自己使用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還是比較複雜的,我自己實現的穩定性也還需要加強,上面如有不到之處還請指出,也希望大家共同探討。   
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章