1.websocket相关
1.1为什么要用websocket
如果有需求要实现服务端向客户端主动推送消息时(比如聊天室,群聊室)有哪几种方案
- 轮训:让浏览器每隔两秒发送一次请求,缺点:有延时,请求太多网站压力大;
- 长轮训:客户端向服务端发送请求,服务端最多夯20秒,一旦有新的数据就立即返回断开请求
- websocket:客户端和服务端创建链接请求不断开,实现双向通道。(推荐)
WebSocket 是一种在 Web 应用程序中实现双向通信的协议。相较于传统的 HTTP 请求-响应模式,WebSocket 提供了全双工的通信方式,使得服务器和客户端之间能够实时地进行双向数据传输。
下面是一些 WebSocket 的应用场景:
- 实时聊天:WebSocket 可以用于实时的聊天应用,例如在线客服系统、实时消息推送等。通过 WebSocket 协议,可以实现高效、低延迟的消息传递,提供更好的用户体验。
- 多人协作:WebSocket 可以用于多人协作应用,例如实时的协同编辑器、白板工具等。多个用户可以同时编辑共享的文档或画布,实时地看到其他用户的修改。
- 实时数据展示:WebSocket 可以用于实时监控和数据展示应用,例如股票行情图、实时天气预报等。通过 WebSocket,可以实时地获取最新的数据,并将其实时展示给用户。
- 游戏开发:WebSocket 在游戏开发中也发挥着重要作用。它可以用于实时的游戏对战、多人游戏中的实时交互等。通过 WebSocket,可以实现游戏中玩家之间的实时通信和数据传输。
- 远程监控:WebSocket 可以用于远程监控和控制应用,例如远程桌面、远程设备管理等。通过 WebSocket,可以实现实时的屏幕共享、远程操作等功能。
我基于websocket+Django写了一个简单的群聊系统。有感兴趣的同学可以参考我下面提供的源码进行学习,有不懂的,欢迎各位打扰!
下载链接:https://pan.baidu.com/s/1wLhkcRV-3awFz9XMxfWegQ?pwd=yyds
提取码:yyds
1.2websocket介绍
websocket 就是web版的socket
原来的Web中:
-
http协议,无状态,短链接
- 需要客户端主动链接服务器
- 客户端再向服务端发送消息,服务端接收消息在响应返回数据
- 客户端收到数据
- 断开请求链接
-
https协议,是http+对数据进行加密
我们在开发过程中想要保留一些状态信息,基于Cookie来做
现在web支持:
- http/https协议,一次请求一次响应
- websocket协议,创建持久的连接请求不断开,基于这个连接可以进行收发数据,用于【服务端向客户端主动推送消息】
- web聊天室
- 实时的图表,柱状图,饼图,投票。
1.3websocket原理
-
http协议
- 连接
- 数据传输
- 断开链接
-
websocket协议,是建立在http协议之上
-
连接,客户端发起
-
握手(验证),客户端发送一个消息,服务端收到消息做一些特殊处理并返回。服务器端支持websocket协议。
-
客户端向服务端发送
GET /chatsocket HTTP/1.1 Host:127.0.0.1:8002 Connection:Upgrade Pragma:no-cache Cache-Control:no-cache Upgrade:websocket Origin:http://localhost:63342 Sec-WebSocket-Version:13 Sec-WebSocket-Key:mnwFxiOlctXFN/DeMt1Amg== Sec-WebSocket-Extensions:permessage-deflate;client_max_window_bits ... ...
-
服务器接收(重要)*
Sec-WebSocket-Key:mnwFxiOlctXFN/DeMt1Amg==与 magic string 进行拼接 magic string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" v1 = "mnwFxiOlctXFN/DeMt1Amg==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" v2 = hmacl(v1)加密1 v3 = base64(v2)加密2
-
如果服务端匹配成功返回客户端
HTTP/1.1 101 Switching protocols Upgrade:websocket Connection:Upgrade Sec-WebSocket-Accept:密文(v3)
-
-
收到数据(加密)
dfanfsdfmkdsgg;fnfsnfonaoiasdono;fsofnoskfaj
-
先获取两个字节8位。例如10001010
-
再获取第二个字节的后7位。例如0001010 ——》payload lens
- =127 ,两个字节8位, 其他字节(4字节masking key + 用户数据)
- =126 ,两个字节, 其他字节(4字节masking key + 用户数据)
- <=125 ,两个字节8位, 其他字节(4字节masking key + 用户数据)
-
获取masking Key 然后对数据进行解密
var DECODED = " "; for(var i = 0;i < DECODED.length;i++){ DECODED[i]= ENCODED[i]^ MASK[i%4]; }
-
-
断开连接
1.4Django + WebSocket
-
-
安装websocket 第三方包
Django默认不支持websocket,需要安装组件
pip3 install channels
-
配置
-
注册channels
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'channels' ]
-
在setting.py中添加asgi_application
ASGI_APPLICATION = "websocket.asgi.application"
-
在asgi.py文件中修改
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter,URLRouter from . import routing os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'websocketp.settings') # application = get_asgi_application() application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": URLRouter(routing.websocket_urlpatterns), })
-
在setting同级目录下创建websocket_urls.py
from django.urls import re_path from app01 import websocket_views websocket_urlpatterns = [ re_path() ]
-
在app01目录下创建websocket_views.py,编写处理websocket的业务逻辑
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): # 有客户端来向服务端发送websocket连接的请求时,自动触发 # 服务端允许和客户端创建连接 self.accept() def websocket_receive(self, message): # 浏览器基于websocket向服务端发送数据,自动触发接收客户端消息 print(message) self.send("你好呀你好") self.close() #表示服务器端主动断开连接 def websocket_disconnect(self, message): # 客户端与服务器断开连接时,自动触发 print("断开连接") raise StopConsumer()
-
-
在Django中:
- WSGI:在以前的学习Django,都是用WSGI
- ASGI:WSGI+异步编程+websocket
-
1.5聊天室
-
访问地址看到聊天室的页面,http请求
-
让客户端主动向服务端发起websocket连接,服务端接收连接后通过(握手)。
-
客户端,websocket。
socket = new WebSocket("ws://localhost:8000/room/123/");
-
服务端 握手并连接成功
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): print("有人来连接了....") # 有客户端来向服务端发送websocket连接的请求时,自动触发 # 服务端允许和客户端创建连接(握手) self.accept()
-
收发消息
-
收发消息(客户端的向服务端发消息)
-
客户端
<div> <input type="text" placeholder="请输入" id="txt"> <input type="button" value="发送" onclick="sendMessage()"> </div> <script> socket = new WebSocket("ws://localhost:8000/room/123/"); function sendMessage(){ let tag = document.getElementById("txt"); socket.send(tag.value); } </script>
-
服务端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): print("有人来连接了....") # 有客户端来向服务端发送websocket连接的请求时,自动触发 # 服务端允许和客户端创建连接(握手) self.accept() def websocket_receive(self, message): # 浏览器基于websocket向服务端发送数据,自动触发接收客户端消息 print("接收到消息了",message)
-
-
-
收发消息(服务端给客户端发消息)
-
服务端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): print("有人来连接了....") # 有客户端来向服务端发送websocket连接的请求时,自动触发 # 服务端允许和客户端创建连接(握手) self.accept() # 服务端给客户端发消息 self.send("来了呀客官") ### 服务端给客户端发消息
-
客户端
<script> socket = new WebSocket("ws://localhost:8000/room/123/"); socket.onmessage = function (event){ console.log(event.data) }
-
回调函数 onmessage :当websocket接收到服务端发来的消息时,自动会触发这个函数
socket.onmessage = function (event){}
-
回调函数onopen : 当创建好连接之后会自动触发,服务端执行self.accept()之后
socket.onopen = function (event){}
-
服务端主动断开连接时,这个方法会被自动触发
-
socket.onclose = function (event){}
-
<script> socket = new WebSocket("ws://localhost:8000/room/123/"); // 当创建好连接之后会自动触发,服务端执行self.accept()之后 socket.onopen = function (event){ let tag = document.createElement("div"); tag.innerText = "[连接成功]"; document.getElementById("message").appendChild(tag); } // 当websocket接收到服务端发来的消息时,自动会触发这个函数 socket.onmessage = function (event){ let tag = document.createElement("div"); tag.innerText = event.data; document.getElementById("message").appendChild(tag); } // 服务端主动断开连接时,这个方法会被自动触发 socket.onclose = function (event){ let tag = document.createElement("div"); tag.innerText = "[断开连接]"; document.getElementById("message").appendChild(tag); } function sendMessage(){ let tag = document.getElementById("txt"); socket.send(tag.value); } function closeConn(){ socket.close();// 向服务端发送断开连接的请求 } </script>
-
-
1.6 群聊1
-
群聊的简单实现
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer CONN_LIST = [] class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): print("有人来连接了....") # 有客户端来向服务端发送websocket连接的请求时,自动触发 # 服务端允许和客户端创建连接(握手) self.accept() CONN_LIST.append(self) # 服务端给客户端发消息 # self.send("来了呀客官") def websocket_receive(self, message): # 浏览器基于websocket向服务端发送数据,自动触发接收客户端消息 print("接收到消息了",message) text = message['text'] print("收到消息", text) # if text == '关闭': # # 想要服务端断开连接, 会给客户端发送断开连接的消息 # self.close() # return res = '{}SB'.format(text) for conn in CONN_LIST: conn.send(res) # self.send(res) # self.close() #表示服务器端主动断开连接 # 不管是客户端还是服务端要主动断开时都会执行这个方法 def websocket_disconnect(self, message): # 客户端与服务器断开连接时,自动触发 print("断开连接") CONN_LIST.remove(self) raise StopConsumer() # 服务端允许客户端断开连接
1.7 群聊2
基于channels中提供的channels layers 来实现,需要做以下配置
-
setting中配置。
方法一:将多个人的连接存储在当前服务器下的内存中
# 相当于我们自定义的 consumers.py下定义的CONN_LIST = [],意思是 将多个人的连接存储在当前服务器下的内存中 也可以将多个人的连接放在使用redis CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer" } }
方法二:也可以将多个人的连接放在使用redis
CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": ['ip', 'port'] } } }
-
实现方法:
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer from asgiref.sync import async_to_sync class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): # 接收这个客户端的连接 self.accept() # 将这个客户端的连接对象加入某个地方 (内存 or redis) # 群号 和别名 # 获取群号 group = self.scope['url_route']['kwargs'].get("group") async_to_sync(self.channel_layer.group_add)(group, self.channel_name) def websocket_receive(self, message): # 通知组内的所有客户端,执行 xx_oo方法 ,在此方法中自己可以去定义任意的功能 group = self.scope['url_route']['kwargs'].get("group") async_to_sync(self.channel_layer.group_send)(group, {"type": "xx.oo", "message": message}) def xx_oo(self, event): text = event['message']['text'] self.send(text) # 不管是客户端还是服务端要主动断开时都会执行这个方法 def websocket_disconnect(self, message): # 客户端与服务器断开连接时,自动触发 group = self.scope['url_route']['kwargs'].get("group") async_to_sync(self.channel_layer.group_discard)(group, self.channel_name) raise StopConsumer() # 服务端允许客户端断开连接
1.8 总结
- websocket是什么?协议
- django中实现websocket,要用到channels组件
- 单独连接的(一对一)的收发数据
- 多对多的(比如说群聊)实现 基于Channels_layers
2.websocket编程的代码实例
在 Python 中,有许多库可以用于实现 WebSocket 功能,其中比较常用的是 websockets
库。以下是使用 websockets
库在 Python 中实现一个简单的 WebSocket 服务器和客户端的示例代码:
WebSocket 服务器端代码:
import asyncio
import websockets
async def handle_websocket(websocket, path):
# 处理 WebSocket 连接
while True:
message = await websocket.recv()
print(f"Received message: {message}")
# 这里可以根据收到的消息进行相应的处理逻辑
response = f"Server received: {message}"
await websocket.send(response)
print(f"Sent response: {response}")
start_server = websockets.serve(handle_websocket, "localhost", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
WebSocket 客户端代码:
import asyncio
import websockets
async def connect_websocket():
async with websockets.connect("ws://localhost:8765") as websocket:
while True:
message = input("Enter a message to send: ")
await websocket.send(message)
print(f"Sent message: {message}")
response = await websocket.recv()
print(f"Received response: {response}")
asyncio.get_event_loop().run_until_complete(connect_websocket())
这个示例实现了一个简单的回显功能,当客户端发送消息给服务器时,服务器会将收到的消息原样返回给客户端。
你可以先运行 WebSocket 服务器端代码,在命令行中执行 python server.py
,然后再运行 WebSocket 客户端代码,执行 python client.py
。在客户端输入消息后,服务器会收到该消息并返回给客户端,客户端再将接收到的响应消息打印出来。