WebSocket是一种在单个TCP连接上进行全双工通信的协议。它由IETF在2011年定为标准RFC 6455,并由RFC7936补充规范,同时WebSocket API也被W3C定为标准。
1、定义与原理
WebSocket是独立的、创建在TCP上的协议,它使用HTTP/1.1协议的101状态码进行握手。为了创建WebSocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
2、特点
- 双向通信:WebSocket支持客户端和服务器之间的实时双向通信,而传统的HTTP协议是单向请求-响应模式。
- 实时性强:由于协议是全双工的,服务器可以随时主动给客户端下发数据,相对于HTTP请求需要等待客户端发起请求服务端才能响应,WebSocket的延迟明显更少。
- 保持连接状态:WebSocket需要先创建连接,因此是一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 控制开销小:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。相对于HTTP请求每次都要携带完整的头部,WebSocket的开销显著减少了。
- 支持二进制:WebSocket定义了二进制帧,可以更轻松地处理二进制内容。
- 支持扩展:WebSocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议,如部分浏览器支持压缩等。
- 更好的压缩效果:相对于HTTP压缩,WebSocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
3.案列
(1)先创建一个django项目(cmd创建或者直接pychram点击创建)
这里我们需要添加channels插件
pip install channels==3.0
在创建一个app用来访问页面,顺便创建需要用到的文件
(2)settings.py (配置一下环境)
在apps里面注册一下
在添加以下配置
ASGI_APPLICATION = "websocket.asgi.application"//"websocket"是我的项目名#channels自带的存储
CHANNEL_LAYERS = {"default":{"BACKEND": "channels.layers.InMemoryChannelLayer",}
}
运行检查一下,配置成功的话会有ASGI/Channels的标识(不对的话就切换成3.0的channels版本)
(3)项目目录下 asgi.py
这里主要是用来区分是http协议还是websocket协议
import osfrom channels.routing import ProtocolTypeRouter,URLRouter
from django.core.asgi import get_asgi_application
from websocket import routingsos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'study_websocket.settings')#application = get_asgi_application()
application = ProtocolTypeRouter({"http": get_asgi_application(),"websocket":URLRouter(routings.websocket_urlpatterns),
})
(4) 项目目录routings.py
from django.urls import re_path
from app import consumerswebsocket_urlpatterns = [re_path(r'room/(?P<group>\w+)/$',consumers.ChatConsumer.as_asgi()),
]
(5)创建一个路由来访问聊天页面
url.py
from django.contrib import admin
from django.urls import path
from app import viewsurlpatterns = [path('admin/', admin.site.urls),path('',views.index),
]
views.py
from django.shortcuts import render# Create your views here.def index(request):num = request.GET.get('num')return render(request,"home.html",{"num":num})
tempaltes文件创建一个home.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><style>.message {height: 500px;border: 1px solid #dddd;width: 100%;}</style>
</head>
<body>
<div class="message" id="message"></div>
<div><input type="text" placeholder="请输入" id="txt"><input type="button" value="发送" onclick="sendMessage()"><input type="button" value="关闭" onclick="closeCount()">
</div>
</body>
<script>//创建连接socket = new WebSocket("ws://127.0.0.1:8000/room/{{ num }}/");//创建好连接之后自动触发(服务端执行self.accept())socket.onopen = function (event){let tag= document.createElement("div")tag.innerText = "[连接成功]";document.getElementById("message").appendChild(tag);}//当websocket接收到服务端发来的消息时,自动触发这个函数socket.onmessage = function (event){console.log(event.data)let tag= document.createElement("div")tag.innerText = event.data;document.getElementById("message").appendChild(tag);}function sendMessage(){let tag=document.getElementById("txt");socket.send(tag.value);}function closeCount(){socket.close()//关闭}
</script>
</html>
(6)consumers.py
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_syncclass ChatConsumer(WebsocketConsumer):def websocket_connect(self, message):#有客户端来向后台发送websocket连接请求时,自动触发#服务端允许客户端创建连接print("有人连接")self.accept()#获取群号group = self.scope['url_route']['kwargs'].get("group")#将这个客户端连接对象加入到内存#channel_layer.group_add这个方法默认是异步的所有需要async_to_sync转成同步async_to_sync(self.channel_layer.group_add)(group,self.channel_name)#浏览器基于websocket向后端发送数据,自动触发接收消息def websocket_receive(self, message):#获取群号group = self.scope['url_route']['kwargs'].get("group")#通知组内所有客户端,执行xx_oo方法,在此方法中可以自己定义任意功能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):print("客户端断开连接")group = self.scope['url_route']['kwargs'].get("group")#客户端与服务器断开连接时自动触发async_to_sync(self.channel_layer.group_discard)(group,self.channel_name)raise StopConsumer()
效果:
同一个num名下才能收到消息
这里只实现了核心功能,具体的样式和效果可以各自发挥,适应各自的场景中。