最近在做的一个项目中需要使用到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

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

GET / HTTP/1.1

Host: 127.0.0.1:1337

User-Agent: Mozilla/5.0 (Macintosh; IntelMacOS 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:

Python

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

;"######

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收到的数据是:’x00testxFF’,所以只需要剥离掉首尾那两个字符就可以了。

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

Python

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

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标准库 对socket二次封装_Python实现同时兼容老版和新版Socket协议的一个简单WebSocket服...相关推荐

  1. pythonsocket自定义协议_Python实现同时兼容老版和新版Socket协议的一个简单WebSocket服务器...

    最近在做的一个项目中需要使用到HTML5中引入的WebSocket技术,本来以为应该很容易就能搞定,谁知道在真正上手开发了以后才发现有很多麻烦的地方,虽然我们是一个以前端开发和设计见长的团队,而且作为 ...

  2. python标准库random中函数的作用_Python随机函数库random的使用方法详解

    Python随机函数库random的使用方法详解 前言 众所周知,python拥有丰富的内置库,还支持众多的第三方库,被称为胶水语言,随机函数库random,就是python自带的标准库,他的用法极为 ...

  3. python标准库os中用来列出指定_Python 标准库 os 中用来列出指定文件夹中的文件和子文件夹列表的方式是listdir()。_高职高专数字资源平台答案_学小易找答案...

    [单选题]以下哪种不是不饱和脂肪酸( ) (2.0分) [名词解释]酶的活性中心 [简答题]6.举例说明酶的结构和功能之间的相互关系. [单选题]下列( )是脂肪酸 (2.0分) [简答题]7.试述维 ...

  4. python标准库(二)

    格式化输出 reprlib 库用来格式化 >>> import reprlib >>> reprlib.repr(set('aabbccddeeeff')) &qu ...

  5. Python标准库asyncio模块基本原理浅析

    Python标准库asyncio模块基本原理浅析 本文环境python3.7.0 asyncio模块的实现思路 当前编程语言都开始在语言层面上,开始简化对异步程序的编程过程,其中Python中也开始了 ...

  6. python第三方库排行-140种Python标准库、第三方库和外部工具

    导读:Python数据工具箱涵盖从数据源到数据可视化的完整流程中涉及到的常用库.函数和外部工具.其中既有Python内置函数和标准库,又有第三方库和工具. 这些库可用于文件读写.网络抓取和解析.数据连 ...

  7. 140种Python标准库、第三方库和外部工具都有了

    导读:Python数据工具箱涵盖从数据源到数据可视化的完整流程中涉及到的常用库.函数和外部工具.其中既有Python内置函数和标准库,又有第三方库和工具. 这些库可用于文件读写.网络抓取和解析.数据连 ...

  8. cassandra可视化工具_耗时1个月整理!160种Python标准库、第三方库和外部工具都有了...

    耗时1个月整理!160种Python标准库.第三方库和外部工具都有了 北京尚学堂 2019-12-09 14:59:15 Python数据工具箱涵盖从数据源到数据可视化的完整流程中涉及到的常用库.函数 ...

  9. Python标准库socketserver实现UDP协议时间服务器

    Python标准库socket提供了套接字编程所需要的绝大部分功能,更多案例详见文末相关阅读. 很久之前推送过一个使用标准库socket实现UDP协议时间服务器的代码,参考 Python使用UDP协议 ...

最新文章

  1. 【LeetCode从零单排】No.9 Palindrome Number
  2. hdu1174(3维射线与圆是否相交)
  3. 浅拷贝 python_python中什么是浅拷贝
  4. java源程序加密解决方案(基于Classloader解密)
  5. Hybris开发环境的license计算实现
  6. 【转】图文详解YUV420数据格式
  7. 智能互联网之数据存储实践
  8. 输入法快捷键导致功能软件功能失效
  9. 在Word文档里如何快速返回目录页-Office学习
  10. 可以批量制作吊牌标签上的条码标签打印软件
  11. 装备仿真模拟推演训练系统软件
  12. pytorch distiller filter channel剪枝
  13. vue两个数组如何判断值是否相同_vue两个数组如何判断重复的数据?
  14. 从零开始,用5年时间,攒够100w,如何够到800w 上海房
  15. 【ssl】为什么域名SSL证书的价格这么高?有哪些成本?
  16. 成功解决:[object Object]
  17. LTE学习-RACH(2)
  18. Java实现图片(Image)转字符(ASCII)图片
  19. 字节跳动宣布以每股155美元回购员工期权 较上一轮上涨9%
  20. 计算机账务处理流程有,了解账务处理系目标 理解账务处理系统业务流程 理解账务.ppt...

热门文章

  1. 图像处理之基础---图像高效不失真缩放既卷积应用
  2. PHP(ThinkPHP5.0) + PHPMailer 进行邮箱发送验证码
  3. VK Cup 2012 Round 1 D. Distance in Tree (树形dp)
  4. 纵览神经架构搜索方法
  5. 从word得到表格数据插入数据库(6位行业代码)
  6. mini 打开窗口提交表单,按钮在页脚
  7. 简单的PL/SQl链接远程ORACLE数据库方法
  8. Redis Sentinel机制与用法说明【转】
  9. Nancy跨平台开发总结(六)三层架构之Token认证的Rest API
  10. 大数据之-Hadoop3.x_Yarn_生产环境核心参数配置案例---大数据之hadoop3.x工作笔记0150