WebSocket基礎知識

WebSocket的生命週期

Java Websocket API中的WebSocket生命週期

WebSocket端點的四個生命週期事件
+ 打開事件:此事件發生在端點上建立新連接時並且在任何其他事件發生之前
+ 消息事件:此事件接收WebSocket對話中另一端發送的消息。它可以發生在WebSocket端點接收了打開了事件之後並且在接收關閉事件關閉連接之前的任意時刻。
+ 錯誤事件:此事件在WebSocket連接或者端點發生錯誤時產生
+ 關閉事件:此事件表示WebSocket端點的連接目前正在部分地關閉,它可以由參與連接的任意一個端點發出

註解式端點事件處理

服務器端點需要使用一個類級別註解@ServerEndpoint,客戶端端點則需要@ClientEndpoint註解。對於註解式端點來說,爲了攔截不同的生命週期事件,我們需要利用方法級註解:@OnOpen,@OnMessage,@OnError和@OnClose。

  • @OnOpen 此註解用於註解式端點的方法,指示當此端點建立新的連接時調用此方法。需要一個方法來處理打開事件的主要原因是,使得開發人員能夠設置在WebScoket對話中的信息,你可能希望爲準備數據庫而執行一些花費昂貴的必要操作,例如在處理事件的方法中打開數據庫連接。此事件伴隨着三部分信息:WebSocket Session對象,用於表示已經建立好的連接;配置對象(EndpointConfig的實例),包含了用來配置端點的信息;一組路徑參數,用於打開階段握手時WebSocket端點匹配入URL。
@OnOpen
public void init(Session session, EndpointConfig config){
    // initialization code
}
  • @OnMessage 此註解允許你裝飾你希望處理入站消息的方法。Java WebSocket API中的消息事件伴隨的信息是Session對象,EndpointConfig對象,打開階段握手中從匹配入站URI過程中獲取的路徑參數以及最重要的消息本身。連接上的消息將以3種基本形式抵達:文本消息二進制消息Pong消息
