Servlet 3.1中的WebSocket編程規範

如果不是很清楚WebSocket協議,可以參考這篇博客

包結構

Servlet 3.1以上的版本制定了WebSocket的編程規範,位於包javax.servlet中:
在這裏插入圖片描述
javax.servlet.websocket下包含了客戶端和服務器端公用的註解、接口、類和異常
javax.servlet.websocket.server下包含了創建和配置WebSocket服務端所需的註解、接口和類。
Maven依賴:

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope>
</dependency>

關於WebSocket的簡易Demo網上已經有很多了,這裏就不介紹這些了。


Endpoint

WebSocket是一個建立在TCP基礎上的雙向通信應用層協議,Endpoint的概念類似於一個Servlet實現類,用於實現WebSocket相關業務邏輯,區別在於Servlet是處理HTTP請求,而EndPoint是處理WebSocket數據幀。例如javax.servlet.websocket.RemoteEndpoint實例代表客戶端。

在實際編程中,Endpoint的編寫有兩種方式:

  • 基於註解(常用)
    Servlet提供了4個用於修飾方法的註解:@OnOpen@OnMessage@OnError@OnClose,其修飾的方法分別用於在連接建立時回調、收到數據幀時回調、處理邏輯發生異常時回調、連接關閉時回調。例如:

    @ServerEndpoint("/ws/chart")
    public class ChartEndpointImpl {
    	@OnOpen
    	public void open(Session session, EndpointConfig conf) { }
    	@OnMessage
    	public void message(Session session, String message) { }
    	@OnError
    	public void error(Session session, Throwable error) { }
    	@OnClose
    	public void close(Session session, CloseReason reason) { }
    }
    

    @ServletEndpoint需要指定一個URI,在上述例子中,當客戶端向/ws/chart發起WebSocket握手HTTP請求時,默認情況下會創建一個新的ChartEndpointImpl實例,並調用@OnOpen修飾的方法(如果存在的話)。
    此外,各個方法的參數列表並不是固定的,具體規則如下:

    • 用戶可以在任何方法中的參數列表指定一個Session類型的參數。
    • @OnMessage修飾的方法中,可以傳入消息對象(類型由Decoder決定,我們稍作討論),一個ServerEndpoint可以具有多個@OnMessage修飾的方法,前提是它們的參數列表互不相同,並且有對應的Docoder
    • @OnError方法中,可以傳入異常Throwable類型的參數。
    • @OnClose方法中,可以傳入CloseReason類型的參數,以分析WebSocket連接關閉的具體原因。
  • 基於javax.servlet.websocket.Endpoint抽象類(較少使用)
    我們可以通過繼承EndPoint方式來重寫控制WebSocket連接生命週期相關的回調方法,例如我們實現一個EndpointImpl

    public class EndpointImpl extends Endpoint {
    	@Override
        public void onOpen(Session session, EndpointConfig config) {
        	//連接建立時回調
        }
    
        public void onClose(Session session, CloseReason closeReason) {
            // 連接被關閉時回調
        }
    
        public void onError(Session session, Throwable throwable) {
            // 連接發生異常時回調
        }
    }
    

    雖然Endpoint沒有定義onMessage處理方法,但是我們可以通過在onOpen方法通過Session對象添加MessageHandler對象來實現onMessage相關的邏輯。

@ServerEndpoint註解除了可以指定URI以外,還可以定義以下內容:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServerEndpoint {
	String value();  //對應的URI
	String[] subprotocols() default {};  //對應的子協議,由握手請求頭Sec-WebSocket-Protocol指定
	Class<? extends Decoder>[] decoders() default {};  //解碼器
	Class<? extends Encoder>[] encoders() default {};  //編碼器
	public Class<? extends ServerEndpointConfig.Configurator> configurator()  //ServerEndpointConfig實現類
	            default ServerEndpointConfig.Configurator.class;
}

value就是我們剛纔提到的URI,除了固定的URI,還可以將URI一部分作爲參數,例如:

@ServerEndpoint("/chart/{id}")
public class EndpointImpl {
	@OnOpen
	public void example(Session session, EndpointConfig cfg, @PathParam("id") String id) { }
}

