Python實現(xiàn)同時兼容老版和新版Socket協(xié)議的一個簡單WebSocket服務(wù)器
最近在做的一個項目中需要使用到HTML5中引入的WebSocket技術(shù),本來以為應(yīng)該很容易就能搞定,誰知道在真正上手開發(fā)了以后才發(fā)現(xiàn)有很多麻煩的地方,雖然我們是一個以前端開發(fā)和設(shè)計見長的團(tuán)隊,而且作為一個二手程序猿又長期不被待見,但是為了讓有同樣需求的朋友少走些彎路,我還是決定把實現(xiàn)方法貼在這個地方。
關(guān)于WebSocket的基本概念,維基百科上解釋的很清楚,而且網(wǎng)上也能搜出來一大把,這里就略過不表,直接進(jìn)入正題。
這次的問題首先有一個前提,就是得用Python來實現(xiàn)這個服務(wù)器,如果對具體語言沒有限制的話,推薦大家首選Node.js的一個第三方庫:Socket.IO,非常好用,10分鐘不打針不吃藥搞定WebSocket Server,而且用JS來寫后端,相信也能對上很多文藝開發(fā)者的胃口。
但是如果選擇用Python,google搜索的結(jié)果幾乎都不能用,最要命的問題是,WebSocket協(xié)議本身還是一個草案,所以不同瀏覽器支持的協(xié)議版本有所不同,Safari 5.1支持的是老版本協(xié)議Hybi-02,Chrome 15以及Firefox 8.0支持的是新版本協(xié)議Hybi-10,老版本協(xié)議和新版本協(xié)議在建立通信的握手方法還有數(shù)據(jù)傳輸?shù)母袷揭笊隙加兴煌?,?dǎo)致網(wǎng)上大多數(shù)實現(xiàn)方式只能適用于Safari瀏覽器,并且Safari和C&F瀏覽器之間無法互相通信。
首先第一步需要解釋的是新、舊版本W(wǎng)ebSocket協(xié)議的握手方式。我們先來看看三個不同瀏覽器發(fā)送的握手?jǐn)?shù)據(jù)的結(jié)構(gòu):
Chrome:
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: 127.0.0.1:1337
Sec-WebSocket-Origin: http://127.0.0.1:8000
Sec-WebSocket-Key: erWJbDVAlYnHvHNulgrW8Q==
Sec-WebSocket-Version: 8
Cookie: csrftoken=xxxxxx; sessionid=xxxxx
Firefox:
Host: 127.0.0.1:1337
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:8.0) Gecko/20100101 Firefox/8.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive, Upgrade
Sec-WebSocket-Version: 8
Sec-WebSocket-Origin: http://127.0.0.1:8000
Sec-WebSocket-Key: 1t3F81iAxNIZE2TxqWv+8A==
Cookie: xxx
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Safari:
Upgrade: WebSocket
Connection: Upgrade
Host: 127.0.0.1:1337
Origin: http://127.0.0.1:8000
Cookie: sessionid=xxxx; calView=day; dayCurrentDate=1314288000000
Sec-WebSocket-Key1: cV`p1* 42#7 ^9}_ 647 08{
Sec-WebSocket-Key2: O8 415 8x37R A8 4
;"######
可以看出,Chrome和Firefox實現(xiàn)的是新版協(xié)議,因此只傳輸了一個”Sec-WebSocket-Key”頭以供服務(wù)端生成握手Token,但是遵循老版本的Safari的數(shù)據(jù)中有兩個Key:”Sec-WebSocket-Key1″和”Sec-WebSocket-Key2″,因此服務(wù)端在生成握手Token的時候,需要做一次判斷。先來看使用老版本協(xié)議的Safari,Token生成算法如下:
取出Sec-WebSocket-Key1中的所有數(shù)字字符形成一個數(shù)值,這里是1427964708,然后除以Key1中的空格數(shù)目,這里好像是6個空格,得到一個數(shù)值,保留該數(shù)值整數(shù)位,得到數(shù)值N1;對Sec-WebSocket-Key2如法炮制,得到第二個整數(shù)N2;把N1和N2按照Big-Endian字符序列連接起來,然后再與另外一個Key3連接,得到一個原始序列ser_key。那么Key3是什么呢?大家可以看到在Safari發(fā)送過來的握手請求最后,有一個8字節(jié)的奇怪的字符串“;”######”,這個就是Key3?;氐絪er_key,對這個原始序列做md5算出一個16字節(jié)長的digest,這就是老版本協(xié)議需要的token,然后將這個token附在握手消息的最后發(fā)送回Client,即可完成握手。
新版協(xié)議生成Token的方法比較簡單:首先把Sec-WebSocket-Key和一串固定的UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”做拼接,然后對這個拼接后的字符串做SHA1加密,得到digest以后,做一次base64編碼,即可獲得Token。
另外需要注意的是,新版本和老版本握手協(xié)議回傳給Client的數(shù)據(jù)結(jié)構(gòu)有所不同,在附件中的server源碼中寫得很清楚了,看看就能明白。
完成握手只是WebSocket Server的一半功能,現(xiàn)在只能保證這個Server能夠和兩個版本的瀏覽器建立鏈接了,但是如果試著把Chrome中的消息發(fā)送給Safari,會發(fā)現(xiàn)Safari無法接收。導(dǎo)致這個結(jié)果的原因,是因為兩個版本的協(xié)議的Data Framing結(jié)構(gòu)不同,也即是在握手建立連接后,Client發(fā)送和接收的數(shù)據(jù)結(jié)構(gòu)都不一樣。
首先第一步需要獲取不同版本協(xié)議下Client發(fā)送過來的原始數(shù)據(jù)。老版本協(xié)議比較簡單,實際上就是在原始數(shù)據(jù)前加了個'\x00′,在最后面加上了一個'\xFF',所以如果Safari的Client發(fā)送一個字符串'test',實際上WebSocket Server收到的數(shù)據(jù)是:'x00test\xFF',所以只需要剝離掉首尾那兩個字符就可以了。
比較麻煩的是新版本協(xié)議的數(shù)據(jù),按照新版draft的解釋,Chrome和Firefox發(fā)過來的數(shù)據(jù)報文由以下幾個部分組成:首先是一個固定的字節(jié)(1000 0001或是1000 0002),這個字節(jié)可以不用理會。麻煩的是第二個字節(jié),這里假設(shè)第二個字節(jié)是1011 1100,首先這個字節(jié)的第一位肯定是1,表示這是一個”masked”位,剩下的7個0/1位能夠計算出一個數(shù)值,比如這里剩下的是 011 1100,計算出來就是60,這個值需要做如下判斷:
如果這個值介于0000 0000 和 0111 1101 (0 ~ 125) 之間,那么這個值就代表了實際數(shù)據(jù)的長度;如果這個數(shù)值剛好等于0111 1110 (126),那么接下來的2個字節(jié)才代表真實數(shù)據(jù)長度;如果這個數(shù)值剛好等于0111 1111 (127),那么接下來的8個字節(jié)代表數(shù)據(jù)長度。
有了這個判斷之后,能夠知道代表數(shù)據(jù)長度的字節(jié)在第幾位結(jié)束,比如我們舉得例子60,這個值介于0~125之間,所以第二個字節(jié)本身就代表了原始數(shù)據(jù)的長度了(60個字節(jié)),所以從第三個字節(jié)開始,我們能抓出4個字節(jié)來,這一串字節(jié)叫做 “masks” (掩碼?),掩碼之后的數(shù)據(jù),就是實際的data…的兄弟了。說是兄弟,是因為這個數(shù)據(jù)是實際data根據(jù)掩碼做過一次位運(yùn)算后得到的,獲得原始data的方法是,將兄弟數(shù)據(jù)的每一位x,和掩碼的第i%4位做xor運(yùn)算,其中i是x在兄弟數(shù)據(jù)中的索引。看得眼花是吧,看看下面這個代碼片段也許就能明白了:
def send_data(raw_str):
back_str = []
back_str.append('\x81')
data_length = len(raw_str)
if data_length < 125:
back_str.append(chr(data_length))
else:
back_str.append(chr(126))
back_str.append(chr(data_length >> 8))
back_str.append(chr(data_length & 0xFF))
back_str = "".join(back_str) + raw_str
這樣生成的back_str,就能夠發(fā)送給使用新版協(xié)議的Chrome或是Firefox了。
至此,這個簡單的WebSocket Server就完成了,能夠同時兼容老版協(xié)議和新版協(xié)議的Socket連接,以及不同版本之間的數(shù)據(jù)傳輸。該Server的源碼請點(diǎn)擊這里下載,需要注意的是里面用到了twisted框架來跑TCP服務(wù),代碼寫得不怎么樣,僅供大家參考。
- Python WebSocket長連接心跳與短連接的示例
- 用Python進(jìn)行websocket接口測試
- python開發(fā)實例之python使用Websocket庫開發(fā)簡單聊天工具實例詳解(python+Websocket+JS)
- Python Websocket服務(wù)端通信的使用示例
- python實現(xiàn)WebSocket服務(wù)端過程解析
- 詳解python websocket獲取實時數(shù)據(jù)的幾種常見鏈接方式
- python實現(xiàn)websocket的客戶端壓力測試
- Python如何爬取實時變化的WebSocket數(shù)據(jù)的方法
- python制作websocket服務(wù)器實例分享
- Python通過websocket與js客戶端通信示例分析
- python和websocket構(gòu)建實時日志跟蹤器的步驟
相關(guān)文章
Django Model層F,Q對象和聚合函數(shù)原理解析
這篇文章主要介紹了Django Model層F,Q對象和聚合函數(shù)原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11七個生態(tài)系統(tǒng)核心庫[python自學(xué)收藏]
無論你是想快速入手Python,還是想成為數(shù)據(jù)分析大神或者機(jī)器學(xué)習(xí)大佬,亦或者對Python代碼進(jìn)行優(yōu)化,本文的python庫都能為你提供一些幫助2021-08-08