用 Python 实现一个简单的 WebSocket 服务器
最近在做的一个项目中需要使用到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:
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:
GET / HTTP/1.1
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:
GET / HTTP/1.1
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实现的是新版协议,因此只传输了一个”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,即可完成握手。