subprotocols用於實現WebSocket協議擴展,當有多個@ServerEndpoint修飾的類的URI相同時,就根據WebSocket握手階段客戶端發送的HTTP請求頭中Sec-WebSocket-Protocol(默認情況下沒有該請求頭)來選取對應的ServerEndpoint
EncoderDecoder對應編碼器和解碼器,如果@OnMessage修飾的方法的參數列表有非基本類型的對象,就需要解碼器將二進制流或者字符流轉換爲Java對象。EncoderDecoder在Servlet規範中只是兩個接口,其實現類需要自己根據業務需求編寫。


Session

WebSocket不像HTTP,它是一個有狀態的協議,WebSocket在連接建立時創建Session對象,在連接關閉時刪除本次連接對應的Session對象,其生命週期等於WebSocket連接。而HTTP協議對應HttpSession通過HTTP協議傳來的會話ID來識別,當會話過期後纔會刪除其HttpSession對象,其生命週期與過期時間有關。

Session本身是一個接口,並繼承了java.io.Closeable接口,定義了一些與會話相關的方法,其具體實現由容器決定,它包含幾個類型的方法:

  • 獲取WebSocket容器:

    WebSocketContainer getContainer();
    
  • 獲取客戶端握手請求相關的信息:

    // 獲取WebSocket協議版本,對應客戶端握手請求頭的Sec-WebSocket-Version字段,一般爲13
    String getProtocolVersion();
    // 獲取握手請求頭的Sec-WebSocket-Protocol對應的值
    String getNegotiatedSubprotocol();
    // 獲取請求URI,在上述例子中就是/ws/chart
    URI getRequestURI();
    // 獲取請求參數
    Map<String, List<String>> getRequestParameterMap();
    // 同樣是獲取請求參數,如果參數存在相同的鍵,則一般是取第一個
    Map<String,String> getPathParameters();
    // 參數原字符串,例如請求/ws/chart?id=123&sid=123,那麼該方法會返回"id=123&sid=123"
    String getQueryString();
    //獲取Sec-WebSocket-Extensions請求頭中的所有字段
    List<Extension> getNegotiatedExtensions();
    
  • 獲取、修改WebSocket連接本身屬性相關的信息:

    // 當前WebSocket連接是否採用了SSL,也就是判定是ws://還是wss://
    boolean isSecure();
    // 當前WebSocket連接是否活躍(已經打開)
    boolean isOpen();
    // 最大空閒時間,噹噹前時間減去最近一次交互數據的時間大於該值時,連接會被關閉
    long getMaxIdleTimeout();
    // 設置最大空閒時間
    void setMaxIdleTimeout(long timeout);
    // 設置存儲二進制類型(opcode爲0x2)的消息緩衝區最大字節數
    void setMaxBinaryMessageBufferSize(int max);
    // 獲取存儲二進制類型(opcode爲0x2)的消息緩衝區最大字節數
    int getMaxBinaryMessageBufferSize();
    // 設置存儲文本類型(opcode爲0x1)的消息緩衝區最大字符數
    void setMaxTextMessageBufferSize(int max);
    // 獲取存儲文本類型(opcode爲0x1)的消息緩衝區最大字符數
    int getMaxTextMessageBufferSize();
    // Session在服務器內部的唯一ID,由容器設置,對客戶端不可見。
    String getId();
    // 關閉WebSocket連接
    @Override void close() throws IOException;
    // 關閉WebSocket連接,並設置關閉原因
    void close(CloseReason closeReason) throws IOException;
    
  • 獲取、修改本次會話的MessageHandler

    // 添加MessageHandler
    void addMessageHandler(MessageHandler handler) throws IllegalStateException;
    // 獲取所有的MessageHandler
    Set<MessageHandler> getMessageHandlers();
    // 刪除MessageHandler
    void removeMessageHandler(MessageHandler listener);
    //添加Partial類型的MessageHandler
    <T> void addMessageHandler(Class<T> clazz, MessageHandler.Partial<T> handler) throws IllegalStateException;
    //添加Whole類型的MessageHandler
    <T> void addMessageHandler(Class<T> clazz, MessageHandler.Whole<T> handler) throws IllegalStateException;
    

    @OnMessage註解修飾的方法其實可以看成是一個特殊的MessageHandler

  • 獲取同步、異步模式的RemoteEndpoint

    //獲取異步RemoteEndpoint
    RemoteEndpoint.Async getAsyncRemote();
    //獲取同步RemoteEndpoint
    RemoteEndpoint.Basic getBasicRemote();
    

    我們可以利用RemoteEndpoint對象隨時向客戶端發送WebSocket數據幀。

  • 其它:

    //獲取用戶參數,初始參數等價於EndpointConfig.getUserProperties, 可以存放一些當前會話的臨時參數,類似Servlet的setAttribute
    Map<String, Object> getUserProperties();
    //獲取用戶權限相關對象
    Principal getUserPrincipal();
    //當前URI所對應的Endpoints中所有活躍Session的對象,可使用該對象進行諸如消息的廣播之類的功能
    Set<Session> getOpenSessions();
    

