一、使用Tomcat提供的WebSocket庫
Java可以使用Tomcat提供的WebSocket庫接口實現WebSocket服務,代碼編寫也非常的簡單。現在的H5聯網游戲基本上都是使用WebSocket協議,基於長連接,服務器可以主動推送消息,而不是傳統的網頁采用客戶端輪詢的方式獲取服務器的消息。下面給出簡單使用Tomcat的WebSocket服務的基本代碼結構。
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的游戲中使用了,學習有助於以后工作中的使用.文章來自我的公眾號,大家如果有興趣可以關注,具體掃描關注下圖。