Java實現WebSocket服務


一、使用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的游戲中使用了,學習有助於以后工作中的使用.文章來自我的公眾號,大家如果有興趣可以關注,具體掃描關注下圖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM