websocket
http是一种无状态的连接,并且它只能由客户端发起请求,然后服务端返回响应,服务端无法自动向客户端推送消息,最常见的就是早期实现的web聊天功能,在没有web的时候,通常通过长轮询的方式处理。
长轮询:客户端发送ajax请求尝试获取消息,服务器为每个客户端维护一个消息队列,而客户端请求从这个队列中取出消息,如果队列没有数据,将会再次阻塞一段时间,经过一段时间,或者获得了数据,将会直接返回,将数据返回给客户端,然后再发送一个请求继续尝试获取消息。
长轮询的效率低,浪费资源, 需要定时不断的发送http请求,并占用http连接,而websocket可以方便的完成服务器向客户端推送消息功能,实现上面的功能。
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
websocket连接实现原理
握手
客户端生成一个随机的字符串,放在http请求头中发送到服务器端,服务器获得全球公认的magic-string字符串,将两个字符串进行拼接,然后使用公认的算法(sha1 + base64)对这个字符串进行加密,将加密后的内容返回到客户端,客户端同样获取magic-string进行相同的加密,如果两个密文相同,表示服务器支持websocket协议。服务器和客户端可以使用websocket协议进行通信。
数据通信
建立连接后,双发可以通过websocket协议发送和接受消息,而这些消息都是进过加密发送的,且加密和解密方法全球公认。
数据解密步骤:
1. 取数据的第二个字节的后7位,该十进制值可能为127,126, <125三种情况
- 值为127,从数据的第11个字节开始读取
- 值为126,从数据的第4个字节开始读取
- 值<125,从数据的第三个字节开始读取
2. 这样读取的内容除去了前面的头信息,是我们需要的消息体,但是还需要经过位运进行解密。消息体的数据前四个字节位mask,该值将与之后的每个字节进行指定的位运算,才能得到加密前的字节序列,从而decode得到可阅读字符串消息。
python实现websocket服务端
import socket import hashlib import base64 def get_headers(data): """解析这个data的头信息,将header解析为一个字典""" 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 get_data(info): 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() # 使用bytesarray保存每个字节的数据,最后将这个字节序列进行decode得到数据 for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding=‘utf-8‘) return body 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) # 等待客户端的连接,获取客户端socket对象, conn, address = sock.accept() # 客户端连接后,将会自动发送随机字符串,服务器获取客户端发送的消息,然后从头信息中解析到这个随机字符串 data = conn.recv(1024) header_dict = get_headers(data) client_random_string = header_dict[‘Sec-WebSocket-Key‘] # 该字符串在头信息中的header的key为 Sec-WebSocket-Key
magic_string = ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11‘
value = client_random_string + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode(‘utf-8‘)).digest()) # 加密后字符串 ac
# 返回的头信息,将生成的密文放在 Sec-WebSocket-Accept 头中进行返回
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 = tpl %ac.decode(‘utf-8‘) # 将随机字符串给浏览器返回回去
conn.send(bytes(response_str, encoding=‘utf-8‘))
while True:
data = conn.recv(1024) # 不断的接受客户端的消息,并使用get_data函数对消息 进行解密
value = get_data(data)
print(value)
在浏览器的客户端,只要使用相同的方式对数据进行加解密即可实现客户端与服务端的交互。而目前的浏览器已经基本实现websocket协议,进行通信只需要使用WebSocket 对象构造函数,新建 WebSocket 实例即可。
// 构造一个websocket实例,
var ws = new WebSocket(‘ws://localhost:8080‘)
ws.readyState // 查看其连接状态
实例化 new WebSocket(‘ws://localhost:8080‘) 后将会自动发送握手消息到服务端进行连接,通过 ws.readyState 可以查看握手状态,共有四种
- CONNECTING:值为0,表示正在连接。
- OPEN:值为1,表示连接成功,可以通信了。
- CLOSING:值为2,表示连接正在关闭。
- CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
该对象常用api
// 用于指定连接成功后的回调函数 ws.onopen = function () { ws.send(‘Hello Server!‘); } // 指定多个回调函数 ws.addEventListener(‘open‘, function (event) { ws.send(‘Hello Server!‘); }); // 指定连接关闭后的回调函数 ws.onclose = function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event }; ws.addEventListener("close", function(event) { var code = event.code; var reason = event.reason; var wasClean = event.wasClean; // handle close event }); //用于指定收到服务器数据后的回调函数 ws.onmessage = function(event) { var data = event.data; // 处理数据, 数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象) if(typeof event.data === String) { console.log("Received data string"); } if(event.data instanceof ArrayBuffer){ var buffer = event.data; console.log("Received arraybuffer"); } }; // 用于向服务器发送数据 // 发送文本 ws.send(‘your message‘); // 发送二进制数据 var file = document .querySelector(‘input[type="file"]‘) .files[0]; ws.send(file);
channel组件
django默认没有实现websocket,需要使用channels模块,这里需要安装channel 2.3版本,pip3 install channels==2.3 并建议使用python3.6的环境,在channels的内部已经帮助我们实现了握手/加密/解密等所有环节,我们只需要简单实现数据的收发即可。
集成channels
1. 在settings的app列表中注册chennel的app,并指定Asgi应用路径
INSTALLED_APPS = [ ‘django.contrib.admin‘, ‘django.contrib.auth‘, ‘django.contrib.contenttypes‘, ‘django.contrib.sessions‘, ‘django.contrib.messages‘, ‘django.contrib.staticfiles‘, # 项目中要使用channels做websocket了. "channels", ] ASGI_APPLICATION = "channel_demo.routing.application" # 指向一个模块中的变量
2. 实现websocket协议
from channels.routing import ProtocolTypeRouter, URLRouter from django.conf.urls import url from app import consumers application = ProtocolTypeRouter({ # 使用websockket协议通信时候的url路径映射 ‘websocket‘: URLRouter([ url(r‘^chat/$‘, consumers.ChatConsumer), # 这里交给app.consumer中的一个类处理该请求 ]) })
3. 请求的处理
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): """客户端发来连接请求之后就会被触发""" # 服务端接收连接,向客户端浏览器发送一个加密字符串 self.accept() # 连接成功 CONSUMER_OBJECT_LIST.append(self) def websocket_receive(self, message): """浏览器向服务端发送消息,此方法自动触发, :param message: {"type": , "text":"客户端发送的数据"} """ # 服务端给客户端回一条消息 self.send(text_data=message[‘text‘]) def websocket_disconnect(self, message): """客户端主动断开连接""" # 服务端断开连接 raise StopConsumer()