消息的發送

在前面介紹Session的API時候提到過,消息的發送分爲異步發送和同步發送,對應於Session對象的getAsyncRemotegetBasicRemote方法所返回的RemoteEndpoint.Async對象和RemoteEndpoint.Basic對象,RemoteEndpoint.AsyncRemoteEndpoint.Basic本身是一個接口,都繼承於RemoteEndpoint

public interface RemoteEndpoint {
	//設置是否允許暫存消息
	void setBatchingAllowed(boolean batchingAllowed) throws IOException;
	//是否允許暫存消息
	boolean getBatchingAllowed();
	//刷新所有緩衝區中的消息到客戶端
	void flushBatch() throws IOException;
	//發送ping消息
	void sendPing(ByteBuffer applicationData) throws IOException, IllegalArgumentException;
	//發送pong消息
	void sendPong(ByteBuffer applicationData) throws IOException, IllegalArgumentException;
}

RemoteEndpoint定義了一些基本的方法:發送PING數據幀(opcode0x9)和發送PONG數據幀(opcode0xA),並可以刷新暫存的數據幀,定義是否允許暫存數據幀的相關方法。

RemoteEndpoint.Basic接口:

interface Basic extends RemoteEndpoint {
	//發送文本類型(opcode爲0x1)的消息
	void sendText(String text) throws IOException;
	//發送二進制類型(opcode爲0x2)的消息
	void sendBinary(ByteBuffer data) throws IOException;
	//發送文本消息,並可以標記是否是最後一條消息,方便客戶端拼接數據幀,isLast爲true時FIN會被標記爲1
	void sendText(String fragment, boolean isLast) throws IOException;
	//發送二進制消息,並可以標記是否是最後一條消息,方便客戶端拼接數據幀,isLast爲true時FIN會被標記爲1
	void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException;
	//獲取面向客戶端的字節輸出流,可調用其write方法直接寫入數據
    OutputStream getSendStream() throws IOException;
    //獲取面向客戶端的字符輸出流,可調用其write方法直接寫入字符串
    Writer getSendWriter() throws IOException;
    //直接發送對象,必須要有對應的Encoder,否則會拋出EncodeException
    void sendObject(Object data) throws IOException, EncodeException;
}

RemoteEndpoint.Basic接口定義了同步發送字符類型的數據幀和二進制類型的數據幀的方法,也可以直接發送對象(在有對應的Encoder的前提下)。這些方法在被調用時,會一直阻塞到數據發送完成纔會返回,對性能有一定的影響。

RemoteEndpoint.Async接口:

interface Async extends RemoteEndpoint {
	//發送消息的超時時間,若在規定時間內消息還沒發送完成,就放棄發送
	long getSendTimeout();
	//設置超時時間
	void setSendTimeout(long timeout);
	//發送文本類型數據幀,立刻返回,在發送完成/超時/異常後會回調SendHandler
    void sendText(String text, SendHandler completion);
    //發送文本類型數據幀,立刻返回Future方便日後查詢發送結果
    Future<Void> sendText(String text);
    //發送二進制數據幀,立刻返回Future方便日後查詢發送結果
    Future<Void> sendBinary(ByteBuffer data);
    //發送二進制類型數據幀,立刻返回,在發送完成/超時/異常後會回調SendHandler
    void sendBinary(ByteBuffer data, SendHandler completion);
    //發送對象,立刻返回,需要有對應的Encoder
    Future<Void> sendObject(Object obj);
    //發送對象,立刻返回,在發送完成/超時/異常後會回調SendHandler
    void sendObject(Object obj, SendHandler completion);
}