// 文本消息處理方法
@OnMessage
public void handleTextMessage(String textmessage{
    // process the textMessage here
}

// 文本消息高級選項:分批接收文本
@OnMessage
public void catchDocumentPart(String text, boolean isLast){
    // process the textMessage here
}

// 二進制消息處理方法
@OnMessage
public void processBinary(byte[] messageData, Session session) {
    // process binary data here
}

// 二進制消息高級選項:分批接收二進制數據
@OnMessage
public void processVideoFragment(byte[] partialData, boolean isLast){
    if (!isLast){
        // there is more to come;
    } else {
        // now we have the whole message;
    }
}

// 使用java.io.InputStream來處理二進制消息
@OnMessage
public void handleBinary(InputStream is){
    // read
}

// 同理使用java.io.Reader處理文本消息

// Pong消息
@OnMessage
public void processPong(PongMessage message){
    // process pong 
}

事實上,處理消息還有更多的選項:你甚至可以讓WebSocket實現把入站消息轉換成自己選擇的對象。

WebSocket應用一般是異步的雙向消息。換言之,典型應用並不總是立即響應入站消息。儘管如此,在一些場景下你希望立刻響應入站消息。因此,通過@OnMessage註解的此類方法上有一個額外選項:方法可以有返回類型或者返回爲空。當使用@OnMessage註解的方法有返回類型時,WebSocket實現立即將返回值作爲消息返回給剛剛在方法中處理的消息的發送者。

  • @OnError 此註解可以用來註解WebSocket端點的方法,使其可以處理WebSocket實現處理入站消息時發生的任何錯誤。
@Error
public void errorHandler(Throwable t){
    // log error here
}
  • @OnClose 可以用來註解多種不同類型的方法來處理關閉事件。伴隨關閉事件的信息是關閉信息,與建立連接的打開階段握手相關聯的任意一個路徑參數,以及一些描述連接關閉原因的信息。
@OnClose
public void goodbye(CloseReason cr){
    // log the reason for posterity
    // close database connection
}

以下是完整示例

import java.io.*;
import java.io.IOException;
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("/lights")
public class LifecycleEndpoint {
    private static String START_TIME = "Start Time";
    private Session session;

    @OnOpen
    public void whenOpening(Session session){
        this.session = session;
    session.getUserProperties().put(START_TIME, System.currentTimeMillis());
    this.sendMessage("3:Just opened");
    }

    @OnMessage
    public void whenGettingAMessage(String message){
        if (message.indexOf("xxx") != -1){
        throw new IllegalArgumentException("xxx not allowed !!");
    } else if (message.indexOf("close") != -1){
        try {
            this.sendMessage("1:Server closing after "+this.getConnectionSeconds()+"s");
        session.close();
        } catch (IOException ioe){
            System.out.println("Error closing session "+ioe.getMessage());
        }
        return; 
    }
    this.sendMessage("3:Just processed a message");
    }

    @OnError
    public void whenSomethingGoesWrong(Throwable t){
        this.sendMessage("2:Error:"+t.getMessage());
    }

    @OnClose
    public void whenClosing(){
        System.out.println("Goodbye !");
    }

    void sendMessage(String message){
        try {
        session.getBasicRemote().sendText(message);
    } catch (Throwable ioe){
        System.out.println("Error sending message "+ioe.getMessage());
    }
    }

    int getConnectionSeconds(){
        long millis = System.currentTimeMillis()-((Long)this.session.getUserProperties().get(START_TIME));
    return (int)millis/1000;
    }
}

編程式端點生命週期

生命週期事件

事件 端點方法
打開 public abstract void onOpen(Session session, EndpointConfig config)
錯誤 public void onError(Session session, Throwable thr)
關閉 public void onClose(Session session, CloseReason cr)

處理消息

爲了處理入站消息,需要提供MessageHandler實現。
+ 對於文本消息,使用MessageHandler.Whole
+ 對於二進制消息,使用MessageHandler.Whole

一旦你實現了上述一個或者兩個接口來定義希望消費者消息的方式,你需要做的所有事情是通過調用

session.addMessageHandler(myMessageHandler handler)

在第一個消息到達之前的某一時刻註冊你的消息處理程序到代表你有興趣偵聽的連接的Session對象上。通常,端點將在onOpen()方法中添加其消息處理程序,因此可以確保不遺漏任何消息

以下是編程式的實現

import java.io.IOException;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.Session;

public class ProgrammaticLifecycleEndpoint extends Endpoint {
    private static String START_TIME = "Start Time";
    private Session session;

    @Override
    public void onOpen(Session session, EndpointConfig config){
        this.session = session;
    final Session mySession = session;
    this.session.addMessageHandler(new MessageHandler.Whole<String>(){
        @Override
        public void onMessage(String message){
            if (message.indexOf("xxx") != -1){
            throw new IllegalArgumentException("xxx not allowed !");
        } else if (message.indexOf("close") != -1){
            try {
                sendMessage("1:Server closing after "+getConnectionSecondes()+"s");
            mySession.close();
            } catch (IOException e){
                System.out.println("Error closing session "+e.getMessage());
            }
            return;
        }
        sendMessage("3:Just processed a message");
        }
    });
    session.getUserProperties().put(START_TIME, System.currentTimeMillis());
    this.sendMessage("3:Just opened");
    }

    @Override
    public void onClose(Session session, CloseReason reason){
        System.out.println("Goodbye !");
    }

    @Override
    public void onError(Session session, Throwable thr){
        this.sendMessage("2:Error:"+thr.getMessage());
    }

    void sendMessage(String message){
        try {
        session.getBasicRemote().sendText(message);
    } catch (IOException e){
        System.out.println("Error sending message "+message);
    }
    }

    int getConnectionSeconds(){
        long millis = System.currentTimeMillis()-((Long)this.session.getUserProperties().get(START_TIME));
    return (int)millis/1000;
    }

}

實例數目及線程機制

上述實現Lifecycle時,將會話存儲爲實例變量的原因是,我們可以使用它來闡述WebSocket端點生命週期中的一個更重要的問題。如果你重新啓動Lifecycle應用,但是這一次打開第二個瀏覽器窗口到同樣的首頁,你將看到兩組交通信號燈。假如你開始按下任意一個瀏覽器窗口的生命週期按鈕,那麼將看到每組信號燈都可以是不同的狀態。這是因爲每個瀏覽器窗口對Lifecycle WebSocket端點來說都充當一個獨立的客戶端,並且WebSocket實現爲每個連接的客戶端使用不同的LifecycleEndpoint實例。
這意味着對於每一個WebSocket端點(不管是註解式還是編程式)定義來說,WebSocket容器在每次有新的客戶端連接時會實例化端點的一個新的實例。這樣做的結果是每個WebSocket端點實例僅能夠永遠看到同樣的會話實例:此實例表示從唯一的客戶端連接到那個端點實例的唯一連接。

WebSocket實現也爲你提供了另外一個重要的保證:同一個會話(或者是連接)中不允許兩個事件線程同時調用一個端點實例。這可能聽起來很抽象,但是這意味着端點實例永遠不會在某時被WebSocket實現的一個以上的線程調用。它意味着如果客戶端發送多條消息,WebSocket實現必須調用端點每次處理一條消息。知道這一點特別重要,因爲這意味着你永遠不需要擔心爲端點實例的併發訪問進行編程。這也是Java WebSocket編程模型與Java Servlet編程模型的關鍵差異,Java Servlet實例可能被多個線程同時調用,每個線程用於處理不同客戶端的請求/響應交互。這意味着WebSocket編程明顯更加容易。

消息通信基礎

消息通信概述

發送消息

RemoteEndpoint接口和它的子類(RemoteEndpoint.Basic和RemoteEndpoint.Async)提供了發送消息的所有方法。

public void sendPing(ByteBuffer applicationData){
    throws IOException, IllegalArgumentException
}

發送字符串消息

RemoteEndpoint.Basic API提供了3中發送字符串的方法
+ 最簡單的方法

// RemoteEndpoint.Basic 發送文本消息
public void sendText(String text) throws IOException
  • 由於WebSocket消息通常表現爲一些高層級的對象形式(序列化成String以便發送),因此Java WebSocket API也提供了一種使用Write API發送String消息的方式
// RemoteEndpoint.Basic 發送文本消息到流
public Write getSendStream() throws IOException
  • WebSocket協議允許把大的WebSocket消息分解成多個小片段
// RemoteEndpoint以片段形式發送文本消息
public void sendText(String partialMessage, boolean isLast) throws IOException

發送二進制消息

有RemoteEndpoint.Basic接口同步發送消息,也有RemoteEndpoint.Async接口異步發送消息,這裏介紹第一種,同樣是3種方式

public void sendBinary(ByteBuffer data) throws IOException
// 分片段發送
public void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException

最後,可以得到一個用來寫入二進制消息數據的java.io.OutputStream的引用。這非常有用,特別是當使用直接將數據對象寫入Java I/O的API時。在完成消息寫入後需要關閉輸入流。一旦關閉輸入流,消息就會被髮送。

public OutputStream getSendStream() throws IOException

發送Java對象消息

可以使用RemoteEndpoint.Basic發送任意Java對象消息

public void sendObject(Object data) throws IOException, EncodeException
  • 傳一個Java基本類型(或者其等值裝箱類),則WebSocket實現會把數據轉換成一個標準的Java字符串(就是使用toString()方法)
  • 傳入的是其它對象,那麼需要爲WebSocket實現提供一個javax.websocket.Encoder接口的實現。在Encoder家族中,最通用的接口是javax.websocket.Encoder.Text,T就是你想發送的對象的類型。
public String encode(T object) throws EncodeException
  • 如果想把對象編碼成WebSocket二進制消息,可以實現Encoder.Binary接口。
  • 如果想把對象編碼成Java I/O流,可以實現Encoder.CharacterStream或者Encoder.BinaryStream

每次使用RemoteEndpoint.Basic的sendObject方法發送T類型的對象時,WebSocket實現都會調用相應的編碼器。發送給遠程端點的實際上是encode()方法返回的字符串。如果你的編碼器無法把指定對象轉換成字符串,很可能會拋出EncodeException異常。在這種情形下,EncodeException將會傳播給RemoteEndpoint.Basic的sendObject()方法

在端點上配置編碼器
  • 對於註解式端點,所有需要做的就是了類級別的WebSocket註解上聲明Encoder類。一旦這樣做,每當你從這個註解式端點上獲取一個RemoteEndpoint引用時,都可以直接傳一個Apple對象給sendObject()方法,WebSocket實現會使用MyAppleEncoder吧Apple對象編碼成WebSocket消息。
@ServerEndpoint(value = "/fruit_trees", encoders = {MyAppleEncoder.class})
  • 對於編程式端點,需要創建EndpointConfig對象,在創建該對象時可以配置編碼器類。
List<Class<? extends Encoder>> encoders = new ArrayList<>();
encoders.add(MyAppleEncoder.class);
ClientEndpointConfig config = ClientEndpointConfig.Builder.create().encoders(encoders).build();

編碼器接口類型

編碼器接口 轉換 主要方法
Encoder.Text T轉換成String String encode(T Object) throws EncodeException
Encoder.TextStream T轉換成Writer void encode(T object, Writer writer) throws EncodeException, IOException
Encoder.Binary T轉換成ByteBuffer ByteBuffer encode(T object) throws EncodeException
Encoder.BinaryStream T轉換成OutputStream void encode(T object, OutputStream os) throws EncodeException, IOException

接收WebSocket消息

在註解式端點中接收WebSocket消息

// 用@OnMessage處理文本消息
@OnMessage
public void handleTextMessages(String textMessage){
    return "I got this "+textMessage + "!";
}

// 用@OnMessage處理到達的文本消息片段
@OnMessage
public void handlePartial(Sting textMessage, boolan isLast)

// 用@OnMessage處理二進制消息
@OnMessage
public String handleBinaryMessages(byte[] messageData){
    return "I got "+messageData.length+" bytes of data !";
}

// 用@OnMessage處理Pong消息
public String handlePongMessages(PongMessage pongMessage){
    return "I got a pong message carrying "+pongMessage.getApplicationData().length+" bytes of data !";
}

// 用@OnMessage處理Java對象消息,同時必須提供一個Decoder接口的實現。
@OnMessage
public void addToBasket(Orange orange){
    this.bag.addShoppingItem(orange);
    this.cost = this.cost + orange.getPrice();
}

// Decoder.Text<Orange>接口的decode()方法簽名
public Orange decode(String rawMessage) throws DecodeEexception

/* 
 * Decoder.Text<Orange>接口的willDeocde()方法簽名,在WebSocket實現中,該方法先於decode()方法被調用,
 * 這是爲了使你有一個跳過解碼消息的機會,例如當消息格式明顯不正確時
 *
 */
public boolean willDecode(String s)

爲什麼不偵聽入站Ping消息?答案是Java WebSocket API沒有提供這樣的方法。WebSocket實現被要求以最快的速度回覆連接中入站的任何Ping消息,Pong消息包含的數據與Ping消息相同,因此不另外寫代碼偵聽Ping消息。

接收方法參數類型

參數類型 處理的消息類型 示例
String 文本消息 public void handle(String message)
String, boolean 文本消息片段 public void handle(String parialMessage, boolean isLast)
Reader 文本消息流 public void handle(Reader message)
byte[] 二進制消息 public void handle(byte[] data)
ByteBuffer 二進制消息 public void handle(ByteBuffer data)
byte[], boolean 二進制消除片段 public void handle(byte[] partialData, boolean isLast)
ByteBuffer, boolean 二進制消息片段 public void handle(ByteBuffer partialData, boolean isLast)
PongMessage Pong消息 public void handle(PongMessage message)

解碼器接口

解碼器接口 轉換 主要解碼方法
Decoder.Text String轉換成T T decode(String raw) throws DecodeException
Decoder.TextStream Reader轉換成T T decode(Reader raw) throws DecodeException
Decoder.Binar ByteBuffer轉化成T T decode(ByteBuffer raw) throws DecodeException
Decoder.BinaryStream InputStream轉換成T T decode(InputStream raw) throws DecodeException

在你的WebSocket端點中,應始終包含錯誤處理方法。除了處理入站消息的錯誤之外,端點上的其他WebSocket方法(如打開事件處理方法)產生的運行時異常也會被傳遞到這裏。如果沒有錯誤處理方法,你可能不知道消息是否已經到達過

Java WebSocket API提供了一個方便,爲Java基本類型和它的等價類提供了內置文本解碼器。WebSocket實現採取的途徑就是使用基本類型或者等價類轉換成其等價類,使用單個字符串參數的構造函數生成等價類。

@OnMessage
public void doCount(Interger message){
    // process Integer
}

Java對象消息的傳遞選項

傳遞選項 解碼器 示例
Java基本類型及其等價類的文本消息 自動 @OnMessage public void handleTransferCode(Double d)
自定義Java對象的文本或者二進制消息 開發人員提供 @OnMessage public void handleObject(CustomObject o)

註解了@OnMessage的方法提供了返回值,返回值的類型決定了WebSocket實現要寄回給消息發送者的WebSocket消息的類型。爲了能迴應一個文本消息,返回類型爲String;爲了迴應一個二進制消息,返回類型應爲byte[]或者ByteBuffer。這意味着在註解式端點中,以下代碼都是有效的消息處理方法

@OnMessage
public String echo(String message){...}

@OnMessage
public Integer processAndConfirm(byte[] uplaod){...}

@OnMessage
public boolean purchase(String item){...}

在註解式端點上,Java WebSocket實現爲了能夠將入站消息分配到正確的消息處理方法上,它設置了一個非常嚴格的限制:每個註解式端點最多隻有一個消息處理方法處理每種本地WebSocket消息類型(即文本消息,二進制消息和Pong消息)

嚴正聲明:從這裏開始,不再更新編程式,只更新註解式

綜合應用

DrawingObject類

public class DrawingObject {

    public static String MESSAGE_NAME = "DrawingObject";
    private Shape shape;
    private Point center;
    private int radius = 0;
    private Color color;

    public DrawingObject(Shape shape, Point center, int radius, Color color){}

    // getter ...

    public void draw(Graphics g){}
}

DrawingClient類

@ClientEndpoint (
    decoders = {DrawingDecoder.class},
    encoders = {DrawingEncoder.class}
)
public class DrawingClient {
    private Session session;
    private DrawingWindow window;

    /*
     * 創建客戶端端點時必須傳入一個DrawingWindow對象,客戶端端點被構造好後,把DrawingWindow引用
     * 保存爲一個私有實例變量供後續使用。同時通過@ClientEndpoint註解中配置解碼器
     */
    public DrawingClient(DrawingWindow window){}

    /*
     * 當建立與服務器的WebSocket連接時,這個方法將傳入的Session對象保存爲私有實例變量供後續使用
     */
    @OnOpen
    public void init(Session session){}

    /*
     * 因爲這個WebSocket端點爲DrawingObject配置瞭解碼器,所以能夠把DrawingObject對象作爲drawingChanged
     * 方法的一個參數。因此,在這種情況下,當收到WebSocket消息時,會把WebSocket消息轉換成DrawingObject對象,
     * 這個方法會被調用。
     */
    @OnMessage
    public void drawingChanged(DrawingObject drawingObject){}

    /*
     * 通過調用RemoteEndpoint的sendObject()方法,給DrawingBoard應用的服務端發送了一個DrawingObject實例。
     * 在這背後,WebSocket實現會使用在類級別@ClientEndpoint註解中提供的解碼器,也就是DrawingEncoder實例。
     * 爲了把DrawingObject實例轉換成WebSocket消息,在運行時會調用DrawingEncoder的encode方法
     */
    public void notifyServerDrawingChanged(DrawingObject drawingObject){
        try {
        this.session.getBasicRemote().sendObject(drawingObject);
    } catch (IOException ioe){
        System.out.println("Error: IO "+ioe.getMessage());
    } catch (EncodeException ee){
        System.out.println("Error encoding object :"+ee.getObject());
    }
    }

    /*
     * 在DrawingClient中顯式地處理在解碼入站消息時產生的這種錯誤。當然,在真正的應用中,
     * 這樣的錯誤處理只是簡單地打印輸出錯誤信息,但handleError()方法會告訴你在代碼中如何區分這些錯誤
     */
    @OnError
    public void handleError(Throwable thw){
        if (thw instanceof DecodeException){
        System.out.println("Error decoding incoming message : "+((DecodeException)thw).getText());
    } else {
        System.out.println("Client WebSocket error : "+thw.getMessage());
    }
    }

    /*
     * 該方法實現的核心是WebSocketContainer類的connectToServer()方法,WebSocketContainer類用於客戶端
     * 端點的實例發佈到提供的URL上
     */
    public static DrawingClient connect(DrawingWindow window, String path){}

    public void disconnect();
}

接下來是編碼類實現

public class DrawingEncoder implements Encoder.Text<DrawingObject>{
    @Override
    public void init(EndpointConfig config){}

    @Override
    public void destroy(){}

    @Override
    public String encode(DrawingObject drawingObject) throws EncodeException {
        // 
    }

}

解碼類實現

public class DrawingDecoder implements Decoder.Text<DrawingObject>{

    @Override
    public void init(EndpointConfig config){}

    @Override
    public void destroy(){}

    @Override
    public DrawingObject decode(String s) throws DecodeException {
        //
    }

    /*
     * 該方法負責對文本消息作初步的檢查,判斷這些消息自己是否能解碼。如果willDecode()方法
     * 沒有返回true,WebSocket實現就不會調用decode()方法,消息也不會以DrawingObject形式被傳遞。
     */
    @Override
    public boolean willDecode(String s){
        return s.startsWith(DrawingObject.MESSAGE_NAME);
    }

}

我們看到解碼器和編碼器接口定義了一些它自己的生命週期。當每個編碼器實例在準備服務和完成服務時,都會調用init(EndpointConfig config)和destroy()方法。如果你實現的編碼器需要初始化或者清理很昂貴的資源時,這些方法就很有用。就像端點自身一樣,WebSocket實例會爲每個對等連接創建一個編碼器實例。因此,在這個DrawingEncoder中,生命週期方法的實現是空的,因爲不需要任何資源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章