最近在做的一個項目中需要使用到HTML5中引入的WebSocket技術,本來以為應該很容易就能搞定,誰知道在真正上手開發了以后才發現有很多麻煩的地方,雖然我們是一個以前端開發和設計見長的團隊,而且作為一個二手程序猿又長期不被待見,但是為了讓有同樣需求的朋友少走些彎路,我還是決定把實現方法貼在這個地方。
關于WebSocket的基本概念,維基百科上解釋的很清楚,而且網上也能搜出來一大把,這里就略過不表,直接進入正題。
這次的問題首先有一個前提,就是得用Python來實現這個服務器,如果對具體語言沒有限制的話,推薦大家首選Node.js的一個第三方庫:Socket.IO,非常好用,10分鐘不打針不吃藥搞定WebSocket Server,而且用JS來寫后端,相信也能對上很多文藝開發者的胃口。
但是如果選擇用Python,google搜索的結果幾乎都不能用,最要命的問題是,WebSocket協議本身還是一個草案,所以不同瀏覽器支持的協議版本有所不同,Safari 5.1支持的是老版本協議Hybi-02,Chrome 15以及Firefox 8.0支持的是新版本協議Hybi-10,老版本協議和新版本協議在建立通信的握手方法還有數據傳輸的格式要求上都有所不同,導致網上大多數實現方式只能適用于Safari瀏覽器,并且Safari和C&F瀏覽器之間無法互相通信。
首先第一步需要解釋的是新、舊版本WebSocket協議的握手方式。我們先來看看三個不同瀏覽器發送的握手數據的結構:
Chrome:
可以看出,Chrome和Firefox實現的是新版協議,因此只傳輸了一個”Sec-WebSocket-Key”頭以供服務端生成握手Token,但是遵循老版本的Safari的數據中有兩個Key:”Sec-WebSocket-Key1″和”Sec-WebSocket-Key2″,因此服務端在生成握手Token的時候,需要做一次判斷。先來看使用老版本協議的Safari,Token生成算法如下:
取出Sec-WebSocket-Key1中的所有數字字符形成一個數值,這里是1427964708,然后除以Key1中的空格數目,這里好像是6個空格,得到一個數值,保留該數值整數位,得到數值N1;對Sec-WebSocket-Key2如法炮制,得到第二個整數N2;把N1和N2按照Big-Endian字符序列連接起來,然后再與另外一個Key3連接,得到一個原始序列ser_key。那么Key3是什么呢?大家可以看到在Safari發送過來的握手請求最后,有一個8字節的奇怪的字符串“;”######”,這個就是Key3。回到ser_key,對這個原始序列做md5算出一個16字節長的digest,這就是老版本協議需要的token,然后將這個token附在握手消息的最后發送回Client,即可完成握手。
新版協議生成Token的方法比較簡單:首先把Sec-WebSocket-Key和一串固定的UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”做拼接,然后對這個拼接后的字符串做SHA1加密,得到digest以后,做一次base64編碼,即可獲得Token。
另外需要注意的是,新版本和老版本握手協議回傳給Client的數據結構有所不同,在附件中的server源碼中寫得很清楚了,看看就能明白。
完成握手只是WebSocket Server的一半功能,現在只能保證這個Server能夠和兩個版本的瀏覽器建立鏈接了,但是如果試著把Chrome中的消息發送給Safari,會發現Safari無法接收。導致這個結果的原因,是因為兩個版本的協議的Data Framing結構不同,也即是在握手建立連接后,Client發送和接收的數據結構都不一樣。
首先第一步需要獲取不同版本協議下Client發送過來的原始數據。老版本協議比較簡單,實際上就是在原始數據前加了個'/x00′,在最后面加上了一個'/xFF',所以如果Safari的Client發送一個字符串'test',實際上WebSocket Server收到的數據是:'x00test/xFF',所以只需要剝離掉首尾那兩個字符就可以了。
比較麻煩的是新版本協議的數據,按照新版draft的解釋,Chrome和Firefox發過來的數據報文由以下幾個部分組成:首先是一個固定的字節(1000 0001或是1000 0002),這個字節可以不用理會。麻煩的是第二個字節,這里假設第二個字節是1011 1100,首先這個字節的第一位肯定是1,表示這是一個”masked”位,剩下的7個0/1位能夠計算出一個數值,比如這里剩下的是 011 1100,計算出來就是60,這個值需要做如下判斷:
如果這個值介于0000 0000 和 0111 1101 (0 ~ 125) 之間,那么這個值就代表了實際數據的長度;如果這個數值剛好等于0111 1110 (126),那么接下來的2個字節才代表真實數據長度;如果這個數值剛好等于0111 1111 (127),那么接下來的8個字節代表數據長度。
有了這個判斷之后,能夠知道代表數據長度的字節在第幾位結束,比如我們舉得例子60,這個值介于0~125之間,所以第二個字節本身就代表了原始數據的長度了(60個字節),所以從第三個字節開始,我們能抓出4個字節來,這一串字節叫做 “masks” (掩碼?),掩碼之后的數據,就是實際的data…的兄弟了。說是兄弟,是因為這個數據是實際data根據掩碼做過一次位運算后得到的,獲得原始data的方法是,將兄弟數據的每一位x,和掩碼的第i%4位做xor運算,其中i是x在兄弟數據中的索引。看得眼花是吧,看看下面這個代碼片段也許就能明白了:
這樣生成的back_str,就能夠發送給使用新版協議的Chrome或是Firefox了。
至此,這個簡單的WebSocket Server就完成了,能夠同時兼容老版協議和新版協議的Socket連接,以及不同版本之間的數據傳輸。該Server的源碼請點擊這里下載,需要注意的是里面用到了twisted框架來跑TCP服務,代碼寫得不怎么樣,僅供大家參考。
新聞熱點
疑難解答
圖片精選