Web端即时通讯技术(SEE,webSocket)

目录

  • 背景
  • 简介
  • 个人见解
  • 被动推送
    • 轮询
      • 简介
      • 实现
    • 长轮询(comet)
      • 简介
      • 实现
    • 比较
  • 主动推送
    • 长连接(SSE)
      • 简介
      • 实现
        • GET
        • POST
      • 效果
    • webSocket
      • 简介
        • WebSocket的工作原理:
        • WebSocket的主要优点:
        • WebSocket的主要缺点:
      • 实现
        • 用法一
        • 用法二
      • **效果**
    • 比较

背景

服务端和客户端应该怎么通信才能实现客户端能获取服务端最新消息让用户有更好的交互体验,如果是正常的发送一个请求首先要建立TCP连接然后等到服务器返回,如果是开发者可以通过发包情况就能知道建立连接成功与否,是否是在等待服务器响应,但是做为非开发者的普通用户当他点击一个按钮却没有任何反应他会怀疑是不是没点到还是卡住了之类了。不是一直点就是点到暴躁的放弃,不仅造成服务器的负担而且用户体验极差。也许我们可以在前端做一个虚假的转圈动画让客户知道正在处理,但是如果是个需要处理1小时的任务没有个进度条他也不知道是否值得等待。又假如我们做一个投票系统或者一个聊天室,我们要怎么让屏幕前的另一个彦祖及时看到呢?

简介

Web端即时通讯技术: 服务器端可以即时地将数据的更新或变化反应到客户端,例如消息即时推送等功能都是通过这种技术实现的。但是在Web中,由于浏览器的限制,实现即时通讯需要借助一些方法。这种限制出现的主要原因是,一般的Web通信都是浏览器先发送请求到服务器,服务器再进行响应完成数据的现实更新。

实现Web端即时通讯的方法: 实现即时通讯主要有四种方式,它们分别是轮询长轮询(comet)长连接(SSE)WebSocket。它们大体可以分为两类,一种是在HTTP基础上实现的,包括短轮询、cometSSE;另一种不是在HTTP基础上实现是,即WebSocket。下面分别介绍一下这四种轮询方式,以及它们各自的优缺点。

个人见解

以下纯属个人见解与实操经验,如有不当之处可以联系修改,感谢

有空再专门详细写一篇理论补充知识,这个得从计算机网络的传输层协议开始说起了,特别是websocket协议。他不是简单的三次握手,在这之后还有使用魔法字符串和key加密。这篇文偏向实操,所见即所得方便上手。

以下使用pythonweb框架的django实现,原理一样实现大同小异,尽量注释说明清楚,有不清楚的也可以联系我解答。

被动推送

轮询

简介

短轮询的基本思路就是浏览器每隔一段时间向浏览器发送http请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。

这种方式的优点是比较简单,易于理解,实现起来也没有什么技术难点。缺点是显而易见的,这种方式由于需要不断的建立http连接,严重浪费了服务器端和客户端的资源。尤其是在客户端,距离来说,如果有数量级想对比较大的人同时位于基于短轮询的应用中,那么每一个用户的客户端都会疯狂的向服务器端发送http请求,而且不会间断。人数越多,服务器端压力越大,这是很不合理的。

注意注意!!! 重点是什么,之前为了实现一个毫秒级的进度条,打开**开发者模式(F12)**后一秒发出了一千次请求,不说给服务器造成了什么样压力(是我的服务器我直接把你拉黑了),重点是不前面的请求记录一瞬间顶到天上去,十分不方便调试!!!!

实现

定义轮询方法

