一、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有以下好处:

    1. 节省每次请求的header
      http的header一般有几十字节
    2. 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)。 比如:

      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等字段。其中要求:

  1. 客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。
  2. 服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。

针对上情况,发现错误的一方可向对方发送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>

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

index.html

2、Django应用:channel

3、Tornado应用:自己有

python之WebSocket协议的更多相关文章

  1. python测试基于websocket协议的即时通讯接口

    随着html5的广泛应用,基于websocket协议的即时通讯有了越来越多的使用场景,本文使用python中的websocket-client模块来做相关的接口测试 import webclient ...

  2. Websocket协议的学习、调研和实现

    本文章同时发在 cpper.info. 1. websocket是什么 Websocket是html5提出的一个协议规范,参考rfc6455. websocket约定了一个通信的规范,通过一个握手的机 ...

  3. WebSocket协议

    websocket 简介 (2013-04-09 15:39:28) 转载▼   分类: websocket 一 WebSocket是html5新增加的一种通信协议,目前流行的浏览器都支持这个协议,例 ...

  4. MQTT协议笔记之mqtt.io项目Websocket协议支持

    前言 MQTT协议专注于网络.资源受限环境,建立之初不曾考虑WEB环境,倒也正常.虽然如此,但不代表它不适合HTML5环境. HTML5 Websocket是建立在TCP基础上的双通道通信,和TCP通 ...

  5. python模拟websocket握手过程中计算sec-websocket-accept

    背景 以前,很多网站使用轮询实现推送技术.轮询是在特定的的时间间隔(比如1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给浏览器.轮询的缺点很明显,浏览器需要不断的向服 ...

  6. webSocket协议与Socket的区别

    WebSocket介绍与原理WebSocket protocol 是HTML5一种新的协议.它实现了浏览器与服务器全双工通信(full-duplex).一开始的握手需要借助HTTP请求完成. ——百度 ...

  7. python之websocket

    一.websocket WebSocket协议是基于TCP的一种新的协议.WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符.它实现了浏览器与服务器全双工 ...

  8. Python面试-websocket及web框架

    一.Websocket 1. websocket概念 在讲websocket之前,我们先来看看ajax轮询和long poll的实现机制. A.  ajax轮询 ajax轮询的原理非常简单,让浏览器隔 ...

  9. HTTPS请求HTTP接口被浏览器阻塞,python实现websocket客户端,websocket服务器,跨域问题,dwebsocket,https,拦截,服务端

    HTTPS请求HTTP接口被浏览器阻塞,python实现websocket客户端,websocket服务器,跨域问题,dwebsocket,https,拦截,服务端 发表时间:2020-03-05 1 ...

随机推荐

  1. OpenCV学习:实现简单的图像叠加

    本实例使用简单的线性叠加方法来实现两幅图像的叠加,主要使用的知识如下: 1)线性融合 2)addWeighted函数 //! computes weighted sum of two arrays ( ...

  2. OpenCV学习:图像的载入和显示

    一.使用IplImage结构读取并显示图像文件: 运行结果: 二.使用Mat类读取并显示图像文件: 使用 Mat 类,内存管理变得简单,不再像使用 IplImage 那样需要自己申请和释放内存,而且一 ...

  3. C/C++ 头文件以及库的搜索路径

    关键点: 1. #include <...> 不会搜索当前目录 2. 使用 -I 参数指定的头文件路径仅次于 搜索当前路径. 3. gcc -E -v 可以输出头文件路径搜索过程 C++编 ...

  4. oracle启动报ORA-03113;

    [案例] 在重启数据库过程中: SQL> startup ORACLE instance started. Total System Global Area 1.0489E+10 bytes F ...

  5. 解决IE中img.onload失效的方法

    解决IE中img.onload失效的方法 - CoffeeCat's IT Blog - IT博客 http://www.cnitblog.com/CoffeeCat/archive/2008/02/ ...

  6. JVM学习(一)

    JVM自身的物理结构:

  7. Centos安装Memcache

    Memcache概述 官方 Memcache是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据.简单的说就是将数据调用到内存中,然后从内存 ...

  8. 一次Win10安装体验

    我下载的是win10 Build 14279版本.http://www.iwin10.com/xiazai/1071.html 下载之后就直接拷到U盘安装了. 安装完之后发现(因为我是分区成了两个)我 ...

  9. 以用户名注册来分析三种Action获取数据的方式

    1.注入属性 直接注入属性: public String userName; public String getUserName() { return userName; } public void ...

  10. go练习5--生成md5

    import "crypto/md5" import "encoding/hex" //go 生成 md5 func T4_1() { m := md5.New ...