相比RemoteEndpoint.Basic相關方法,RemoteEndpoint.Async就是可以異步發送數據,方法在調用後會立刻返回,由容器中的相關線程處理。用戶事後可以通過Future查詢執行結果,也可以通過指定SendHandler回調:

public interface SendHandler {
    void onResult(SendResult result);
}

SendResult對象僅包含兩個成員變量:

public final class SendResult {
    private final Throwable exception;
    private final boolean ok;
    //...
}

如果執行成功,那麼oktrue並且exceptionnull。如果執行失敗,那麼okfalse並且exception不爲null


數據幀處理流

WebSocket數據幀的接收和發送可以用以下圖來概括:
在這裏插入圖片描述
當Web容器收到一個數據幀時,因爲在握手階段已經確定Endpoint,所以只需要根據WebSocket數據幀的opcode字段判斷是二進制類型的數據還是字符類型數據,前者使用Decoder.Binary或者Decoder.BinaryStream實現類,後者使用Decoder.Text或者Decoder.TextStream實現類。解析成Java對象後,只需要在@OnMessage修飾的方法中找出合適的即可(符合參數列表類型的)。

Decoder接口定義如下:

public interface Decoder {
    void init(EndpointConfig endpointConfig);  //初始化實例
    void destroy();  //銷燬實例
    interface Binary<T> extends Decoder {
        T decode(ByteBuffer bytes) throws DecodeException;
        boolean willDecode(ByteBuffer bytes);
    }

    interface BinaryStream<T> extends Decoder {
        T decode(InputStream is) throws DecodeException, IOException;
    }

    interface Text<T> extends Decoder {
        T decode(String s) throws DecodeException;
        boolean willDecode(String s);
    }

    interface TextStream<T> extends Decoder {
        T decode(Reader reader) throws DecodeException, IOException;
    }
}

可以看出Decoder是多例的,其生命週期等同於一個WebSocket連接,在完成WebSocket握手後,Decoder會被實例化並調用其init方法(需要保證Decoder有一個無參的公有構造方法),在連接失效後,會調用destory方法,該方法一般用於釋放資源等。

Decoder分爲兩大類:二進制WebSocket數據幀解碼器和字符類型WebSocket數據幀解碼器。
其中,如果存在BinaryStreamTextStream,那麼會直接調用該方法的decode方法解析字節流或者字符流。如果存在TextBinary類型的解碼器,則會首先調用willDecode方法,如果返回true,那麼纔會調用decode方法,否則會嘗試尋找另外一個Decoder實現類。

數據發送需要經過編碼器Encoder,生命週期和Decoder相同。只有在調用RemoteEndpointsendObject時纔會利用到Encoder,其它方法都是直接傳遞給客戶端的。Encoder同樣區分字符類型和二進制類型:

public interface Encoder {
    void init(EndpointConfig endpointConfig);
    void destroy();
    interface Text<T> extends Encoder {
        String encode(T object) throws EncodeException;
    }
    interface TextStream<T> extends Encoder {
        void encode(T object, Writer writer) throws EncodeException, IOException;
    }
    interface Binary<T> extends Encoder {
        ByteBuffer encode(T object) throws EncodeException;
    }
    interface BinaryStream<T> extends Encoder {
        void encode(T object, OutputStream os) throws EncodeException, IOException;
    }
}

如果一個消息是由多個對象組成的,那麼實現帶Stream的Encoder,否則選擇不帶Stream的Encoder
帶Stream的encode方法需要根據傳入的object對象寫入到OutputStream中,類似於:

void encode(T object, OutputStream os) throws EncodeException, IOException {
	byte[] b = object.serialize();
	os.write(b);
}

不帶Stream的encode方法一般直接返回其解碼結果就行:

ByteBuffer encode(T object) throws EncodeException {
	byte[] b = object.serialize();
	return ByteBuffer.wrap(b);
}

本文由官方文檔以及源代碼整理而成,如果有錯誤歡迎在評論區指出。

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