function renovate(t) {console.log("come in ")var sitv = setInterval(function () {var prog_url = '/renovate?t=' + t$.getJSON(prog_url, function (num_progress) {if (num_progress === 0) {console.log("正在讀取中")$('.progress-bar').css('width', '20%');$('.progress-bar').text('正在讀取中');} else if (num_progress > 99) {console.log("come in 99")clearInterval(sitv);$('.progress-bar').css('width', '99%');$('.progress-bar').text('99%');} else {console.log('t:' + t + '  num_progress' + num_progress)$('.progress-bar').css('width', num_progress + '%');$('.progress-bar').text(num_progress + '%');}});}, 1000);   //1000毫秒查询一次后台进度}

调用轮询

$(function () {bindBtnAddEvent();})function bindBtnAddEvent() {$('#save').click(function () {{#alert('開始')#}var t = $.now()renovate(t)});}

这里我专门在后端写了个接口来读取数据库数据给前端获取,很简单就不写出来了,传了个时间参数是为了在后端构建一个字典使得获取到正确的属于该用户的进度条,不然可能出现如果两个人同时访问脏数据的可能,就以防万一。

长轮询(comet)

简介

ajax实现:
  当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置)才返回。。 客户端JavaScript响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。

简单来说:就是服务端维持一个消息队列,当收到一个请求就检查消息队列看有没有数据有就把数据取出来返回给客户端,因为队列是先进先出所以顺序是对的,没有数据就hold住这个请求一会,众所周知TCP连接后还有一个响应时间如果在响应时间也就是还没响应时,当你看开发者模式这时的status就不是状态码而是pending

举例理解一下

requests.get('www.baidu.com',timeout=(5,10))

这里的代码是一个简单的请求,设置的这个timeout是一个元组的形式,第一个参数为连接超时,第二个是响应超时,这里我们设置10秒,当10秒后没有响应这个连接也结束了,这样就不会hold住。

实现

因为是简单实现一个demo,所以其实里面有很多不完善的地方不要纠结细节。

demo是实现一个聊天室

urls.py(这里是配置路由的,就是接口)

from django.conf import settings
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import servefrom app01.views import chaturlpatterns = [path('longPoll/chat/', chat.longPoll_chat),path('send/msg/', chat.send_msg),path('get/msg/', chat.get_msg),
]

效果
在这里插入图片描述

view.py(也就是视图函数,啥框架都有的处理接口的那个)

注意这里用全局变量是不合理的,就是demo而已,深度建议用redis的发布与订阅实现,一是减少开销不用给每个新用户建一个队列浪费空间,二是redis比较快

import queuefrom django.shortcuts import render
from django.http import JsonResponse# 这里用python的队列来模拟。也可以使用redis的发布和订阅来实现,则不需要建那么多队列,所有访客都可以访问同一个消息队列
USER_QUEUE = {}  # {'asd':queue.Queue(),'qwe':queue.Queue()} 为每一个访客建一个队列def longPoll_chat(request):""" 展示聊天界面 """uid = request.GET.get('uid')USER_QUEUE[uid] = queue.Queue() # 来了个彦祖,为他创建一个队列return render(request, 'longPoll_chat.html', {'uid': uid})def send_msg(request):""" 发送消息接口,当用户在聊天室发一句话点击事件就会触发这个 """text = request.GET.get('text') # 获取文本for uid, q in USER_QUEUE.items(): # 遍历所有的队列q.put(text) # 为每一个队列添加消息,为了让这个聊天室的每个用户消息同步return JsonResponse({"msg": 'ok'})def get_msg(request):"""" 获取消息,就是看这个聊天室里面有没有其他人发了消息,有就接收以下 """uid = request.GET.get('uid')q = USER_QUEUE[uid]  # 获取自己的队列result = {'status': True, 'data': None}try:# 这里就是 hold 请求了,监听队列10s如果有数据就能get到,如果10s过了还没有就会触发 except queue.Empty,不同的status发送到前端前端就可以判断了data = q.get(timeout=10) # 注意这里如果不设置timeout会一直等下去,程序会卡在这里,等的花都谢了result['data'] = dataexcept queue.Empty as e:result['status'] = Falsereturn JsonResponse(result)

longPoll_chat.html(这里就是前端部分了,只是前后端不分离用了一些模板引擎的语法,但是光看js怎么发送请求应该就懂了,html怎么写不太重要自己修改即可)

{% extends 'layout.html' %}{% block css %}<style>.message {height: 300px;border: 1px solid #dddddd;width: 100%;}</style>
{% endblock %}{% block content %}<div class="message" id="message"></div><div><input type="text" placeholder="请输入" id="txt"><input type="button" value="发送" onclick="sendMessage();"> # 这里给这个按钮绑定点击事件</div>
{% endblock %}{% block js %} // 这是模板引擎的语法,就继承模板的那个挖洞,有点类似vue的组件和插槽的感觉<script>USER_UID = "{{ uid }}"; // 这是模板引擎的语法,就是接收后端发过来的东西,有点类似vue的那个插值表达式const MAX_REQUESTS = 5; // 最大请求次数,主要是不想看他一直发,你可以写不同的逻辑let requestCount = 0; // 请求计数器function sendMessage() {let text = $("#txt").val();// 获取文本,jQuery没啥好说的了,不是这篇文的重点,后面有关jQuery的就不注释说明了$.ajax({url: '/send/msg/',type: 'GET',data: {text: text},success: function (res) {console.log("请求发送成功", res)}})}function getMessage() {$.ajax({url: '/get/msg/',type: 'GET',data: {uid: USER_UID,},dataType: "JSON",success: function (res) {// 超时,没有数据,也就是status为False,不是True自然是False就不写了// 有新数据,暂时信息数据if (res.status) {//将内容拼成div标签,并添加到message区域var tag = $("<div>");tag.text(res.data)  //<div>啊大大</div>$("#message").append(tag);}// 请求次数requestCount++;// 检查是否达到最大请求次数if (requestCount === MAX_REQUESTS) {console.log('达到最大请求次数,结束长轮询');return; // 结束长轮询}getMessage();//自己调用自己,JS该模式实际不是递归,不会栈溢出}})}$(function () {getMessage();})</script>
{% endblock %}

轮询与长轮询都是基于HTTP的,两者本身存在着缺陷:轮询需要更快的处理速度;长轮询则更要求处理并发的能力;两者都是“被动型服务器”的体现:服务器不会主动推送信息,而是在客户端发送ajax请求后进行返回的响应。而理想的模型是"在服务器端数据有了变化后,可以主动推送给客户端",这种"主动型"服务器是解决这类问题的很好的方案。

注意!! 但是啊,虽然websocket这种双向通信非常实用但是旧浏览器以及部分老设备的客户端不支持websocket协议,所以一般大公司(eg:微信)是使用长轮询,比较通用。虽然使用更多内存会占用服务器资源但是人家家大业大不怕,人家追求稳定通用。

比较

长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。

主动推送

长连接(SSE)

简介

Server-Sent Events(SSE) 是一种用于实现服务器向客户端实时推送数据的Web技术。与传统的轮询和长轮询相比,SSE提供了更高效和实时的数据推送机制。

SSE基于HTTP协议,允许服务器将数据以事件流(Event Stream)的形式发送给客户端。客户端通过建立持久的HTTP连接,并监听事件流,可以实时接收服务器推送的数据。

SSE的主要特点包括:

  • 简单易用SSE使用基于文本的数据格式,如纯文本、JSON等,使得数据的发送和解析都相对简单。
  • 单向通信SSE支持服务器向客户端的单向通信,服务器可以主动推送数据给客户端,而客户端只能接收数据。
  • 实时性SSE建立长时间的连接,使得服务器可以实时地将数据推送给客户端,而无需客户端频繁地发起请求。

进行SSE实时数据推送时的注意点

  • 异步处理: 由于SSE是基于长连接的机制,推送数据的过程是一个长时间的操作。为了不阻塞服务器线程,推荐使用异步方式处理SSE请求。您可以在控制器方法中使用**@Async注解或使用CompletableFuture**等异步编程方式。
  • 超时处理: SSE连接可能会因为网络中断、客户端关闭等原因而发生超时。为了避免无效的连接一直保持在服务器端,您可以设置超时时间并处理连接超时的情况。可以使用SseEmitter对象的setTimeout()方法设置超时时间,并通过onTimeout()方法处理连接超时的逻辑。
  • 异常处理: 在实际应用中,可能会出现一些异常情况,如网络异常、推送数据失败等。您可以使用SseEmitter对象的completeWithError()方法将异常信息发送给客户端,并在客户端通过eventSource.onerror事件进行处理。
  • 内存管理: 使用SseEmitter时需要注意内存管理,特别是在大量并发连接的情况下。当客户端断开连接时,务必及时释放SseEmitter对象,避免造成资源泄漏和内存溢出。
  • 并发性能: SSE的并发连接数可能会对服务器的性能造成影响。如果需要处理大量的并发连接,可以考虑使用线程池或其他异步处理方式,以充分利用服务器资源。
  • 客户端兼容性: 虽然大多数现代浏览器都支持SSE,但仍然有一些旧版本的浏览器不支持。在使用SSE时,要确保您的目标客户端支持SSE,或者提供备用的实时数据推送机制。
    这些注意点将有助于您正确和高效地使用SseEmitter进行SSE实时数据推送。根据具体的应用需求,您可以根据实际情况进行调整和优化。

实现

SSE的优点就是实现比较简单,跟HTTP基本一样所以就不需要改动太多

view.py(直接在你原来用HTTP那个处理逻辑上修改就行了基本一样)

from django.http import HttpResponse, JsonResponse, StreamingHttpResponsedef test(request):""" 该实现我采用两种返回结果,因为考虑到可能会有客户端不支持SSE,前端判断支持之后再使用流式传输 """data = json.loads(request.body.decode())text_list = data.get('text') # [{},{}]# 没有该标志采用JSON传输if request.GET.get('stream') is None:# 这就是你原来的处理逻辑了# 这里是个demo就简单做一个保存数据库,你可以根据需求写你自己的逻辑id_list = []for text in text_list:id = User.objects.create(**text)id_list.append(id)time.sleep(60) # 这里模拟一下耗时操作,你的逻辑可能就是这里导致处理特别慢,循环多少次就要多少个一分钟了# 一次性返回,假如要一小时来执行上面的逻辑,用户傻傻等待一小时快要睡着了以为还没开始然后突然跟你说全部处理好了简直人麻了return JsonResponse({"code": 200, "data": id_list, "msg": '保存成功'})# 采用流式传输elif request.GET.get('stream') is True or request.GET.get('stream') == 'true':def sse_stream():""" 原来的处理逻辑 """# 闭包可以获取外部数据# 这里是个demo就简单做一个保存数据库,你可以根据需求写你自己的逻辑for text in text_list:id = User.objects.create(**text)time.sleep(60) # 这里模拟一下耗时操作,你的逻辑可能就是这里导致处理特别慢,不管循环多少次,至少一分钟用户就能得到响应而不是循环次数*一分钟后才得到响应data_dict = {'code': 200,'message': '有一个成功了','data': id}# 返回部分数据给用户查看# 注意了,SSE传输的数据格式必须是这样# "data: 你的数据\n\n" ,必须是data开头\n\n结束sse_message = "data: {}\n\n".format(json.dumps(data_dict))yield sse_message # 这里就是不一样的地方了,先返回一段去给用户看,直接就知道是哪个好了# return # 你的逻辑中那些try...except,if...else之类的想要服务器主动中断链接的这里就直接return就行了# 这是第二个区别了,这是在闭包之外也就是真正返回响应的地方,这里把JsonResponse换成StreamingHttpResponse就可以进行流式传输# 注意content_type必须是text/event-streamreturn StreamingHttpResponse(sse_stream(), content_type='text/event-stream')else:# 带了标记但是不是true也不管他return responseJson(400, None, "流式传输参数错误")

客户端重新连接策略:

yield "retry: 5000\n\n"  # 5 seconds

所有数据发送完成时: 当服务器发送完所有数据并且生成器函数结束时,连接将自动关闭。在这种情况下,除非客户端尝试重新连接,否则连接不会再次建立。

使用return:在生成器函数中使用return将会结束该函数,从而结束SSE连接。如果客户端被设置为在连接断开时自动重连(这是默认行为),它可能会尝试重新连接。您可以通过发送一个特定的retry值来控制或禁止这种行为。例如,yield “retry: 0\n\n” 会告诉客户端在连接断开时不要重新连接。

yield "retry: 0\n\n"

总的来说,当生成器函数不再产生输出时,SSE连接将结束,无论是由于函数自然结束还是由于某个return语句。

前端js

GET

// 使用GET请求接收流式数据的类
class StreamDataFetcherGET {constructor(endpoint) {this.endpoint = endpoint;}// 初始化并监听数据init() {// 创建一个新的EventSource实例,连接到提供的endpointconst evtSource = new EventSource(this.endpoint);// 当从服务器收到新的数据时evtSource.onmessage = (event) => {const data = JSON.parse(event.data);this.renderData(data);};// 当与服务器的连接发生错误时evtSource.onerror = (error) => {console.error("EventSource failed:", error);evtSource.close();};}// 渲染接收到的数据renderData(data) {// 这里的代码取决于如何渲染数据到你的页面上console.log(data);}
}// 使用GET请求的示例
const fetcherGET = new StreamDataFetcherGET("请求接口");
fetcherGET.init();

还可以先判断是否客户端支持是否支持SSE(下面POST请求同理),示例如下

// 我后台配置了只有带上?stream=true采用流式传输,你先判断浏览器类型支持SSE不,IE系列都不支持,然后特别老的Edge和狗都不用的浏览器也不支持,如果他不支持就不要走这个方法走你原来那个函数我就不cv了// 检查是否支持SSE
if (typeof EventSource !== "undefined") {// 支持SSE,所以请求流式数据const evtSource = new EventSource("SSE的请求接口");evtSource.onmessage = function(event) {const data = JSON.parse(event.data);console.log(data);};evtSource.onerror = function(error) {console.error("EventSource failed:", error);evtSource.close(); // 断开SSE连接};
} else {// 不支持SSE,所以请求JSON数据fetch("JSON的请求接口").then(response => response.json()).then(data => {console.log(data);}).catch(error => {console.error("Error fetching JSON data:", error);});
}

POST

对于POST请求,EventSource不适用,因为它仅支持GET请求。使用POST请求处理SSE需要一些额外的工作,如下所示:

// 使用POST请求接收流式数据的类
class StreamDataFetcherPOST {constructor(endpoint, postData) {this.endpoint = endpoint;this.postData = postData;}// 初始化并监听数据async init() {// 使用fetch进行POST请求const response = await fetch(this.endpoint, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(this.postData)});const reader = response.body.getReader();const decoder = new TextDecoder();let buffer = '';while (true) {const { done, value } = await reader.read();if (done) break;buffer += decoder.decode(value, { stream: true });while (buffer.includes("\n")) {const lineEnd = buffer.indexOf("\n");const line = buffer.slice(0, lineEnd).trim();buffer = buffer.slice(lineEnd + 1);this.renderData(JSON.parse(line));}}}// 渲染接收到的数据renderData(data) {// 这里的代码取决于如何渲染数据到你的页面上console.log(data);}
}// 使用POST请求的示例
const postData = { key: "value" };  // 替换为你的POST数据
const fetcherPOST = new StreamDataFetcherPOST("请求接口", postData);
fetcherPOST.init();

不用类

async function createStreamFetcherPOST(endpoint, postData) {const response = await fetch(endpoint, {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify(postData)});const reader = response.body.getReader();const decoder = new TextDecoder();let resultString = '';while (true) {const { done, value } = await reader.read();if (done) break;resultString += decoder.decode(value, { stream: true });while (resultString.includes("\n\n")) {const lineEnd = resultString.indexOf("\n\n");const line = resultString.slice(0, lineEnd).trim();// 检查是否有retry: 0消息if (fullMessage === "retry: 0") {console.log("Received retry: 0. Stopping connection...");reader.cancel();  // 取消读取,这将导致流结束并跳出循环return;  // 结束处理函数}try {const payload = JSON.parse(line.replace(/^data: /, ""));renderData(payload);} catch (e) {console.error("Error parsing segment:", e);}resultString = resultString.slice(lineEnd + 2);}}function renderData(data) {console.log(data);if (payload.code === 200) {// 处理数据,简单就设个变量累加就知道数量,不然渲染逻辑传参就在这} else {// 错误的,简单就不累加咯}}
}// 使用POST请求的示例
const postData = { key: "value" };  // 替换为您的POST数据
createStreamFetcherPOST("your_POST_endpoint_url_here", postData);

效果

这个EverntStream 就是用了SSE
在这里插入图片描述

webSocket

注意,其他都是基于HTTP协议,而websocket是基于TCP协议

简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket的工作原理:

  1. 客户端发起WebSocket连接,通过HTTP协议发起Upgrade请求将连接升级为WebSocket连接。
  2. 服务器端响应Upgrade请求,完成连接升级。
  3. 建立WebSocket连接后,客户端和服务器端就可以通过这个连接通道自由地双向传输数据,无需等待对方的请求。

WebSocket的主要优点:

  • 允许全双工通信: 客户端和服务器端都可以主动发送数据。
  • 更实时: 服务器有新数据可以立即主动推送给客户端。
  • 更轻量: 建立连接的开销小,通信高效,减少不必要的网络请求。
  • 利用HTTP协议做升级握手,默认端口是80和443,避免了跨域问题。
  • 支持扩展,可以扩展自定义的子协议。

WebSocket的主要缺点:

  • 不如HTTP协议广泛应用,存在兼容性问题。
  • 需要浏览器和服务器端都支持WebSocket协议,增加了开发成本。
  • 有连接建立和关闭的开销,不适用于量小数据的交互。
  • 安全性需要额外考虑,通信内容是明文,需要加密。
  • 处于连接状态时,会占用服务器端资源。

在这里插入图片描述
在这里插入图片描述
简介就不多说了哈~大概图解一下就行了,他这个连接过程要详细知道得单独写一篇,直接进入实操。

实现

因为是基于django来实现的所以有些东西该配置还得配置,不同框架这个配置确实就不一样了,但是前端是一样的

安装库

这里可能会遇到一些问题,因为channels需要和django版本对应上,如有问题可以参考我的另一篇文章,django使用channels实现webSocket启动失败,求顺手一赞嘿嘿。

pip install channels

settings.py同级目录下新增asgi.py文件(django4自带了)和routing.py文件,内容稍后说

目录结构如下
在这里插入图片描述

settings.py(配置文件)

INSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','channels', # 注册这个'app01.apps.App01Config',
]WSGI_APPLICATION = 'staffSystem_django.wsgi.application' # 原本只有这个东西,这个不去掉也可以
ASGI_APPLICATION = 'staffSystem_django.asgi.application' # 新增这个,就是你的asgi目录所在地# channels 配置存在内存中
CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer",}
}
# channels 配置存在redis中
# CHANNEL_LAYERS = {
#    "default": {
#        "BACKEND": "channels_redis.core.RedisChannelLayer",  # pip install channels-redis
#        "CONFIG": {
#            "hosts": [('127.0.0.1', 6379)]
#            # "hosts": ["redis://127.0.0.1:6379/1"]
#        },
#    }
# }

这里建议 channels 配置存在redis中,这只是demo才配在内存中,在setting.py修改这个配置就行了其他不用变

routing.py(配置使用websocket的路由)

# @Author: fbz
# @File : routing.py
from django.urls import path
from app01.views import chatwebsocket_urlpatterns = [path(r'ws/<group>/',chat.wsChat.as_asgi()),
]

asgi.py(配置asgi)

"""
ASGI config for staffSystem_django project.It exposes the ASGI callable as a module-level variable named ``application``.For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""import osfrom django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from . import routingos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'staffSystem_django.settings')# application = get_asgi_application() # 如果这样配置就收不到http请求了application = ProtocolTypeRouter({'http': get_asgi_application(), # http请求走这个'websocket': URLRouter(routing.websocket_urlpatterns) # ws请求走这个
})

urls.py

"""staffSystem_django URL ConfigurationThe `urlpatterns` list routes URLs to views. For more information please see:https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views1. Add an import:  from my_app import views2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views1. Add an import:  from other_app.views import Home2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf1. Import the include() function: from django.urls import include, path2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.conf import settings
from django.contrib import admin
from django.urls import path, re_path
from django.views.static import servefrom app01.views import depart, user, account, admin, pretty, task, order, chart, upload, city, chaturlpatterns = [path('ws/chat/', chat.ws_chat),
]

配置完成启动django项目的效果
在这里插入图片描述

ws_chat.html(前端)

{% extends 'layout.html' %}
{% block css %}<style>.message {height: 300px;border: 1px solid #dddddd;width: 100%;}</style>
{% endblock %}{% block content %}<div class="message" id="message"></div><div><input type="text" placeholder="请输入" id="txt"><input type="button" value="发送" onclick="sendMessage();"><input type="button" value="关闭连接" onclick="closeConn();"></div>
{% endblock %}{% block js %}<script>// socket = new WebSocket('ws://127.0.0.1:8000/ws/123/');socket = new WebSocket('ws://' + window.location.host + '/ws/{{group_id}}/');// 创建好连接之后自动触发(服务端执行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>
{% endblock %}

用法一

基本每一行都注释说明了,真是极致详细

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_syncdef ws_chat(request):return render(request, 'ws_chat.html') # 显示界面class wsChat(WebsocketConsumer):def websocket_connect(self, message):# 有客户端来向后端发送ws连接的请求时,自动触发。self.accept()  # 服务端允许和客户端创建连接print('连接成功')# 服务端允许和客户端创建连接(握手)self.accept()  # 同时请求WebSocket HANDSHAKING和WebSocket CONNECT,分别是握手和连接def websocket_receive(self, message):# 浏览器基于ws向后端发送数据,自动触发接收消息print(message)self.send('asdasd')# self.close()  # 服务端主动断开连接text = message['text']  # {'type': 'websocket.receive', 'text': 'asd'}if text == 'close':# 服务端主动断开连接self.close()  # 同时也会执行客户端断开连接方法 WebSocket DISCONNECT(websocket_disconnect())return  # 不再执行下面的代码,如果断开连接还发送消息会报错# raise StopConsumer() #如果服务端断开连接时,执行raise StopConsumer(),那么不会执行websocket_disconnect()方法# print('接收到消息:', text)self.send(f'接收到消息:{text}')  # 服务端给客户端发送消息def websocket_disconnect(self, message):# 客户端与服务端端开连接时自动触发(客户端主动端开连接)print('断开连接')raise StopConsumer()  # WebSocket DISCONNECT

用法二

使用了组,这也是django特有的,像flask不是使用channels实现websocket的 他没有组这个概念。

修改上面的类

class wsChat(WebsocketConsumer):def websocket_connect(self, message):# 有客户端来向后端发送ws连接的请求时,自动触发。print('连接成功')# 获取群号,获取路由匹配中的group = self.scope['url_route']['kwargs'].get('group')# 服务端允许和客户端创建连接(握手)self.accept()  # 同时请求WebSocket HANDSHAKING和WebSocket CONNECT,分别是握手和连接# 将这个客户端的连接对象加入到内存或redis中,取决于setting.py中CHANNEL_LAYERS# async_to_sync将异步转为同步async_to_sync(self.channel_layer.group_add)(group, self.channel_name)def websocket_receive(self, message):# 浏览器基于ws向后端发送数据,自动触发接收消息text = message['text']  # {'type': 'websocket.receive', 'text': 'asd'}if text == 'close':# 服务端主动断开连接self.close()  # 同时也会执行客户端断开连接方法 WebSocket DISCONNECT(websocket_disconnect())return  # 不再执行下面的代码,如果断开连接还发送消息会报错# raise StopConsumer() #如果服务端断开连接时,执行raise StopConsumer(),那么不会执行websocket_disconnect()方法# self.send(f'接收到消息:{text}')  # 服务端给客户端发送消息# 获取群号,获取路由匹配中的group = self.scope['url_route']['kwargs'].get('group')# 通知组内的所有客户端,执行 xx_oo 方法,在此方法中自己可以去定义任意的功能async_to_sync(self.channel_layer.group_send)(group, {'type': 'aa.bb', 'message': message}) # aa_bb,下划线变成点def aa_bb(self, event):text = event['message']['text']# 这是给组内的所有人发送消息,在websocket_receive中的self.send(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()  # WebSocket DISCONNECT

该类实现了一个功能就是只有同个组的成员才可以收到消息,也就是我们的群聊功能,只有当群号(组号)一样的聊天室才可以收到同一个群的其他成员发送的消息。

效果

注意!!注意看请求头,必须携带这些信息才能建立一个ws连接

服务器需要检查Upgrade头信息,如果支持WebSocket,就返回101状态码和Upgrade头信息,表示切换协议。

WebSocket 协议在建立连接时的 Sec-WebSocket-Key 头信息和魔法字符串的相关验证过程。这主要是为了防止恶意的 WebSocket 请求。
具体过程是:

  1. 客户端发起 WebSocket 请求时,需要在请求头包含 Sec-WebSocket-Key 字段,内容为一个随机的字符串。
  2. 服务器端收到请求后,会将客户端发来的 Sec-WebSocket-Key 后的值与一个特定的魔法字符串(magic_string) “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 拼接。
  3. 对拼接后的字符串做 SHA-1 摘要,然后进行 BASE-64 编码,得到一个 hash 值。
  4. 服务器端需要在响应头的 Sec-WebSocket-Accept 字段设置这个 hash 值。
  5. 客户端收到响应后,也按相同的算法计算出一个 hash 值,与服务器返回的 Sec-WebSocket-Accept 值进行比较。
  6. 如果两个 hash 值一致,则验证通过,确认服务器端支持 WebSocket 协议,然后建立连接。

这个验证过程可以防止普通的 HTTP 客户端与服务器建立 WebSocket 连接,保证了连接的安全性。所以它是 WebSocket 协议握手过程中的一个重要组成部分。
在这里插入图片描述

在这里插入图片描述

比较

SSE与WebSocket的比较
WebSocket是另一种用于实现实时双向通信的Web技术,它与SSE在某些方面有所不同。下面是SSEWebSocket之间的比较:

  • 数据推送方向: SSE是服务器向客户端的单向通信,服务器可以主动推送数据给客户端。而WebSocket双向通信,允许服务器和客户端之间进行实时的双向数据交换。
  • 连接建立: SSE使用基于HTTP的长连接,通过普通的HTTP请求和响应来建立连接,从而实现数据的实时推送。WebSocket使用自定义的协议,通过建立WebSocket连接来实现双向通信。
  • 兼容性:由于SSE基于HTTP协议,它可以在大多数现代浏览器中使用,并且不需要额外的协议升级。WebSocket在绝大多数现代浏览器中也得到了支持,但在某些特殊的网络环境下可能会遇到问题。
  • 适用场景: SSE适用于服务器向客户端实时推送数据的场景,如股票价格更新、新闻实时推送等。WebSocket适用于需要实时双向通信的场景,如聊天应用、多人协同编辑等。

根据具体的业务需求和场景,选择SSEWebSocket取决于您的实际需求。如果您只需要服务器向客户端单向推送数据,并且希望保持简单易用和兼容性好,那么SSE是一个不错的选择。如果您需要实现双向通信,或者需要更高级的功能和控制,那么WebSocket可能更适合您的需求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/11346.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学习笔记|大模型优质Prompt开发与应用课(二)|第四节:大模型帮你写代码,小白也能做程序

文章目录 01软件开发产业趋势与技术革新软件开发产业趋势与技术革新技术性人才很受欢迎软件开发产业趋势与技术革新技术门槛越来越低 02 大模型驱动的软件开发需求分析prompt 产品设计开发和测试prompt输出回复promptpromptprompt回复 发布和部署promptprompt 维护和更新prompt…

Docker中的网络

文章目录 网络网桥&#xff08;bridge&#xff09;创建网桥接口hostnonecontaineroverlayoverlay底层原理 网络 网桥&#xff08;bridge&#xff09; 在Docker中&#xff0c;网桥&#xff08;Bridge&#xff09;是一种网络驱动&#xff0c;用于实现Docker容器之间和容器与宿主…

SpringBoot中接口幂等性实现方案-自定义注解+Redis+拦截器实现防止订单重复提交

场景 SpringBootRedis自定义注解实现接口防刷(限制不同接口单位时间内最大请求次数)&#xff1a; SpringBootRedis自定义注解实现接口防刷(限制不同接口单位时间内最大请求次数)_redis防刷_霸道流氓气质的博客-CSDN博客 以下接口幂等性的实现方式与上面博客类似&#xff0c;…

python pygbag教程 —— 在网页上运行pygame程序(全网中文教程首发)

pygame是一款流行的游戏制作模块&#xff0c;经过特殊的方式编译后&#xff0c;可以在浏览器web网页上运行。web上的打包主要使用第三方模块pygbag。 pygame教程&#xff1a;Python pygame(GUI编程)模块最完整教程&#xff08;1&#xff09;_pygame模块详解_Python-ZZY的博客-…

【配置环境】Windows下 VS Code 远程连接虚拟机Ubuntu

一&#xff0c;环境 Windows 11 家庭中文版VMware Workstation 16 Pro &#xff08;版本&#xff1a;16.1.2 build-17966106&#xff09;ubuntu-22.04.2-desktop-amd64 二&#xff0c;关键步骤 Windows下安装OpenSSHVS Code安装Remote - SSH插件 三&#xff0c;详细步骤 在Ubun…

React 前端应用中快速实践 OpenTelemetry 云原生可观测性(SigNoz/K8S)

OpenTelemetry 可用于跟踪 React 应用程序的性能问题和错误。您可以跟踪从前端 web 应用程序到下游服务的用户请求。OpenTelemetry 是云原生计算基金会(CNCF)下的一个开源项目&#xff0c;旨在标准化遥测数据的生成和收集。已成为下一代可观测平台的事实标准。 React(也称为 Re…

Kotlin 内联函数语法之let、apply、also、run、with的用法与详解

一、介绍 kotlin的语法千奇百怪&#xff0c;今天我们将介绍项目中频率使用比较高的几个内联函数。 二、什么叫内联函数&#xff1f; 内联函数 的语义很简单&#xff1a;把函数体复制粘贴到函数调用处 。使用起来也毫无困难&#xff0c;用 inline关键字修饰函数即可。 语法&a…

详解zookeeper安装使用

目录 1.概述 1.1.功能 1.2.特点 1.3.数据结构 2.安装 2.1.Windows 2.2.Linux 3.基础操作 3.1.增 3.2.删 3.3.改 3.4.查 3.5.监听 4.JAVA操作Zookeeper 4.1.依赖 4.2.客户端 4.3.增 4.4.删 4.5.查 4.6.改 1.概述 1.1.功能 zookeeper&#xff0c;Apache旗下…

pdf转换word软件哪个好?式?这款软件帮你轻松实现转换

在工作中&#xff0c;我们常常遇到这样的情况&#xff1a;我们的文件可能是PDF格式的&#xff0c;但对方要求我们以Word形式发送&#xff0c;因为Word相对于PDF占用更小的内存&#xff0c;打开更方便&#xff0c;发送时间更短。这时我们需要将PDF转换为Word格式&#xff0c;然而…

【跨代码仓库合并方案】

1、背景&#xff1a; 1、wiser绑定的uiidA的定制修改内容和ELKO绑定的uiidB基本是一样的&#xff0c;需要手动粘贴同步&#xff0c;增加测试保障风险&#xff0c;还会浪费开发资源投入&#xff1b; 2、施耐德wiser和elko面板两套面板基本一致&#xff0c;但是经过new art升级后…

机器学习深度学习——感知机

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——softmax回归的简洁实现 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们…

市面上的ipad国产触控笔怎么样?精选的性价比电容笔

要知道&#xff0c;真正的苹果品牌的那款原装电容笔&#xff0c;光是一支电容笔就价格近千元。实际上&#xff0c;平替电容笔对没有太多预算的用户是个不错的选择。一支苹果品牌的电容笔&#xff0c;价格是平替品牌的四倍&#xff0c;但电容笔的书写效果&#xff0c;却丝毫不逊…

科技云报道:是时候全员FinOps了吗?

科技云报道原创。 在论坛上&#xff0c;国外某企业的真实案例引发了热议。一开始该企业只顾技术创新&#xff0c;积极上云&#xff0c;不顾成本。 直到有一天&#xff0c;高层介入喊停&#xff1a;“这个云不能再上了&#xff0c;成本已经远大于收益了”。该企业因为成本失控…

java-day01

一&#xff1a;基础常识 软件&#xff1a;按照特定顺序的计算机数据与指令的集合。可分为系统软件&#xff08;如操作系统&#xff09;和应用软件&#xff08;如QQ&#xff09; 人机交互方式&#xff1a;图形化界面&#xff08;GUI&#xff09;与命令行&#xff08;CLI&#…

性能优化 - 前端性能监控和性能指标计算方式

性能优化 - 前端性能监控和性能指标计算方式 前言一. 性能指标介绍1.1 单一指标介绍1.2 指标计算① Redirect(重定向耗时)② AppCache(应用程序缓存的DNS解析)③ DNS(DNS解析耗时)④ TCP(TCP连接耗时)⑤ TTFB(请求响应耗时)⑥ Trans(内容传输耗时)⑦ DOM(DOM解析耗时) 1.3 FP(f…

代码随想录算法训练营第二天| 977

977. 有序数组的平方y 思路&#xff0c;原数组是有序的&#xff0c;但是因为负数平方后可能变无序了&#xff0c;因此利用双指针遍历原数组&#xff0c;比较 nums[left]*nums[left]和nums[right]*nums[right]谁更大&#xff0c;然后对新数组赋值 class Solution {public int…

MFC第二十四天 使用GDI对象画笔和画刷来开发控件(分页控件选择态的算法分析、使用CToolTipCtrl开发动静态提示)

文章目录 GDI对象画笔和画刷来开发控件梯形边框的按钮控件CMainDlg.hCMainDlg.cppCLadderCtrl.hCLadderCtrl.cpp 矩形边框的三态按钮控件 CToolTipCtrl开发动静态提示CMainDlg.hCMainDlg.cppCLadderCtrl.hCLadderCtrl.cpp: 实现文件 矩形边框的三态按钮控件 CToolTipCtrl开发动…

欢乐暑假,华为儿童手表5系列为孩子位置安全保驾护航!

暑假带娃&#xff0c;就像爸妈的练兵场。幸好有 5 系列&#xff0c;离线定位、位置提醒、行为记录等安全守护功能面面俱到、样样精通&#xff0c;陪伴孩子度过悠长假期&#xff0c;也让爸妈长辈更安心更省力&#xff5e; 暑期到了&#xff0c;小朋友们都想出去玩&#xff0c;但…

修改密码和再次确认密码的js和element-ui的使用

<template><div><!-- plan的插槽 --><plan title"修改密码"><!-- 插槽的名字 --><span slot"header">修改密码</span><el-form:model"ruleForm2"status-icon:rules"rules2"ref"rul…

【数据结构】实验七:字符串

实验七 字符串实验报告 一、实验目的与要求 1&#xff09;巩固对串的理解&#xff1b; 2&#xff09;掌握串的基本操作实现&#xff1b; 3&#xff09;掌握 BF 和 KMP 算法思想。 二、实验内容 1. 给定一个字符串ababcabcdabcde和一个子串abcd,查找字串是否在主串中出现。…