Python实现同时兼容老版和新版Socket协议的一个简单WebSocket服务器


Posted in Python onJune 04, 2014

最近在做的一个项目中需要使用到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,即可完成握手。

新版协议生成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在兄弟数据中的索引。看得眼花是吧,看看下面这个代码片段也许就能明白了:

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,就能够发送给使用新版协议的Chrome或是Firefox了。

至此,这个简单的WebSocket Server就完成了,能够同时兼容老版协议和新版协议的Socket连接,以及不同版本之间的数据传输。该Server的源码请点击这里下载,需要注意的是里面用到了twisted框架来跑TCP服务,代码写得不怎么样,仅供大家参考。

Python 相关文章推荐
利用Python读取文件的四种不同方法比对
May 18 Python
Python2.7基于淘宝接口获取IP地址所在地理位置的方法【测试可用】
Jun 07 Python
Python实现可获取网易页面所有文本信息的网易网络爬虫功能示例
Jan 15 Python
python中数据爬虫requests库使用方法详解
Feb 11 Python
python如何使用unittest测试接口
Apr 04 Python
Python异步操作MySQL示例【使用aiomysql】
May 16 Python
Python搭建Spark分布式集群环境
Jul 05 Python
Python 中list ,set,dict的大规模查找效率对比详解
Oct 11 Python
django框架cookie和session用法实例详解
Dec 10 Python
解决Pytorch 加载训练好的模型 遇到的error问题
Jan 10 Python
Django多层嵌套ManyToMany字段ORM操作详解
May 19 Python
python两种注释用法的示例
Oct 09 Python
Python程序员鲜为人知但你应该知道的17个问题
Jun 04 #Python
Python和Ruby中each循环引用变量问题(一个隐秘BUG?)
Jun 04 #Python
python控制台英汉汉英电子词典
Apr 23 #Python
测试、预发布后用python检测网页是否有日常链接
Jun 03 #Python
Python中的CURL PycURL使用例子
Jun 01 #Python
Python实现多线程下载文件的代码实例
Jun 01 #Python
python使用在线API查询IP对应的地理位置信息实例
Jun 01 #Python
You might like
POSIX 风格和兼容 Perl 风格两种正则表达式主要函数的类比(preg_match, preg_replace, ereg, ereg_replace)
2010/10/12 PHP
PHP图像处理之imagecreate、imagedestroy函数介绍
2014/11/19 PHP
js获取input标签的输入值实现代码
2013/08/05 Javascript
基于JQuery的列表拖动排序实现代码
2013/10/01 Javascript
JavaScript新窗口与子窗口传值详解
2014/02/11 Javascript
js获取和设置属性的方法
2014/02/20 Javascript
Javascript 运动中Offset的bug解决方案
2014/12/24 Javascript
jquery+ajax实现跨域请求的方法
2015/01/20 Javascript
JavaScript中使用concat()方法拼接字符串的教程
2015/06/06 Javascript
第九章之路径分页标签与徽章组件
2016/04/25 Javascript
全面解析Javascript无限添加QQ好友原理
2016/06/15 Javascript
jQGrid动态填充select下拉框的选项值(动态填充)
2016/11/28 Javascript
JS实现改变HTML上文字颜色和内容的方法
2016/12/30 Javascript
json对象及数组键值的深度大小写转换问题详解
2018/03/30 Javascript
js 使用ajax设置和获取自定义header信息的方法小结
2020/03/12 Javascript
[51:29]Alliance vs TNC 2019国际邀请赛小组赛 BO2 第二场 8.16
2019/08/18 DOTA
Python中的异常处理简明介绍
2015/04/13 Python
Python常用库推荐
2016/12/04 Python
python pandas库中DataFrame对行和列的操作实例讲解
2018/06/09 Python
使用pandas模块读取csv文件和excel表格,并用matplotlib画图的方法
2018/06/22 Python
Python实现重建二叉树的三种方法详解
2018/06/23 Python
详解Python中的分组函数groupby和itertools)
2018/07/11 Python
python2与python3中关于对NaN类型数据的判断和转换方法
2018/10/30 Python
python flask web服务实现更换默认端口和IP的方法
2019/07/26 Python
django admin 自定义替换change页面模板的方法
2019/08/23 Python
浅谈keras 模型用于预测时的注意事项
2020/06/27 Python
详解Canvas 实现炫丽的粒子运动效果(粒子生成文字)
2018/02/01 HTML / CSS
澳大利亚新奇小玩意网站:Yellow Octopus
2017/12/28 全球购物
用C#语言写出与SQLSERVER访问时的具体过程
2013/04/16 面试题
优秀英语专业毕业生求职信
2013/11/23 职场文书
社区敬老月活动总结
2015/05/07 职场文书
禁毒主题班会教案
2015/08/14 职场文书
祝福语集锦:送给闺蜜的生日祝福语
2019/10/08 职场文书
python实现会员信息管理系统(List)
2022/03/18 Python
SpringBoot全局异常处理方案分享
2022/05/25 Java/Android
Win11怎么添加用户?Win11添加用户账户的方法
2022/07/15 数码科技