如果不是很清楚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
。
Encoder
和Decoder
對應編碼器和解碼器,如果@OnMessage
修飾的方法的參數列表有非基本類型的對象,就需要解碼器將二進制流或者字符流轉換爲Java對象。Encoder
和Decoder
在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
對象的getAsyncRemote
和getBasicRemote
方法所返回的RemoteEndpoint.Async
對象和RemoteEndpoint.Basic
對象,RemoteEndpoint.Async
和RemoteEndpoint.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數據幀(opcode
爲0x9
)和發送PONG數據幀(opcode
爲0xA
),並可以刷新暫存的數據幀,定義是否允許暫存數據幀的相關方法。
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;
//...
}
如果執行成功,那麼ok
爲true
並且exception
爲null
。如果執行失敗,那麼ok
爲false
並且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數據幀解碼器。
其中,如果存在BinaryStream
或TextStream
,那麼會直接調用該方法的decode
方法解析字節流或者字符流。如果存在Text
或Binary
類型的解碼器,則會首先調用willDecode
方法,如果返回true
,那麼纔會調用decode
方法,否則會嘗試尋找另外一個Decoder
實現類。
數據發送需要經過編碼器Encoder
,生命週期和Decoder
相同。只有在調用RemoteEndpoint
的sendObject
時纔會利用到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);
}
本文由官方文檔以及源代碼整理而成,如果有錯誤歡迎在評論區指出。