一、使用Tomcat提供的WebSocket庫 

Java可以使用Tomcat提供的WebSocket庫介面實現WebSocket服務,程式碼編寫也非常的簡單。現在的H5聯網遊戲基本上都是使用WebSocket協議,基於長連線,伺服器可以主動推送訊息,而不是傳統的網頁採用客戶端輪詢的方式獲取伺服器的訊息。下面給出簡單使用TomcatWebSocket服務的基本程式碼結構。

 1 @ServerEndpoint("/webSocket")
2 public class WebSocket {
3 @OnOpen
4 public void onOpen(Session session) throws IOException{
5 logger.debug("新連線");
6 }
7 @OnClose
8 public void onClose(){
9 logger.debug("連線關閉");
10 }
11 @OnMessage
12 public void onMessage(String message, Session session) throws IOException {
13 logger.debug("收到訊息");
14 }
15 @OnError
16 public void onError(Session session, Throwable error){
17 error.printStackTrace();
18 }
19 }

二、WebSocket協議的整個流程

  1. 基於TCP協議

WebSocket本質是基於TCP協議的,採用Java編寫WebSocket服務時可以使用NIO或者AIO實現高併發的服務。

  2. 握手過程

客戶端採用TCP協議連線伺服器指定埠後,首先需要傳送一條HTTP的握手協議

GET /web HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 127.0.0.1:8001
Origin: http://127.0.0.1:8001
Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw==
Sec-WebSocket-Version: 13

請求的頭裡面必須包含以下內容:

1. Connection 其值為Upgrade,表示升級協議

2. Upgrade  其值為websocket,表示升級為WebSocket協議

3. Sec-WebSocket-Key 客戶端傳送給伺服器的金鑰,用於標識每個客戶端,其值是16位的隨機base64編碼。

4. Sec-WebSocket-Version WebSocket的協議版本
        伺服器收到這條協議驗證成功後進行協議升級,並且不會關閉Socket連線,併發送給客戶端響應升級握手成功的HTTP協議包。

HTTP/1.1 101 Switching Protocols
Content-Length: 0
Upgrade: websocket
Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g=
Connection: Upgrade
Date: Wed, 21 Jun 2017 03:29:14 GMT

響應的協議包裡面,首先是101的狀態碼,更換協議;其中最重要的就是Sec-WebSocket-Accept欄位。其值是通過客戶端的Key加上固定的"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"金鑰,通過採用16位的base64編碼後傳送給客戶端驗證,如果客戶端也驗證成功就表示握手完成。

1 String acc = secKey + WEBSOCK_MAGIC_TAG;
2 MessageDigest sh1 = MessageDigest.getInstance("SHA1");
3 String key = Base64.getEncoder().encodeToString(sh1.digest(acc.getBytes()));

    3. 資料的讀寫

握手成功後就可以進行資料傳送和讀取,WebSocket的資料可以是二進位制或者純文字。每次讀取和傳送資料需要打包成幀資料,需要按照其標準的格式進行傳送或讀取才能夠正常的進行資料通訊。

上圖就是幀資料的結構圖,解析幀資料的程式碼如下,由於是摘錄的部分程式碼,所以只能作為理解和參考,不可直接使用。

 1 protected WebSocketFrameData ParseFrame(NetPacketBuffer bytes){
2 bytes.mark();
3 WebSocketFrameData frame = new WebSocketFrameData();
4 int opData = bytes.readByte();
5 frame.UnPackOpCodeHeader(opData); // 第一步
6 int length = frame.UnPackMaskHeader(bytes.readByte()); // 第二步
7 // 讀取長度
8 if (length == 126) {
9 length = bytes.readShort();
10 } else if (length == 127){
11 length = (int) bytes.readInt64();
12 }
13 // 資料不足,進來的是半包
14 if(length + 4 > bytes.remaining()){
15 bytes.reset(); //
16 return null;
17 }
18 // 讀取mask if frame.mMasked
19 byte[] masks = new byte[4]; // 第三步
20 for (int i = 0; i < 4; i++) {
21 masks[i] = (byte) bytes.readByte();
22 }
23 frame.mLength = length;
24 frame.mData = bytes.readMulitBytes(length);
25 frame.MaskData(masks); // 第四步
26 return frame;
27 }

上面程式碼中第一步是解析出當前幀是否是最後幀mFin標記、操作碼mOpCode,採用位處理,具體的實現如下。

1 public void UnPackOpCodeHeader(int opData){
2 mRsv1 = (opData & 64) == 64;
3 mRsv2 = (opData & 32) == 32;
4 mRsv3 = (opData & 16) == 16;
5
6 mFin = (opData & 128) == 128;
7 mOpCode = (opData & 15);
8 }

第二步在讀取長度前,先解析當前幀是否有采用Mask掩碼加密處理,並且裡面有可能包含整個幀的長度資訊,具體看上面的判斷程式碼。

1 public int UnPackMaskHeader(int mkData){
2 mMasked = (mkData & 128) == 128;
3 return (mkData & 127); // 這裡返回的是長度資訊
4 }

接下來就是讀取Mask內容,注意只有客戶端傳送給服務端時需要採用Mask對資料做處理,服務端傳送給客戶端時不需要做處理。最後通過Mask掩碼解析出真實資料。

1 public void MaskData(byte[] masks){
2 if (!mMasked or masks.length == 0) return ;
3 for (int i = 0; i < mLength; i++) {
4 mData[i] = (byte) (mData[i] ^ masks[i % 4]);
5 }
6 }

以上就解析出單幀的資料,幀資料可以分為訊息資料(細分為文字資料和二進位制資料)、PING包、PONG包、CLOSE包、CONTINUATION包(資料未傳送完成包)。而且幀資料又有mFin標記資料是否完整,否則需要將多個幀資料合成一個完整的訊息資料。

 1 // 讀取幀資料,可能存在多幀資料,因此需要手動拆分
2 WebSocketFrameData frame = ParseFrame(mCachePacket);
3 if(frame == null){
4 break; // 說明資料不完整,暫不處理。
5 }
6 // 不完整的幀的時候,只有第一幀會標記幀的型別
7 opCode = opCode == -1? frame.mOpCode: opCode;
8 mCacheFrame.append(frame.mData, 0, frame.mLength);
9 if(!frame.mFin) // 非完整的資料不處理。
10 {
11 continue;
12 }
13 // 處理完整的資料
14 switch(opCode)
15 {
16 case WebSocketFrameData.OP_TEXT:
17 case WebSocketFrameData.OP_BINARY:
18 mCacheFrame.flip();
19 this.OnMessage(mCacheFrame, opCode);
20 break;
21 case WebSocketFrameData.OP_PING:
22 this.OnPing(mCacheFrame);
23 break;
24 case WebSocketFrameData.OP_PONG:
25 this.OnPong(mCacheFrame);
26 break;
27 case WebSocketFrameData.OP_CLOSE:
28 this.OnClosed();
29 break;
30 case WebSocketFrameData.OP_CONTINUATION:
31 this.Close();
32 break;
33 }
34 opCode = -1;
35 mCacheFrame.clear();

讀取整個客戶端的協議資料流程就已經完成了,服務端傳送回去的資料就只需要注意兩點:

        1. 大的資料包需要分幀資料傳送。

        2. 不需要採用Mask掩碼加密,因此Mask位置設定為0,並且不寫入掩碼資料。

三、最後

WebSocket協議已經在H5的遊戲中使用了,學習有助於以後工作中的使用.文章來自我的公眾號,大家如果有興趣可以關注,具體掃描關注下圖。