1. 程式人生 > >通訊框架 t-io 學習——websocket 部分源碼解析

通訊框架 t-io 學習——websocket 部分源碼解析

update remove leg return hashmap ext 菜鳥 未來 offset

前言

  前端時間看了看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文檔對比,還是多少能夠理解一些的。再次感謝開源貢獻者,向所有開源大神致敬。

通訊框架 t-io 學習——websocket 部分源碼解析