网络编程
位置:首页>> 网络编程>> Python编程>> Python实现同时兼容老版和新版Socket协议的一个简单WebSocket服务器

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

  发布时间:2023-05-21 10:05:28 

标签: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,即可完成握手。

新版协议生成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服务,代码写得不怎么样,仅供大家参考。

0
投稿

猜你喜欢

  • 目录1. 警告不是异常2. 警告能捕获吗3. 捕获警告方法一4. 捕获警告方法二5. 捕获警告方法三1. 警告不是异常你是不是经常在使用一些
  • 设计页面时,经常会从一个页面打开一个子窗口以供浏览者查看。通常,这种子窗口中的内容一经浏览者看过,对于浏览者而言就不再需要,而他们常常会忘记
  • 实现制作抽奖程序,需要认知到我们可以看到一般抽奖程序界面上是有很多按钮的,比如中奖区域,按键开始区域等等,所以我们先要设置界面,然后把这些按
  • 拼接字符串使用“+”运算符可完成对多个字符串的拼接,“+”运算符可以连接多个字符串并产生一个字符串对象。字符串不允许直接与其他类型数据拼接。
  • 简介mplcursors包也可以为matplotlib提供交互式的数据光标(弹出式注释框),它的灵感来源于mpldatacursor包,可以
  • 本文实例解析Python SqlAlchemy的使用方法,分享给大家供大家参考,具体内容如下1.初始化连接from sqlalchemy i
  • 利用空闲几天把《JavaScript权威指南》安静的读了一篇。真是一本好书呀!呵呵,这句话见的太多了。好在什么地方呢?听我慢慢道来。从开始接
  • 一.字典的基本方法1.新建字典1)、建立一个空的字典>>> dict1={} >>> dict2=dic
  • 一、命令格式:定义变量名称条件函数用“:”作为结尾下一级命令用空格键缩进,默认使用Tab键因为这样比较明显例如:yongHuMing = i
  • python版本为python3.51.要求1)输入用户名密码2)认证成功后显示欢迎信息3)输错三次后锁定2.需求分析1)用户信息存储在文件
  • 用下列代码即可:<%On error resume nextSet session=Creat
  • 本文实例讲述了PHP引用的调用方法。分享给大家供大家参考,具体如下:示例1:function test($arr){} echo test(
  • 北京邮电大学 张剑XML的局限性目前,许多Web网站的内容数据都存放在数据库或数据文件中。对于Web程序开发人员来说,如果要想把有用的信息从
  • 最近给卫生局做一个表格上传/可视化系统,算是小有成果。今天把项目中的文件拖拽上传模块分离出来,做了一个独立的小demo,并把相关代码打包上传
  •      NumPy提供了多种存取数组内容的文件操作函数。保存数组数据的文件可以是二进制格式或者文本格式。二进
  • python爬虫学习之定向爬取淘宝商品价格,供大家参考,具体内容如下import requestsimport redef getHTMLT
  • 一、设计说明设计这个自动化的目的是想要交替、重复地使用固定的几个分区(分区编号01~05)来保存数据,当最后一个分区就是快满的时候,我们会把
  • 写作思路1、简述实现原理2、部分代码解析3、位置同步解析(①上下两屏位置同步②编辑屏位置保持不变)效果图如下:版本1:这就是我们常见的预览窗
  • 本文实例讲述了Django框架模型简单介绍与使用。分享给大家供大家参考,具体如下:ORM介绍ORM Object relational ma
  • 前言:二分法也就是二分查找,它是一种效率较高的查找方法假如公司新来了一个人,叫张三,他是你们公司第47个人,过了一段时间后,有些人呢看张三不
手机版 网络编程 asp之家 www.aspxhome.com