python之WebSocket协议
一、WebSocket理论部分
1、websocket是什么
Websocket是html5提出的一个协议规范,参考rfc6455。
websocket约定了一个通信的规范,通过一个握手的机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似tcp的连接,从而方便c-s之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。
WebSocket是为解决客户端与服务端实时通信而产生的技术。websocket协议本质上是一个基于tcp的协议,是先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。
注意:此时不再需要原HTTP协议的参与了。
2、websocket的优点
以前web server实现推送技术或者即时通讯,用的都是轮询(polling),在特点的时间间隔(比如1秒钟)由浏览器自动发出请求,将服务器的消息主动的拉回来,在这种情况下,我们需要不断的向服务器发送请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源。
而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求(reuqest)。
WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。 此外,服务器与客户端之间交换的标头信息很小。
WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;
因此从服务器角度来说,websocket有以下好处:
- 节省每次请求的header
http的header一般有几十字节 - Server Push
服务器可以主动传送数据给客户端
3、websocket的协议规范
1.基于flash的握手协议
使用场景是IE的多数版本,因为IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,还没有原生的支持WebSocket。此处,server唯一要做的,就是准备一个WebSocket-Location域给client,没有加密,可靠性很差。
2.基于md5加密方式的握手协议
其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 这几个头信息是web server用来生成应答信息的来源,依据 draft-hixie-thewebsocketprotocol-76 草案的定义。
web server基于以下的算法来产生正确的应答信息:
1. 逐个字符读取 Sec-WebSocket-Key1 头信息中的值,将数值型字符连接到一起放到一个临时字符串里,同时统计所有空格的数量;
2. 将在第(1)步里生成的数字字符串转换成一个整型数字,然后除以第(1)步里统计出来的空格数量,将得到的浮点数转换成整数型;
3. 将第(2)步里生成的整型值转换为符合网络传输的网络字节数组;
4. 对 Sec-WebSocket-Key2 头信息同样进行第(1)到第(3)步的操作,得到另外一个网络字节数组;
5. 将 [8-byte security key] 和在第(3)、(4)步里生成的网络字节数组合并成一个16字节的数组;
6. 对第(5)步生成的字节数组使用MD5算法生成一个哈希值,这个哈希值就作为安全密钥返回给客户端,以表明服务器端获取了客户端的请求,同意创建websocket连接
3.基于sha加密方式的握手协议
也是目前见的最多的一种方式,这里的版本号目前是需要13以上的版本。
客户端请求:
GET /ls HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: www.qixing318.com
Sec-WebSocket-Origin: http://www.qixing318.com
Sec-WebSocket-Key: 2SCVXUeP9cTjV+0mWB8J6A==
Sec-WebSocket-Version: 13
服务器返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket Connection:
Upgrade Sec-WebSocket-Accept: mLDKNeBNWz6T9SxU+o0Fy/HgeSw=
其中 server就是把客户端上报的key拼上一段GUID( “258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿这个字符串做SHA-1 hash计算,然后再把得到的结果通过base64加密,最后再返回给客户端。
-格式:\r\n
-创建链接之后默认不断开
4、基于sha加密的Opening Handshake(握手环节)
客户端发起连接Handshake请求
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
服务器端响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
- Upgrade:WebSocket
表示这是一个特殊的 HTTP 请求,请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议。 - Sec-WebSocket-Key
是一段浏览器base64加密的密钥,server端收到后需要提取Sec-WebSocket-Key 信息,然后加密。 -
Sec-WebSocket-Accept
服务器端在接收到的Sec-WebSocket-Key密钥后追加一段神奇字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,并将结果进行sha-1哈希,然后再进行base64加密返回给客户端(就是Sec-WebSocket-Key)。 比如:
如果加密算法错误,客户端在进行校检的时候会直接报错。如果握手成功,则客户端侧会出发onopen事件。function encry($req) { $key = $this->getKey($req); $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; # 将 SHA-1 加密后的字符串再进行一次 base64 加密 return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); }
- Sec-WebSocket-Protocol
表示客户端请求提供的可供选择的子协议,及服务器端选中的支持的子协议,“Origin”服务器端用于区分未授权的websocket浏览器 - Sec-WebSocket-Version: 13
客户端在握手时的请求中携带,这样的版本标识,表示这个是一个升级版本,现在的浏览器都是使用的这个版本。 -
HTTP/1.1 101 Switching Protocols
101为服务器返回的状态码,所有非101的状态码都表示handshake并未完成。
Data Framing
Websocket协议通过序列化的数据帧传输数据。数据封包协议中定义了opcode、payload length、Payload data等字段。其中要求:
- 客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。
- 服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。
针对上情况,发现错误的一方可向对方发送close帧(状态码是1002,表示协议错误),以关闭连接。
具体数据帧格式如下图所示:
- FIN
标识是否为此消息的最后一个数据包,占 1 bit - RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit
- Opcode
数据包类型(frame type),占4bits
0x0:标识一个中间数据包
0x1:标识一个text类型数据包
0x2:标识一个binary类型数据包
0x3-7:保留
0x8:标识一个断开连接类型数据包
0x9:标识一个ping类型数据包
0xA:表示一个pong类型数据包
0xB-F:保留 - MASK:占1bits
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。 - Payload length
Payload data的长度,占7bits,7+16bits,7+64bits:- 如果其值在0-125,则是payload的真实长度。
- 如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
- 如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
这里的长度表示遵循一个原则,用最少的字节表示长度(尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。
-
Payload data
应用层数据server解析client端的数据
接收到客户端数据后的解析规则如下:
- 1byte
- 1bit: frame-fin,x0表示该message后续还有frame;x1表示是message的最后一个frame
- 3bit: 分别是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
- 4bit: frame-opcode,x0表示是延续frame;x1表示文本frame;x2表示二进制frame;x3-7保留给非控制frame;x8表示关 闭连接;x9表示ping;xA表示pong;xB-F保留给控制frame
- 2byte
- 1bit: Mask,1表示该frame包含掩码;0表示无掩码
- 7bit、7bit+2byte、7bit+8byte: 7bit取整数值,若在0-125之间,则是负载数据长度;若是126表示,后两个byte取无符号16位整数值,是负载长度;127表示后8个 byte,取64位无符号整数值,是负载长度
- 3-6byte: 这里假定负载长度在0-125之间,并且Mask为1,则这4个byte是掩码
- 7-end byte: 长度是上面取出的负载长度,包括扩展数据和应用数据两部分,通常没有扩展数据;若Mask为1,则此数据需要解码,解码规则为- 1-4byte掩码循环和数据byte做异或操作。
示例代码:
while True:
# 对数据进行解密
# send_msg(conn, bytes('alex', encoding='utf-8'))
# send_msg(conn, bytes('SB', encoding='utf-8'))
# info = conn.recv(8096)
# print(info)
info = conn.recv(8096)
payload_len = info[1] & 127
if payload_len == 126:
extend_payload_len = info[2:4]
mask = info[4:8]
decoded = info[8:]
elif payload_len == 127:
extend_payload_len = info[2:10]
mask = info[10:14]
decoded = info[14:]
else:
extend_payload_len = None
mask = info[2:6]
decoded = info[6:]
bytes_list = bytearray()
for i in range(len(decoded)):
chunk = decoded[i] ^ mask[i % 4]
bytes_list.append(chunk)
msg = str(bytes_list, encoding='utf-8')
rep = msg + 'sb'
send_msg(conn,bytes(rep,encoding='utf-8'))
5、原理代码:
import socket import hashlib import base64 def get_headers(data): """ 将请求头格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') header, body = data.split('\r\n\r\n', 1) header_list = header.split('\r\n') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(' ')) == 3: header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ') else: k, v = header_list[i].split(':', 1) header_dict[k] = v.strip() return header_dict def send_msg(conn, msg_bytes): """ WebSocket服务端向客户端发送消息 :param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept() :param msg_bytes: 向客户端发送的字节 :return: """ import struct token = b"\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8002)) sock.listen(5) # 等待用户连接 conn, address = sock.accept() # WebSocket发来的连接 # 1. 获取握手数据 data = conn.recv(1024) headers = get_headers(data) # 2. 对握手信息进行加密: magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) # 3. 返回握手信息 response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://127.0.0.1:8002\r\n\r\n" response_str = response_tpl % (ac.decode('utf-8'),) conn.sendall(bytes(response_str, encoding='utf-8')) # 之后,才能进行首发数据。 while True: # 对数据进行解密 # send_msg(conn, bytes('alex', encoding='utf-8')) # send_msg(conn, bytes('SB', encoding='utf-8')) # info = conn.recv(8096) # print(info) info = conn.recv(8096) payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) msg = str(bytes_list, encoding='utf-8') rep = msg + 'sb' send_msg(conn,bytes(rep,encoding='utf-8'))
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>WebSocket协议学习</h1> <script type="text/javascript"> // 向 127.0.0.1:8002 发送一个WebSocket请求 var socket = new WebSocket("ws://127.0.0.1:8002"); socket.onmessage = function (event) { /* 服务器端向客户端发送数据时,自动执行 */ var response = event.data; console.log(response); }; </script> </body> </html>
二、应用:
1、Flask中应用: pip3 install gevent-websocket
from flask import Flask,request,render_template,session,redirect
import uuid
import json
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
app = Flask(__name__)
app.secret_key = 'asdfasdf'
GENTIEMAN = {
'1':{'name':'钢弹','count':0},
'2':{'name':'铁锤','count':0},
'3':{'name':'闫帅','count':0},
}
WEBSOCKET_DICT = {
}
@app.before_request
def before_request():
if request.path == '/login':
return None
user_info = session.get('user_info')
if user_info:
return None
return redirect('/login')
@app.route('/login',methods=['GET','POST'])
def login():
if request.method == "GET":
return render_template('login.html')
else:
uid = str(uuid.uuid4())
session['user_info'] = {'id':uid,'name':request.form.get('user')}
return redirect('/index')
@app.route('/index')
def index():
return render_template('index.html',users=GENTIEMAN)
@app.route('/message')
def message():
# 1. 判断到底是否是websocket请求?
ws = request.environ.get('wsgi.websocket')
if not ws:
return "请使用WebSocket协议"
# ----- ws连接成功 -------
current_user_id = session['user_info']['id']
WEBSOCKET_DICT[current_user_id] = ws
while True:
# 2. 等待用户发送消息,并接受
message = ws.receive() # 帅哥ID
# 关闭:message=None
if not message:
del WEBSOCKET_DICT[current_user_id]
break
# 3. 获取用户要投票的帅哥ID,并+1
old = GENTIEMAN[message]['count']
new = old + 1
GENTIEMAN[message]['count'] = new
data = {'user_id': message, 'count': new,'type':'vote'}
# 4. 给所有客户端推送消息
for conn in WEBSOCKET_DICT.values():
conn.send(json.dumps(data))
return 'close'
@app.route('/notify')
def notify():
data = {'data': "你的订单已经生成,请及时处理;", 'type': 'alert'}
print(WEBSOCKET_DICT)
for conn in WEBSOCKET_DICT.values():
conn.send(json.dumps(data))
return '发送成功'
if __name__ == '__main__':
http_server = WSGIServer(('192.168.11.143', 5000), app, handler_class=WebSocketHandler)
http_server.serve_forever()
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post"> <input type="text" name="user"> <input type="submit" value="提交"> </form> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>投票系统:参与投票的人</h1> <ul> {% for k,v in users.items() %} <li id="user_{{k}}" ondblclick="vote('{{k}}')">{{v.name}} <span>{{v.count}}</span> </li> {% endfor %} </ul> <script src="{{ url_for('static',filename='jquery-3.3.1.min.js')}}"></script> <script> var socket = new WebSocket("ws://192.168.11.143:5000/message"); socket.onmessage = function (event) { /* 服务器端向客户端发送数据时,自动执行 */ var response = JSON.parse(event.data); // {'user':1,'count':new} if(response.type == 'vote'){ var nid = '#user_' + response.user_id; $(nid).find('span').text(response.count) }else{ alert(response.data); } }; /* 我要给某人投票 */ function vote(id) { socket.send(id); } </script> </body> </html>
2、Django应用:channel
3、Tornado应用:自己有