1. 程式人生 > 其它 >關於學習websocket遇到的技術問題及總結

關於學習websocket遇到的技術問題及總結

(注:解決後的原始碼在最下面的技術總結裡)

最近本來是為一個課程作業做設計(不過老師有提供課題,我這個怕是留著做畢設了),設計著設計著,就發現我可能需要實現一個web端的聊天功能,巧的是,最近在學javasocket程式設計,不巧的是,socket和websocket還有點不太一樣(握手,解析資料,巴拉巴拉)。

先寫一下目前遇到的問題:

客戶端傳送一個數據給服務端的時候,我們不是要解析這個資料嘛,按照這個資料幀的格式來看

(來源:https://www.cnblogs.com/laohaozi/p/12537571.html)

 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

別的不看,就看第二個位元組Payload len,原本我是把所有的接收資料的payloadlen都輸出出來的,結果發現全部是負值,觀察了一下規律後發現,其實data[1]的值其實是補碼,資料真正的payloadlen取data[1]補碼就行,但是和0x7f與一下也行,原因在下面。

data[1]:-113
payloadlen:15

原本應該這樣結束才對,但是我試了一下邊緣長度125,126,127,128,試出問題了

真正的資料長度(byte) data[1](byte) payloadlen(byte)
124 -4 124
125 -3 125
126 -2 126
127 -2 126
128 -2 126

資料長度再大往下都是126,然後我看著上面那個資料幀的格式陷入了沉思:127呢?

在思考了很長一段時間後我意識到一件事情,我是個傻X,126提供的資料長度已經很長了,我可能沒有耐心測試到127出現。

然後我就一直在測試資料範圍,發現了幾件事情,一件一件說:

①當傳送資料過於長的時候,就會進行分片(寫到後面發現這個“分片”是不對的,大家看個樂呵就行)。

②當客戶端傳送資料過長的時候偶爾會出現mask位變成0的情況,隨後連線會因為這個關閉,也可能不會關閉,挺玄學的。(同上)

③當mask位變成0的時候,data[1]就和payloadlen一樣了,這個很好解釋,mask位被當作符號位處理了,所以正常來說data[1]後7位就是原碼,而且本來就應該和0x7f相與。

④當發生分片時,第一個分片的fin=1,後面的分片可能是1也可能是0。(同①②)

-------------------------------------------------------分割線,此處為浪費時間,沒有看的價值--------------------------------------------

然後再測了幾次發現是在1024個數字字元時發生分片。

具體如下(某一次測試的資料,分片後fin和mask可能都會變,payloadlen在第二個分片開始也是不一樣的,即便是一樣的資料)

資料長度(byte) 分片? payloadlen(byte) fin mask
128 N 126 1 1
256 N 126 1 1
512 N 126 1 1
1024 Y 126 1 1
7 0 0
2048 Y 126 1 1
111 1 1
111 1 1

由於是1024開始有分片,所以我要找一下512-1024中間,什麼時候開始有分片

資料長度(byte) 分片? payloadlen(byte) fin mask
768 N 126 1 1
896 N 126 1 1
960 N 126 1 1
992 N 126 1 1
1008 N 126 1 1
1016 N 126 1 1
1020 Y 126 1 1
24 1 1
1018 Y 126 1 1
91 0 1
1017 Y 126 1 1
126 0 1

最終測試出來是在1017這裡,剛剛好就是1017,往下不分片,1017及以上一定會分片,而且很有意思,兩個分片的payloadlen都是126,試了很多次都不會變,似乎當傳送資料的頻次過快時,連線就會關閉。

為什麼是1017呢?為什麼1017個字元剛剛好兩個分片都是126。

這個問題先擱置在這裡。(1017已經解決,但是為什麼“分片”都是126還不知道)

-----------------------------------------------------------------------分割線完-----------------------------------------------------------------------

然後我就想輸出一下資料的真實長度看看呢,126的情況下輸出extendedpayloadlen

fin和mask現在沒啥意義就不寫了

資料長度 分片code Ext
1024 1 13621
2 1542
2048 1 25987
2 22102
3 22102
4096 1 22359
2 25700
3 25700
4 25700
5 25700
//看到接收到的資料是這樣,我就忍不住把程式碼貼出來了
long extendPayloadLen = ((data[2]&0xff)<<8)+data[3]; //byte[] data = byte[1024];,然後通過inputStream一次性全讀進來的,每次讀1024個。。。我寫到這裡才恍然大悟,為什麼前面1017位元組資料後資料會被分次讀入,原因就在這裡,一次讀1024個位元組,超過的資料下次再載入,所以得到資料是這樣的,偶爾mask和fin會變成0的原因就在這裡,根本就是讀錯了。後面我把緩衝區改成2048後果然1024位元組的資料就不‘分片’了

再重新捋一下,1017個位元組後資料會被分次讀入,因為一次讀1024位元組,那除了我傳送的1017個位元組的資料外,剩下7個位元組就是資料幀的組成部分了。為了保證7個位元組結論的準確性,我在緩衝區2048和4096的情況下發送資料做測試,結果2040位元組不會“分片”,2041會,4088不會,4089會。

 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

也就是說,我一次能夠接收的資料的最大數量=緩衝區大小-7,那7個位元組就是資料幀的重要組成部分,但是回過頭來看除了Payload Data的其他位元組,一共有8個位元組(126的情況下),這就給我整不會了。

那現在就有兩個問題:

①這7個位元組代表什麼?資料幀中明明有8個位元組是必要的

②為什麼傳送相同的資料,extendedpayloadlen值是不一樣的,雖然差別不大(沒有'分片時)

先解決第②個問題,我犯了個錯誤,就是我一直在寫2個位元組,然後雖然data[0]和data[1]沒寫錯,但是我漏了個data[2],直接寫data[3]和data[4],data[3]不變,看來純度還是不夠,因為是高位,所以看上去資料變化不大。我發現一邊默唸一邊思考一邊記下來能發現好多之前光想想不到的事情,特別是我要寫成一篇文章發出去的時候。

面對①,我有一個猜想:其實7位元組結論不對,或許傳送的資料存在缺少的部分沒有接收到,為了驗證這個是不是對的,只能先解析出資料輸出看看,為了方便,緩衝區設定為256位元組,傳送的資料設為248位元組。

-----------------------------------------------------------------------分割線-----------------------------------------------------------------------

以上是一夜未睡寫的,睡到下午起床神清氣爽了不少,當然①我也不管了,上面的內容就當放屁了,至於為什麼保留這些放屁的內容,因為這是我思考過程中踩的坑走錯的路,所以發出了銘記一下。

ok,因為接收資料的read()方法可以根據給的byte陣列長度去讀取對應位元組的資料,所以只要在payloadData前的標誌位,掩碼等資訊解析出來,然後建立payloadlen長度的陣列去接收資料幀剩下的所有位元組就行了。

-----------------------------------------------------------------------技術總結-----------------------------------------------------------------------

首先是握手資訊,瀏覽器和服務端建立連線時,瀏覽器會發送一個握手請求,這個握手請求的opcode是1,也就是文字資訊,直接用BufferedReader也是可以讀出來的。這個資訊不會加密,可以直接打印出來。如下:

GET / HTTP/1.1
Host: localhost:13655
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Upgrade: websocket
Origin: http://localhost
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Sec-WebSocket-Key: hoBQ13wddX4Zsisa/o7oYg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

這段資訊中最重要的就是Sec-WebSocket-Key欄位

收到這段資訊後反正用盡任何手段獲取到這個欄位就行了,在這裡我用的是正則表示式匹配

"Sec-WebSocket-Key: (.+)"後的內容(:後有空格要加上去)

public static String getWSKeyInfo(String keymsg){
        String pattern = "Sec-WebSocket-Key: (.+)"; //匹配模式
        Pattern p = Pattern.compile(pattern);
        Matcher m = p.matcher(keymsg);
        if (m.find()){
            return m.group(1); //group(1)的值就是(.+)中的內容,即我要的key
        }else{
            return null;
        }
    }

獲取到key之後,我們要用到一個magic的字串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",然後和key拼接起來,這裡用的DigestUtils和Base64是apache的codec的包。拼接結果先經過sha1編碼,再經過base64編碼(得到一個byte陣列),後面要把這個accept作字串拼接,所以最後把byte轉為str返回

public static String getWSKeyAccept(String key) throws UnsupportedEncodingException {
        String magic_str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
        String value = key+magic_str;
        byte[] sha1encode = DigestUtils.sha1(value);
        byte[] base64encode = Base64.encodeBase64(sha1encode);
        String accept = new String(base64encode,"utf-8");
        return accept;
    }

得到accept資訊後,進行字串拼接,沒什麼好說的,照這個格式寫返回資訊就可以。

public static  String getRetMsg(String key) throws UnsupportedEncodingException {
        String accept = getWSKeyAccept(key);
        StringBuffer sb = new StringBuffer();
        sb.append("HTTP/1.1 101 Switching Protocols\r\n");
        sb.append("Upgrade: websocket\r\n");
        sb.append("Connection: Upgrade\r\n");
        sb.append("Sec-WebSocket-Accept: ");
        sb.append(accept);
        sb.append("\r\n");
        sb.append("\r\n");
        String retMsg = sb.toString();
        return retMsg;
    }

獲取到返回資訊後,把輸出流返回資訊write再flush就行了。

-----------------------------------------------------------------------分割線-----------------------------------------------------------------------

然後是獲取資訊的部分,這部分卡了我好幾天,我覺得原因在於我對這個websocket的資料幀瞭解甚少,而且不願意去理解它,希望從其他博文裡拿現成的,但是這東西真的不難,自己不理解的話就沒有學的意義了。

廢話不多說,握手建立後,從瀏覽器傳送過來的字元資訊的資料幀的opcode值為2,也就是代表位元組流資訊,事實上也要基於位元組去解析這些資料,為了方便起見,我就直接用InputStream和OutputStream,包括上面的握手返回資訊,將握手資訊轉為二進位制流,直接用OutputStream去write也是OK的。

以下為接收資料並輸出

while (true){
    byte[] first = new byte[1];
    ins.read(first); //將流中第一個位元組的資料讀入first
    int finflag = (first[0]&0x80)>>7; //終止位資訊fin = (xyyyyyyyy & 10000000)>>7 = x,
    int opcode = first[0]&0x0f; //表示接收的資訊型別opcode = yyyyxxxx & 0000ffff = 0000xxxx
    //opcode為0時是接收附加資料,1是文字資料,2是二進位制流資料,8是關閉連線
    if (opcode == 8){
        System.out.println("連線被瀏覽器關閉"); 
        webSocketServer.close(); //收到opcode=8時就關閉連線,把所有的流和socket關閉
        break;
    }
    byte[] second = new byte[1];
    ins.read(second); //讀入第二個位元組
    int mask = (second[0]&0x80)>>7; //是否有掩碼,從瀏覽器傳送過來的資訊一定有,mask = (xyyyyyyyy & 10000000)>>7 = x
    int payloadlen = second[0]&0x7f; // payloadlen = yxxxxxxxx & 011111111 = 0xxxxxxxx
    int extendPayloadLen = 0; 
    if (payloadlen == 126){
        // 若payloadlen == 126,則接收的資料長度為後兩個位元組的值,
        int index = 0;
        byte[] extended = new byte[2];//附加長度
        ins.read(extended); //讀入附加長度的值
        while(index < 2){
            extendPayloadLen = extendPayloadLen<<8; //當前的獲得的位元組全部左移8位
            extendPayloadLen += (extended[index++]&0xff); //當前獲得的資料加上下一個位元組
        }
        payloadlen = extendPayloadLen; //得到真正的資料長度
    }else if(payloadlen == 127){
        // 若payloadlen == 127,則接收的資料長度為後八個位元組的值。,同上一個if中的內容
        int index = 0;
        byte[] extended = new byte[8];
        ins.read(extended);
        while(index < 8){
            extendPayloadLen = extendPayloadLen<<8;
            extendPayloadLen += (extended[index++]&0xff);
        }
        payloadlen = extendPayloadLen;
    }else{
        // 若payloadlen < 126,接收的資料長度為0~125,
        payloadlen = payloadlen; //脫褲子放屁
    }
    byte[] maskingkey = new byte[4];
    ins.read(maskingkey); //讀入maskingkey
    byte[] themsg = new byte[payloadlen]; //真正的資訊佔payloadlen個位元組
    ins.read(themsg,0,payloadlen); //原本設定themsg的長度是payloadlen+128,感覺安全一點,但是轉str後在linux下會把空位元組段也輸出出來,不是很好看,所以就直接用payloadlen,跟著協議走應該OK
    int index = 0;
    if (mask == 1){
        //如果資料幀有掩碼
        while(index < payloadlen){
            //themsg[index]和maskingkey[index%4]進行異或運算後得到真正的資料
            themsg[index] = (byte) ((themsg[index]^maskingkey[index%4])&0xff);
            index++;
        }
    }
    String msg = new String(themsg,"UTF-8"); //把byte陣列轉為str,編碼是utf8
    System.out.println(msg);
    System.out.println(msg.length());
}

傳送資料的部分還沒有寫,之後寫了會連同接收資料的部分一起再發一次博文。