前言
前端時間看了看t-io的websocket部分原始碼,於是抽時間看了看websocket的握手和他的通訊機制。本篇只是簡單記錄一下websocket握手部分。
WebSocket握手
好多人都用過websocket,不過有的都是在框架之上,只知道連線某個地址,然後呼叫js API就可以使用websocket了。但是通過閱讀t-io的原始碼才稍微有點明白,服務端到底做了什麼。將t-io的websocket demo執行起來之後,我們看一下請求。
可以看到,請求頭部分:
Connection:Upgrade 固定
Upgrade:websocket 固定
Host:為websocket請求地址
Sec-WebSocket-Version:13,websocket協議版本號
Sec-WebSocket-Key:傳送給服務端需要校驗的key,是一個Base64 encode的值,這個是瀏覽器隨機生成的。那麼服務端如果響應的話,需要做如下操作:將 Key 追加固定字串 :“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然後進行SHA-1加密,在轉化為base64.
服務端響應如下:
Status Code:101 Switching Protocols
sec-websocket-accept:為上文中轉化為base64的串。
upgrade:升級為websocket協議
握手成功,可以進行通訊。
握手原始碼
程式碼來源:tio/websocket/server/WsServerAioHandler.java
public static HttpResponse updateWebSocketProtocol(HttpRequest request, ChannelContext channelContext) { //首先獲取請求頭部資訊 Map<String, String> headers = request.getHeaders(); //獲取Sec-WebSocket-Key String Sec_WebSocket_Key = headers.get(HttpConst.RequestHeaderKey.Sec_WebSocket_Key); //如果key是空的話,肯定不會握手成功 if (StringUtils.isNotBlank(Sec_WebSocket_Key)) { //追加固定串 String Sec_WebSocket_Key_Magic = Sec_WebSocket_Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; //SHA-1加密 byte[] key_array = SHA1Util.SHA1(Sec_WebSocket_Key_Magic); //轉化為base64 String acceptKey = BASE64Util.byteArrayToBase64(key_array); //構造響應體 HttpResponse httpResponse = new HttpResponse(request, null); //響應狀態碼 101 Switching Protocols httpResponse.setStatus(HttpResponseStatus.C101); Map<String, String> respHeaders = new HashMap<>(); //Connection:upgrade respHeaders.put(HttpConst.ResponseHeaderKey.Connection, HttpConst.ResponseHeaderValue.Connection.Upgrade); //Upgrade:websocket respHeaders.put(HttpConst.ResponseHeaderKey.Upgrade, "WebSocket"); //Sec-WebSocket-Accept:生成的base64串 respHeaders.put(HttpConst.ResponseHeaderKey.Sec_WebSocket_Accept, acceptKey); //設定響應頭 httpResponse.setHeaders(respHeaders); //返回響應資訊 握手成功 return httpResponse; } return null; }
WebSocket 資料幀解析
注:部落格部分內容來源於:https://github.com/zhangkaitao/websocket-protocol/wiki/5.%E6%95%B0%E6%8D%AE%E5%B8%A7 有興趣的同學可以直接讀本連結內容。
相信很多人從其他部落格中也看過這個圖,當然啦,這個圖是官方出品的權威資料幀格式圖。
其實我第一眼看的時候確實看不懂,不過沒關係,一點一點的看。
FIN:1bit,指示這個訊息是否為最後片段,1是,0否。如果不是最後片段,則服務端需要將所有訊息接受完並組裝成一個完整的訊息才可以。(t-io中目前只支援FIN=1)
RSV123每個長度為1bit,目前就都是固定 0。
opcode:4bit,資料操作型別。
- %x0 代表一個繼續幀
- %x1 代表一個文字幀
- %x2 代表一個二進位制幀
- %x3-7 保留用於未來的非控制幀
- %x8 代表連線關閉
- %x9 代表ping
- %xA 代表pong
- %xB-F 保留用於未來的控制幀
MASK:1bit,是否掩碼,1掩碼,0非掩碼。從客戶端傳送到服務端的這個值必須為1,否則服務端不接受。服務端返回到客戶端的這個值必須為 0.
Payload len:負載資料的長度,7bit。由於7bit只能儲存0-127,所以為了能夠表示準確的長度,在這個值為0-125區間的時候,payload length的長度就是該值。當 值為126的時候,後邊兩個位元組(16位)的值表示長度。當值為127的時候,後邊8位元組(64位)的值表示長度。
Mask key:掩碼,0或4個bit。值取決於MASK是否為1.在有掩碼的情況下,資料就要根據掩碼來解析。否則不用解析。解析規則為:每個位元組的值與掩碼的索引(位元組索引值對4取模)異或運算。(array[i] = array[i] ^ mask[i % 4])
其實說實話我也沒弄得非常懂,但是基本瞭解了以上這些知識之後,我們就可以讀懂原始碼的意思了。
資料幀解析原始碼
程式碼來源:tio/websocket/common/WsServerDecoder.java
程式碼中的註釋為我自己的理解所新增的註釋,不一定正確。(由於原始碼中有部分註釋,我的註釋新增“注”字以作區分)
public static WsRequest decode(ByteBuffer buf, ChannelContext channelContext) throws AioDecodeException { WsSessionContext imSessionContext = (WsSessionContext) channelContext.getAttribute(); List<byte[]> lastParts = imSessionContext.getLastParts(); //第一階段解析 int initPosition = buf.position(); int readableLength = buf.limit() - initPosition; int headLength = WsPacket.MINIMUM_HEADER_LENGTH; if (readableLength < headLength) { return null; } //注:讀取第一個位元組 這裡以 0x81舉例 它的二進位制為:10000001 byte first = buf.get(); //注:這個 0xff還是很有意思的,當byte型別想轉為int型別的時候,比如: int res = byteValue & 0xff; //int b = first & 0xFF; //轉換成32位 // 0x80(127) 10000000 // 0x81(128) 10000001 // 此行程式碼說實話,我是用了很長的時間才理解,說來慚愧,剛開始連 & 操作符啥意思都不清楚。 // 按位與運算子“&”是雙目運算子。其功能是參與運算的兩數各對應的二進位相與。只要對應的二個二進位都為1時,結果位就為1。 // 參與運算的兩個數均以補碼出現。 // 0x80 & 0x81 10000000 boolean fin = (first & 0x80) > 0; //得到第8位 10000000>0 //注:這段我不理解什麼意思,為什麼要右移4位 @SuppressWarnings("unused") int rsv = (first & 0x70) >>> 4;//得到5、6、7 為01110000 然後右移四位為00000111 //注:獲取操作碼 //0x0f 00001111 (按位與操作,前四位都為0,那麼操作結果就是opCode的值) byte opCodeByte = (byte) (first & 0x0F);//後四位為opCode 00001111 //注:轉換OpCode Opcode opcode = Opcode.valueOf(opCodeByte); if (opcode == Opcode.CLOSE) { //Aio.remove(channelContext, "收到opcode:" + opcode); //return null; } if (!fin) { log.error("{} 暫時不支援fin為false的請求", channelContext); Aio.remove(channelContext, "暫時不支援fin為false的請求"); return null; //下面這段程式碼不要刪除,以後若支援fin,則需要的 // if (lastParts == null) { // lastParts = new ArrayList<>(); // imSessionContext.setLastParts(lastParts); // } } else { imSessionContext.setLastParts(null); } //注:開始解析第二個位元組。8-16位,第八位為mask掩碼值1或者0,後7位為payload length byte second = buf.get(); //向後讀取一個位元組 //注:又是 & 操作。 0xff:11111111 // 11111111 & 10000001 = 10000001 向右移動七位,只剩下第一位的值 00000001 //所以該操作過後就知道第一位為 0 或者 1 ,得知 payload Data是否經過掩碼處理 boolean hasMask = (second & 0xFF) >> 7 == 1; //用於標識PayloadData是否經過掩碼處理。如果是1,Masking-key域的資料即是掩碼金鑰,用於解碼PayloadData。客戶端發出的資料幀需要進行掩碼處理,所以此位是1。 // Client data must be masked if (!hasMask) { //第9為為mask,必須為1 //throw new AioDecodeException("websocket client data must be masked"); } else { //注:有掩碼的情況下,掩碼佔用4個位元組,所以在這裡headLength + 4 headLength += 4; } //注:第一位為mask位置,後7位為payload length //0x7f : 01111111 //&操作過後得到payload的值 //讀取後7位 Payload legth,如果<126則payloadLength int payloadLength = second & 0x7F; byte[] mask = null; //注:如果payloadLength = 126,那麼說明這個值不是真正的payloadLength,後邊兩個位元組才表示真正的length //為126讀2個位元組,後兩個位元組為payloadLength if (payloadLength == 126) { //需要多佔兩個位元組表示payloadLength。headlength + 2 headLength += 2; if (readableLength < headLength) { return null; } payloadLength = ByteBufferUtils.readUB2WithBigEdian(buf); log.info("{} payloadLengthFlag: 126,payloadLength {}", channelContext, payloadLength); } //注:如果payloadLength = 127,則後 8個位元組 64位長度的值表示payloadLength //127讀8個位元組,後8個位元組為payloadLength else if (payloadLength == 127) { //頭部長度 + 8 headLength += 8; if (readableLength < headLength) { return null; } //注:我猜測getLong方法就讀取buf中下一位長整數,即64位的payloadLength(first ,second都已經讀取完) //|first|second|payloadLength| payloadLength = (int) buf.getLong(); log.info("{} payloadLengthFlag: 127,payloadLength {}", channelContext, payloadLength); } if (payloadLength < 0 || payloadLength > WsPacket.MAX_BODY_LENGTH) { throw new AioDecodeException("body length(" + payloadLength + ") is not right"); } if (readableLength < headLength + payloadLength) { return null; } if (hasMask) { //注:有掩碼,掩碼長度為4個位元組,讀取掩碼的值 mask = ByteBufferUtils.readBytes(buf, 4); } //第二階段解析 WsRequest websocketPacket = new WsRequest(); //注:設定各種屬性值 websocketPacket.setWsEof(fin); websocketPacket.setWsHasMask(hasMask); websocketPacket.setWsMask(mask); websocketPacket.setWsOpcode(opcode); websocketPacket.setWsBodyLength(payloadLength); if (payloadLength == 0) { return websocketPacket; } //注:讀取payloadLength長度的body值 byte[] array = ByteBufferUtils.readBytes(buf, payloadLength); if (hasMask) { //注:有掩碼,所以需要通過掩碼解析 for (int i = 0; i < array.length; i++) { //^操作 位值相同為0 ,不同為1 // 00001111 ^ 00001010 = 00000101 array[i] = (byte) (array[i] ^ mask[i % 4]); } } if (!fin) { //lastParts.add(array); log.error("payloadLength {}, lastParts size {}, array length {}", payloadLength, lastParts.size(), array.length); return websocketPacket; } else { int allLength = array.length; if (lastParts != null) { for (byte[] part : lastParts) { allLength += part.length; } byte[] allByte = new byte[allLength]; int offset = 0; for (byte[] part : lastParts) { System.arraycopy(part, 0, allByte, offset, part.length); offset += part.length; } System.arraycopy(array, 0, allByte, offset, array.length); array = allByte; } websocketPacket.setBody(array); if (opcode == Opcode.BINARY) { } else { try { String text = null; text = new String(array, WsPacket.CHARSET_NAME); websocketPacket.setWsBodyText(text); } catch (UnsupportedEncodingException e) { log.error(e.toString(), e); } } } return websocketPacket; }
總結
由於本人也是小菜鳥,能看懂的就那麼多了,很多程式碼都讀不懂。哎,大神就是大神啊,編碼都精準到每一個bit上了。不過通過閱讀原始碼和websocket文件對比,還是多少能夠理解一些的。再次感謝開源貢獻者,向所有開源大神致敬。