即时通讯技术简单的说就是实现这样一种功能:服务器端可以即时地将数据的更新或变化反应到客户端,例如消息即时推送等功能都是通过这种技术实现的。但是在Web中,由于浏览器的限制,实现即时通讯需要借助一些方法。这种限制出现的主要原因是,一般的Web通信都是浏览器先发送请求到服务器,服务器再进行响应完成数据的现实更新。
实现Web端即时通讯的方法:实现即时通讯主要有四种方式,它们分别是轮询、长轮询(comet)、长连接(SSE)、WebSocket
。它们大体可以分为两类,一种是在HTTP基础上实现的,包括短轮询、comet
和SSE
;另一种不是在HTTP基础上实现是,即WebSocket
。下面分别介绍一下这四种轮询方式,以及它们各自的优缺点。
轮询
这种方式的优点是比较简单,易于理解,实现起来也没有什么技术难点。缺点是显而易见的,这种方式由于需要不断的建立http
连接,严重浪费了服务器端和客户端的资源。尤其是在客户端,距离来说,如果有数量级想对比较大的人同时位于基于短轮询的应用中,那么每一个用户的客户端都会疯狂的向服务器端发送http
请求,而且不会间断。人数越多,服务器端压力越大,这是很不合理的。
因此短轮询不适用于那些同时在线用户数量比较大,并且很注重性能的Web应用。
轮询直白点说就是前端每隔固定时间向后台发送一次请求,询问服务器是否有新数据
缺点: 延迟,需要固定的轮询时间,不一定是实时数据;大量耗费服务器内存和宽带资源,因为不停的请求服务器,很多时候 并没有新的数据更新,因此绝大部分请求都是无效请求
长轮询(comet)
当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)才返回。 。 客户端JavaScript响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
长轮询和短轮询比起来,明显减少了很多不必要的http
请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。
轮询与长轮询都是基于HTTP的,两者本身存在着缺陷:轮询需要更快的处理速度;长轮询则更要求处理并发的能力;两者都是“被动型服务器”的体现:服务器不会主动推送信息,而是在客户端发送ajax
请求后进行返回的响应。而理想的模型是”在服务器端数据有了变化后,可以主动推送给客户端”,这种”主动型”服务器是解决这类问题的很好的方案。Web Sockets就是这样的方案。
长连接(SSE)
SSE是HTML5
新增的功能,全称为Server-Sent Events。它可以允许服务推送数据到客户端。SSE在本质上就与之前的长轮询、短轮询不同,虽然都是基于http
协议的,但是轮询需要客户端先发送请求。而SSE最大的特点就是不需要客户端发送请求,可以实现只要服务器端数据有更新,就可以马上发送到客户端。
SSE的优势很明显,它不需要建立或保持大量的客户端发往服务器端的请求,节约了很多资源,提升应用性能。并且后面会介绍道,SSE的实现非常简单,并且不需要依赖其他插件。
WebSocket
WebSocket
是Html5
定义的一个新协议,与传统的http
协议不同,该协议可以实现服务器与客户端之间全双工通信。简单来说,首先需要在客户端和服务器端建立起一个连接,这部分需要http
。连接一旦建立,客户端和服务器端就处于平等的地位,可以相互发送数据,不存在请求和响应的区别。
WebSocket
的优点是实现了双向通信,缺点是服务器端的逻辑非常复杂。现在针对不同的后台语言有不同的插件可以使用。
WebSocket
的模式就是在于当前端向后端发送请求创建一个websocket
链连接之后,连接默认不断开,前端和服务端就维护了一个连接,前端可以通过连接给服务端发消息,服务端也可以通过连接给前端发消息,实现了双向通信,也叫双工通道
websocket原理
websocket协议原理
1.WebSocket
协议是基于TCP的一种新的协议。WebSocket
最初在HTML5
规范中被引用为TCP连接,作为基于TCP的套接字API
的占位符。它实现了浏览器与服务器全双工(full-duplex)通信。其本质是保持TCP连接,在浏览器和服务端通过Socket进行通信
2.websocket
是建立在http
协议之上的
连接,客户端发起
握手(验证),客户端发送一个消息,后端接收到消息再做一些特殊处理并返回,服务端需要支持websocket协议
收发数据(加密)
断开连接
websocket握手流程
- 从请求【握手】信息中提取
Sec-WebSocket-Key
- 利用
magic_string
和 Sec-WebSocket-Key
进行hmac1
加密,再进行base64
加密 - 将加密结果响应给客户端
注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
扩展:
http协议:\r\n分割、请求头和请求体\r\n分割、无状态、短连接。
websocket协议:\r\n分割,创建连接后不断开、验证+数据加密;
1.客户端向服务的发送
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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 ... ...
\r\n\r\n
|
2.服务的接收返回
1 2 3 4 5 6 7 8 9 10
| // 1.Sec-WebSocket-Key 与 magic string 进行拼接 // magic string默认为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11 v1 = 'mnwFxiOlctXFN/DeMt1Amg==' + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
// 2.将拼接好的结果进行hmac1加密 v2 = hmac1(v1)
// 3. 将hmac1加密过的数据进行base64加密 v3 = base64(v2)
|
1 2 3 4 5
| // 服务端返回 HTTP/1.1 101 Switching Protocols Upgrade:websocket Connection: Upgrade Sec-WebSocket-Accept:v3 //将加密好的数据通过Sec-WebSocket-Accept返回给前端
|
收发数据(加密)
客户端和服务端传输数据时,以字节的形式进行传输需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。
流程:
1.先获取第二个字节,1个字节是8位
2.再获取第二个字节的后7位 - > payload len
payload len== 127
,向后读取8个字节,然后是其他字节(取4个字节拿到masking key 再去读后面的数据进行解密)
payload len== 126
,向后读取2个字节,再去读取2个字节,然后是其他字节 (取4个字节拿到masking key 再去读后面的数据进行解密)
payload len<= 125
, 向后读取2个字节,然后是其他字节 (取4个字节拿到masking key 再去读后面的数据进行解密)
3.获取masking key 进行解密
1 2 3 4 5 6
| // 解密算法
var DECODED = ""; for (var i = 0; i < ENCODED.length; i++) { DECODED[i] = ENCODED[i] ^ MASK[i % 4]; }
|
几种Web即时通信技术比较
性能、兼容性对比
从兼容性角度考虑:短轮询>长轮询>长连接SSE>WebSocket
从性能方面考虑:WebSocket
>长连接SSE>长轮询>短轮询
实现对比
对比条件 | 短轮询 | 长轮询 | Websocket | sse |
---|
通讯方式 | http | http | 基于TCP长连接通讯 | http |
触发方式 | 轮询 | 轮询 | 事件 | 事件 |
优点 | 兼容性好容错性强,实现简单 | 相比短轮询大大降低了http 请求次数,节约资源 | 全双工通讯协议,性能开销小、安全性高,有一定可扩展性 | 实现简便,开发成本低 |
缺点 | 安全性差,占较多的内存资源与请求数 | 安全性差,占较多的内存资源与请求数 | 传输数据需要进行二次解析,增加开发成本及难度 | 只适用高级浏览器 |
适用范围 | b/s服务 | b/s服务 | 网络游戏、银行交互和支付 | 服务端到客户端单向推送 |
短轮询、长轮询、sse
实现
![]()
代码实现
轮询
前端每隔固定时间向后台发送一次请求,询问服务器是否有新数据
轮询的工作流程
前端发送数据
后端接收处理数据
前端定时请求数据
后端返回新数据
发送-接收数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| // 前端 <div class="message" id="message">
<div> <input type="text" id="txt" placeholder="请输入"> {#发送按钮绑定点击事件#} <input type="button" value="发送" onclick="sendMessage();">
</div> {#jquery cdn #} <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script> <script>
function sendMessage() {
var text = $("#txt").val(); $.ajax({ url: '/pool/send/msg/', type: 'GET', data: { text: text }, success: function (res) { console.log('请求发送成功', res) }
}) } </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
DB = []
def views(request): return render(request, 'home.html')
def send_msg(request): print('接收到到前端请求', request.GET) text = request.GET.get('text') DB.append(text) return HttpResponse('ok')
|
定时请求、更新数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
max_index = 0;
setInterval(function () { $.ajax( { url: '/pool/get/msg/', type: 'GET', data: { index: max_index }, success: function (dataDict) {
max_index = dataDict.max_index $.each(dataDict.data, function (index, item) { var tag = $('<div>') tag.text(item) $("#message").append(tag);
}) } } )
}, 1000)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
|
def get_msg(request): index = request.GET.get('index') index = int(index) context = { 'data': DB[index:], 'max_index': len(DB) }
return JsonResponse(context)
|
长轮询
长轮询: 当服务器收到前端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,
如果一直没有数据,则到达一定的时间限制(服务器端设置)才返回,返回完之后,客户端再次建立连接,周而复始,基于事件的触发,一个事件接一个事件
长轮询处理流程:
访问接口地址的时候,后端为每个用户创建一个队列
前端发送内容,数据发送到后天,将数据存储到每个人的队列中
前端递归获取消息,去自己的队列中获取数据,然后在界面上显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| USER_UID = "{{ uid }}";
function sendMessage() {
var text = $("#txt").val(); $.ajax({ url: '/pool/send/msg/', type: 'GET', data: { text: text
} }) }
function getMessage() { $.ajax( { url: '/pool/get/msg', type: 'GET', dataType: 'JSON', data: { uid: USER_UID, }, success: function (res) { if (res.status) { var tag = $("div"); tag.text(res.data) $("#message").append(tag); } getMessage(); } } ) }
$(function (){ getMessage(); })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| from django.shortcuts import render, HttpResponse from django.http.response import JsonResponse import queue
USER_QUEUE = {}
def home(request): uid = request.GET.get('uid') USER_QUEUE[uid] = queue.Queue() return render(request, 'home.html', {'uid': uid})
def send_msg(request): text = request.GET.get('text') for uid, q in USER_QUEUE.items() q.put(text)
return HttpResponse('ok')
def get_msg(request): uid = request.GET.get('uid') q = USER_QUEUE[uid] result = {'status': True, 'data': None} try: data = q.get(timeout=10) result['data'] = data except queue.Empty as e: result['status'] = False return JsonResponse(result)
|
Q:服务端持有连接,服务器压力是不是很大?
A:如果基于IO多路复用 + 异步,可以解决这个问题
WebSocket
原来的web中:
http
协议:无状态&短连接
客户端主动连接服务端
客户端向服务端发送消息,服务端收到返回数据
客户端接收到数据
断开连接
https
对数据进行加密
我们在开发过程中想要保留一些状态信息,基于cookie来做
现在支持:
http
协议:一次请求一次响应
websocket协议创建持久的连接不断开,基于这个连接可以进行收发数据【服务端向客户端主动推送消息】
channels 配置websocket
Django
默认不支持websocket
,需要Django
支持的话需要安装第三方组件
django channels
是django
支持websocket
的一个模块。
1.安装
2.settings.py
配置
1 2 3 4
| INSTALLED_APPS = ( 'channels', )
|
1 2
| ASGI_APPLICATION = "demo.asgi.application"
|
3.修改asgi文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter from WebSocketPretty import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DemoName.settings')
application = ProtocolTypeRouter( {'http': get_asgi_application(), 'websocket': URLRouter(routing.websocket_urlpatterns) } )
|
4.在app下创建routing.py (ws主路由)
Channels路由配置类似于Django URLconf
,当通道服务器接收到HTTP请求时,它告诉通道运行什么代码。
在app目录下,创建一个文件 routing.py文件
1 2 3 4 5 6 7 8
| from django.urls import path
from WebSocketPretty import consumers
websocket_urlpatterns = [ path('ws/',consumers.ChatConsumer.as_asgi()) ]
|
5.在app下创建consumers.py 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
self.accept()
def receive(self, text_data=None, bytes_data=None): print(text_data) self.send('ok')
def websocket_disconnect(self, message): raise StopConsumer
|
websocket收发消息
客户端主动向服务端发起websocket连接,服务端接收到连接后通过(握手)
客户端 websocket
1
| socket = new WebSocket('ws://127.0.0.1/ws/');
|
服务端
1 2 3 4
| def websocket_connect(self, message):
self.accept()
|
收发消息(客户端向服务端发消息)
1 2 3 4
| def receive(self, text_data=None, bytes_data=None): print('收到的消息->',text_data)
|
收发消息(服务端向客户端发送消息)
断开连接
websocket self
概念
self代表当前用户客户端与服务端的连接对象,比如两客户端发来了两个连接,我们可以把两个连接放在一起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| CONN_List = []
class LiveConsumer(WebsocketConsumer):
def websocket_connect(self, message): CONN_List.append(self)
def receive(self, text_data=None, bytes_data=None): for conn in CONN_List: conn.send('消息')
def websocket_disconnect(self, message): CONN_List.remove(self) raise StopConsumer
|
channels layers
settings.py
配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer", } }
安装:pip3 install channels-redis
CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG":{ "hosts":['10.211.55.25',6379] } }}
|
channel_layer使用
连接
1 2 3 4 5 6 7 8 9 10
| def websocket_connect(self, message): self.channel_layer.grouo_add('名称A',self.channel_name)
from asgiref.sync import async_to_sync def websocket_connect(self, message): async_to_sync(self.channel_layer.grouo_add)('名称A',self.channel_name)
|
发送消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def receive(self, text_data=None, bytes_data=None): self.send('1') async_to_sync(self.channel_layer.group_send)('名称A', {'messgae': '消息'})
def receive(self, text_data=None, bytes_data=None): async_to_sync(self.channel_layer.group_send)('名称A', {'type': 'ws_func','message':'消息'})
def ws_func(self,event): text = event['message'] self.send(text)
|
断开链接
1 2 3 4
| def websocket_disconnect(self, message): async_to_sync(self.channel_layer.group_discard)('名称A',self.channel_name)_ raise StopConsumer()
|
动态获取路由匹配分组
1 2 3 4 5 6 7
| re_path(r'room/(?P<group>\w+)/$',Consumer)
def websocket_connect(self, message):
group = self.scope['url_route']['kwargs'].get('group') async_to_sync(self.channel_layer.grouo_add)(group, self.channel_name)
|