python之WebSocket协议

1、WebSocket理论部分

一、websocket是什么html

Websocket是html5提出的一个协议规范,参考rfc6455。html5

websocket约定了一个通讯的规范,经过一个握手的机制,客户端(浏览器)和服务器(webserver)之间能创建一个相似tcp的链接,从而方便c-s之间的通讯。在websocket出现以前,web交互通常是基于http协议的短链接或者长链接。python

WebSocket是为解决客户端与服务端实时通讯而产生的技术。websocket协议本质上是一个基于tcp的协议,是先经过HTTP/HTTPS协议发起一条特殊的http请求进行握手后建立一个用于交换数据的TCP链接,此后服务端与客户端经过此TCP链接进行实时通讯。jquery

注意:此时再也不须要原HTTP协议的参与了。web

二、websocket的优势算法

之前web server实现推送技术或者即时通信,用的都是轮询(polling),在特色的时间间隔(好比1秒钟)由浏览器自动发出请求,将服务器的消息主动的拉回来,在这种状况下,咱们须要不断的向服务器发送请求,然而HTTP request 的header是很是长的,里面包含的数据可能只是一个很小的值,这样会占用不少的带宽和服务器资源。json

而最比较新的技术去作轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通讯,但依然须要发出请求(reuqest)。flask

WebSocket API最伟大之处在于服务器和客户端能够在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只须要要作一个握手的动做,在创建链接以后,服务器能够主动传送数据给客户端,客户端也能够随时向服务器发送数据。 此外,服务器与客户端之间交换的标头信息很小。后端

WebSocket并不限于以Ajax(或XHR)方式通讯,由于Ajax技术须要客户端发起请求,而WebSocket服务器和客户端能够彼此相互推送信息;数组

所以从服务器角度来讲,websocket有如下好处:

节省每次请求的header http的header通常有几十字节

Server Push 服务器能够主动传送数据给客户端

三、websocket的协议规范

3.1基于flash的握手协议

使用场景是IE的多数版本,由于IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,尚未原生的支持WebSocket。此处,server惟一要作的,就是准备一个WebSocket-Location域给client,没有加密,可靠性不好。

3.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.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 -建立连接以后默认不断开 

3.四、基于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)。 好比:

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)); } 若是加密算法错误,客户端在进行校检的时候会直接报错。若是握手成功,则客户端侧会出发onopen事件。 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-rsv一、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')) 

五、原理代码:

后端

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')) 

2、应用:

一、Flask中应用: pip3 install gevent-websocket

View Code

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() 

login.html

<!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> 

index.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> 

二、Django应用:channel

三、Tornado应用:本身有

 

 识别图中二维码,领取python全套视频资料

相关文章
相关标签/搜索