WebSocket
WebSocket协议是基于TCP的一种新的协议。WebSocket最初在HTML5规范中被引用为TCP连接,作为基于TCP的套接字API的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信。
原理
建立连接
客户端向服务器发送随机字符串(Sec-WebSocket-Key),服务器接收到随机字符串后在字符串后拼接魔法字符串(全球统一),然后对新字符串进行sha1加密后再进行base64加密,然后将加密后的字符串返回给客户端(Sec-WebSocket-Accept)。客户端使用同样的方式对随机字符串加工后和服务器发来的字符串就行对比,如果不相同则表示服务器不支持websocket协议,如果相同则建立连接。
数据解密
客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。
当接受到客户端发来的数据时对其进行解密,具体步骤为:获取数据第二个字节的后七位(payload len),判断payload len的值,如果 payload len <= 125,则再向后取4个字节获得masking key,剩下的就是数据了,但是这个数据还需要使用masking key进行位运算,最终得到的才是数据本身。如果payload len = 126,则再向后数2个字节,然后再取4个字节获得masking key,如果payload len = 127,则再向后数8个字节,然后再取4个字节获得masking key.
示例
服务端
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 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()
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)
conn, address = sock.accept()
# 等待用户连接
header_dict = get_headers(conn.recv(1024))
# 握手环节
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
# 魔法字符串
random_string = header_dict['Sec-WebSocket-Key']
# 获取随机字符串
value = random_string + magic_string
# 拼接魔法字符串
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
# 对魔法字符串进行加密
response = "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 = response %ac.decode('utf-8')
conn.send(response.encode('utf-8'))
# 将加密后字符串返回给客户端
while True:
# 循环接受数据
data = conn.recv(1024)
msg = get_data(data)
# 对字符串进行解密
print(msg)
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input type="button" value="建立连接" onclick="startConnect();">
<script>
var ws = null;
function startConnect() {
ws = new WebSocket('ws://127.0.0.1:8000')
// 创建wx对象
ws.send("test")
// 发送数据
}
</script>
</body>
</html>
channels
由于django和flask框架内部都是基于wsgi做的socket,默认都不支持websocket协议,只支持http协议。
channels是以django插件的形式存在,它不仅能处理http请求,还提供对websocket、MQTT等长连接支持。不仅如此,channels在保留了原生django的同步和易用的特性上还带来了异步处理方式(channels2.X版本),并且将django自带的认证系统以及session集成到模块中,扩展性非常强。
安装
pip3 install channels
一对一
服务器:
在settings中注册channels
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
'channels',
]
在settings中配置入口
ASGI_APPLICATION = "app1.routing.application"
创建routing
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from app1 import consumers
application = ProtocolTypeRouter({
'websocket': URLRouter([
url(r'^ws/$', consumers.ChatConsumer),
])
})
创建consumers
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
""" websocket连接到来时,自动执行 """
print('链接到来')
self.accept()
def websocket_receive(self, message):
""" websocket浏览器给发消息时,自动触发此方法 """
print('接收到消息', message)
self.send(text_data='已收到') # 发送数据
# self.close() # 关闭连接
def websocket_disconnect(self, message):
"""客户端主动断开连接了,自动触发此方法 """
print('客户端断开连接了')
raise StopConsumer() # 抛出指定异常
客户端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="form">
<input id="txt" type="text" placeholder="请输入文字">
<input id="btn" type="button" value="发送" onclick="sendMessage();">
</div>
<div id="content">
</div>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
var ws;
$(function () {
initWebSocket();
});
function initWebSocket() {
ws = new WebSocket("ws://127.0.0.1:8000/ws/");
ws.onopen = function(){
// 建立链接触法
alert('连接成功')
};
ws.onmessage = function (arg) {
// 接受服务器发送的消息
var tag = document.createElement('div');
tag.innerHTML = arg.data;
$('#content').append(tag);
};
ws.onclose = function () {
// 断开链接
ws.close();
}
}
function sendMessage() {
ws.send($('#txt').val());
}
</script>
</body>
</html>
多对一
服务器
在settings中配置
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
}
}
consumers:
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
# 导入async_to_sync
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
""" websocket连接到来时,自动执行 """
print('建立链接')
async_to_sync(self.channel_layer.group_add)('组名', self.channel_name)
self.accept()
def websocket_receive(self, message):
""" websocket浏览器给发消息时,自动触发此方法 """
print('接收到消息', message)
async_to_sync(self.channel_layer.group_send)('组名', {
'type': 'xxx.ooo',
'message': message['text']
})
def xxx_ooo(self, event):
message = event['message']
self.send(message)
def websocket_disconnect(self, message):
"""客户端主动断开连接了,自动触发此方法 """
print('客户端主动断开连接了')
async_to_sync(self.channel_layer.group_discard)('组名', self.channel_name)
raise StopConsumer()