Android 即時通信(二)WebSocket傳輸消息

一、利用WebSocket傳輸消息
文本與圖片的即時通信都可以由SocketIO實現(xiàn),看似它要一統(tǒng)即時通信了,可是深究起來會發(fā)現(xiàn)SocketIO存在很多局限,包括但不限于下列幾點:
(1)SocketIO不能直接傳輸字節(jié)數(shù)據(jù),只能重新編碼成字符串后(比如BASE64編碼)再傳輸,造成了額外的系統(tǒng)開銷。
(2)SocketIO不能保證前后發(fā)送的數(shù)據(jù)被收到時仍然是同樣順序,如果業(yè)務要求實現(xiàn)分段數(shù)據(jù)的有序性,開發(fā)者就得自己采取某種機制確保這種有序性。
(3)SocketIO服務器只有一個main程序,不可避免地會產(chǎn)生性能瓶頸。倘若有許多通信請求奔涌過來,一個main程序很難應對。

為了解決上述幾點問題,業(yè)界提出了一種互聯(lián)網(wǎng)時代的Socket協(xié)議,名叫WebSocket。它支持在TCP連接上進行全雙工通信,這個協(xié)議在2011年被定為互聯(lián)網(wǎng)的標準之一,并納入HTML5的規(guī)范體系。

相對于傳統(tǒng)的HTTP與Socket協(xié)議來說,WebSocket具備以下幾點優(yōu)勢:
(1)實時性更強,無須輪詢即可實時獲得對方設備的消息推送。
(2)利用率更高,連接創(chuàng)建之后,基于相同的控制協(xié)議,每次交互的數(shù)據(jù)包頭部較小,節(jié)省了數(shù)據(jù)處理的開銷。
(3)功能更強大,WebSocket定義了二進制幀,使得傳輸二進制的字節(jié)數(shù)組不在話下。
(4)擴展更方便,WebSocket接口被托管在普通的Web服務之上,跟著Web服務方便擴容,有效規(guī)避了性能瓶頸。
WebSocket不僅擁有如此豐富的特性,而且用起來也特別簡單。
先在服務端的WebSocket編程,除了引入它的依賴包javaee-api-8.0.1.jar,服務器添加相關代碼如下:

package com.websocket.server;

import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

/**
 * @ServerEndpoint 注解是一個類層次的注解,它的功能主要是將目前的類定義成一個websocket服務器端,
 * 注解的值將被用于監(jiān)聽用戶連接的終端訪問URL地址,客戶端可以通過這個URL來連接到WebSocket服務器端
 */
@ServerEndpoint("/testWebSocket")
public class WebSocketServer {
    // 存放每個客戶端對應的WebSocket對象
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();
private Session mSession; // 當前的連接會話

// 連接成功后調(diào)用
@OnOpen
public void onOpen(Session session) {
    System.out.println("WebSocket連接成功");
    this.mSession = session;
    webSocketSet.add(this);
}

// 連接關閉后調(diào)用
@OnClose
public void onClose() {
    System.out.println("WebSocket連接關閉");
    webSocketSet.remove(this);
}

// 連接異常時調(diào)用
@OnError
public void onError(Throwable error) {
    System.out.println("WebSocket連接異常");
    error.printStackTrace();
}

// 收到客戶端消息時調(diào)用
@OnMessage
public void onMessage(String msg) throws Exception {
    System.out.println("接收到客戶端消息:" + msg);
    for(WebSocketServer item : webSocketSet){
        item.mSession.getBasicRemote().sendText("我聽到消息啦“"+msg+"”");
   }
  }
}

啟動服務器的Web工程,便能通過形如ws://localhost:8080/HttpServer/testWebSocket這樣的地址訪問WebSocket。
App端的WebSocket編程,由于WebSocket協(xié)議尚未納入JDK,因此要引入它所依賴的jar包tyrus-standalone-client-1.17.jar。

代碼方面則需自定義客戶端的連接任務,注意給任務類添加注解@ClientEndpoint,表示該類屬于WebSocket的客戶端任務。任務內(nèi)部需要重寫onOpen(連接成功后調(diào)用)、processMessage(收到服務端消息時調(diào)用)、processError(收到服務端錯誤時調(diào)用)三個方法,還得定義一個向服務端發(fā)消息的發(fā)送方法,消息內(nèi)容支持文本與二進制兩種格式。

下面是處理客戶端消息交互工作的示例代碼:

import android.app.Activity;
import android.util.Log;

import javax.websocket.*;

@ClientEndpoint
public class AppClientEndpoint {
private final static String TAG = "AppClientEndpoint";
private Activity mAct; // 聲明一個活動實例
private OnRespListener mListener; // 消息應答監(jiān)聽器
private Session mSession; // 連接會話

public AppClientEndpoint(Activity act, OnRespListener listener) {
    mAct = act;
    mListener = listener;
}

// 向服務器發(fā)送請求報文
public void sendRequest(String req) {
    Log.d(TAG, "發(fā)送請求報文:"+req);
    try {
        if (mSession != null) {
            RemoteEndpoint.Basic remote = mSession.getBasicRemote();
            remote.sendText(req); // 發(fā)送文本數(shù)據(jù)
            // remote.sendBinary(buffer); // 發(fā)送二進制數(shù)據(jù)
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 連接成功后調(diào)用
@OnOpen
public void onOpen(final Session session) {
    mSession = session;
    Log.d(TAG, "成功創(chuàng)建連接");
}

// 收到服務端消息時調(diào)用
@OnMessage
public void processMessage(Session session, String message) {
    Log.d(TAG, "WebSocket服務端返回:" + message);
    if (mListener != null) {
        mAct.runOnUiThread(() -> mListener.receiveResponse(message));
    }
}

// 收到服務端錯誤時調(diào)用
@OnError
public void processError(Throwable t) {
    t.printStackTrace();
}

// 定義一個WebSocket應答的監(jiān)聽器接口
public interface OnRespListener {
    void receiveResponse(String resp);
 }
}

App的活動代碼,依次執(zhí)行下述步驟就能向WebSocket服務器發(fā)送消息:獲取WebSocket容器→連接WebSocket服務器→調(diào)用WebSocket任務的發(fā)送方法。其中前兩步涉及的初始化代碼如下:

private AppClientEndpoint mAppTask; // 聲明一個WebSocket客戶端任務對象
// 初始化WebSocket的客戶端任務
private void initWebSocket() {
    // 創(chuàng)建文本傳輸任務,并指定消息應答監(jiān)聽器
    mAppTask = new AppClientEndpoint(this, resp -> {
        String desc = String.format("%s 收到服務端返回:%s",
                DateUtil.getNowTime(), resp);
        tv_response.setText(desc);
    });
    // 獲取WebSocket容器
    WebSocketContainer container = ContainerProvider.getWebSocketContainer();
    try {
        URI uri = new URI(SERVER_URL); // 創(chuàng)建一個URI對象
        // 連接WebSocket服務器,并關聯(lián)文本傳輸任務獲得連接會話
        Session session = container.connectToServer(mAppTask, uri);
        // 設置文本消息的最大緩存大小
        session.setMaxTextMessageBufferSize(1024 * 1024 * 10);
        // 設置二進制消息的最大緩存大小
        //session.setMaxBinaryMessageBufferSize(1024 * 1024 * 10);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

因為WebSocket接口仍為網(wǎng)絡操作,所以必須在子線程中初始化WebSocket,啟動初始化線程的代碼如下所示:

// 啟動線程初始化WebSocket客戶端
new Thread(() -> initWebSocket()).start(); 

同理,發(fā)送WebSocket消息也要在子線程中操作,啟動消息發(fā)送線程的代碼如下:

 // 啟動線程發(fā)送文本消息
 new Thread(() -> mAppTask.sendRequest(content)).start();

最后確保后端的Web服務正在運行,再運行并測試該App,在編輯框輸入待發(fā)送的文本,此時交互界面如圖【成功發(fā)送WebSocket消息】所示:


成功發(fā)送WebSocket消息.png
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容