当前位置: 首页 > news >正文

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可能更适合您的需求。

相关文章:

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

目录 背景简介个人见解被动推送轮询简介实现 长轮询&#xff08;comet&#xff09;简介实现 比较 主动推送长连接&#xff08;SSE&#xff09;简介实现GETPOST 效果 webSocket简介WebSocket的工作原理:WebSocket的主要优点:WebSocket的主要缺点: 实现用法一用法二 **效果** 比较…...

脑电信号处理与特征提取——4.脑电信号的预处理及数据分析要点(彭微微)

目录 四、脑电信号的预处理及数据分析要点 4.1 脑电基础知识回顾 4.2 伪迹 4.3 EEG预处理 4.3.1 滤波 4.3.2 重参考 4.3.3 分段和基线校正 4.3.4 坏段剔除 4.3.5 坏导剔除/插值 4.3.6 独立成分分析ICA 4.4 事件相关电位&#xff08;ERPs&#xff09; 4.4.1 如何获…...

分析npm run serve之后发生了什么?

首先需要明白的是&#xff0c;当你在终端去运行 npm run ****&#xff0c;会是什么过程。 根据上图的一个流程&#xff0c;就可以衍生出很多问题。 1&#xff0c;为什么不直接运行vue-cli-service serve? 因为直接运行 vue-cli-service serve&#xff0c;会报错&#xff0c…...

LINUX上操作redis 用shell7

LINUX上操作redis 用shell7 步骤1&#xff1a;连接到Linux服务器步骤2&#xff1a;安装和配置Redis步骤3&#xff1a;连接到Redis服务器步骤4&#xff1a;操作Redis数据步骤5&#xff1a;断开与Redis服务器的连接 步骤1&#xff1a;连接到Linux服务器 首先&#xff0c;需要使用…...

Python的threading模块

为引入多线程的概念&#xff0c;下面是一个例子&#xff1a; import time, datetimestartTime datetime.datetime(2024, 1, 1, 0, 0, 0) while datetime.datetime.now() < startTime:time.sleep(1)print(Program now starting on NewYear2024) 在等待time.sleep()的循环调…...

HTML5 的离线储存怎么使用,工作原理

TML5提供了一种称为离线储存&#xff08;Offline Storage&#xff09;的功能&#xff0c;它允许网页在离线时缓存和存储数据&#xff0c;以便用户可以在没有网络连接的情况下访问这些数据。离线储存是通过使用Web Storage API或者应用程序缓存&#xff08;Application Cache&am…...

FTP文件传输协议与DHCP

基本概念 主机之间传输文件是IP网络的一个重要功能 互联网早期&#xff0c;最通用方式就是使用FTP&#xff08;File Transfer Protocol&#xff0c;文件传输协议&#xff09;以及&#xff08;Trivial File Transfer Protocol&#xff0c;简单文件传输协议&#xff09; FTP采用…...

【UE5 多人联机教程】06-显示玩家名称

效果 可以看到玩家输入各自的名称&#xff0c;会显示到自己控制的角色头上。但是目前有一个BUG就是&#xff0c;当客户端加入游戏时会多创建一个服务端的角色。 步骤 1. 打开“BP_ThirdPersonCharacter”&#xff0c;添加一个控件组件&#xff0c;用于显示玩家名称 作为网格体…...

Rust vs Go:常用语法对比(五)

题图来自 Rust vs Go 2023[1] 81. Round floating point number to integer Declare integer y and initialize it with the rounded value of floating point number x . Ties (when the fractional part of x is exactly .5) must be rounded up (to positive infinity). 按规…...

Flutter 扩展函数项目实用之封装SizedBox

Flutter里扩展函数可以用简化代码写法&#xff0c;关键字为extension&#xff0c;伪代码写法如下&#xff1a; extension 扩展类名 on 扩展类型 { //扩展方法 } 在Flutter页面里实现控件间距会常用到SizedBox&#xff0c;可使用扩展函数封装来达到简化代码的目的&#xff0…...

EMC学习笔记(二十)EMC常用元件简单介绍(二)

EMC常用元件简单介绍&#xff08;二&#xff09; 1.瞬态抑制二极管&#xff08;TVS&#xff09;2.气体放电管3.半导体放电管 电磁兼容性元件是解决电磁干扰发射和电磁敏感度问题的关键,正确选择和使用这些元件是做好电磁兼容性设计的前提。由于每一种电子元件都有它各自的特性,…...

基本排序算法

目录 一&#xff0c;插入排序 二&#xff0c;希尔排序 三&#xff0c;选择排序 四&#xff0c;冒泡排序 五&#xff0c;快排 5.1 Hoare法 5.2 挖坑法 5.3 指针法 5.4 非递归写法 六&#xff0c;归并排序 6.1 递归 6.2 非递归 一&#xff0c;插入排序 基本思想&…...

python调用百度ai将图片/pdf识别为表格excel

python调用百度ai将图片识别为表格excel 表格文字识别(异步接口)图片转excel 表格文字识别V2图片/pdf转excel通用 表格文字识别(异步接口) 图片转excel 百度ai官方文档&#xff1a;https://ai.baidu.com/ai-doc/OCR/Ik3h7y238 使用的是表格文字识别(异步接口)&#xff0c;同步…...

Ansible最佳实践之Playbook管理滚动更新

写在前面 理解不足小伙伴帮忙指正 傍晚时分&#xff0c;你坐在屋檐下&#xff0c;看着天慢慢地黑下去&#xff0c;心里寂寞而凄凉&#xff0c;感到自己的生命被剥夺了。当时我是个年轻人&#xff0c;但我害怕这样生活下去&#xff0c;衰老下去。在我看来&#xff0c;这是比死亡…...

基于Citespace、vosviewer、R语言的文献计量学可视化分析及SCI论文高效写作方法教程

详情点击链接&#xff1a;基于Citespace、vosviewer、R语言的文献计量学可视化分析技术及全流程文献可视化SCI论文高效写作方法 前言 文献计量学是指用数学和统计学的方法&#xff0c;定量地分析一切知识载体的交叉科学。它是集数学、统计学、文献学为一体&#xff0c;注重量…...

【MATLAB】GM(1,1) 灰色预测模型及算法

一、灰色预测模型概念 灰色预测是一种对含有不确定因素的系统进行预测的方法。 灰色预测通过鉴别系统因素之间发展趋势的相异程度&#xff0c;即进行关联分析&#xff0c;并对原始数据进行生成处理来寻找系统变动的规律&#xff0c;生成有较强规律性的数据序列&#xff0c;然后…...

Go重写Redis中间件 - Go实现Redis协议解析器

Go实现Redis协议解析器 Redis网络协议详解 在解决完通信后,下一步就是搞清楚 Redis 的协议-RESP协议,其实就是一套类似JSON、Protocol Buffers的序列化协议,也就是我们的客户端和服务端通信的协议 RESP定义了5种格式 简单字符串(Simple String) : 服务器用来返回简单的结…...

海外抖音Tiktok强势来袭,有些人半年赚别人十倍工资

TikTok作为一款流行的短视频社交应用程序&#xff0c;确实在全球范围内取得了很大的成功。许多人通过在TikTok上分享有趣、创意或有吸引力的视频内容&#xff0c;获得了广泛的关注和认可。一些用户甚至能够通过TikTok赚取高额的收入&#xff0c;远远超过传统职业所能获得的工资…...

devDept Eyeshot 2024 预告-Update-Crack

即将发布的版本 开发商在一个动态的环境中运作&#xff0c;事情可能会发生变化。本页提供的信息旨在概述 devDept 软件产品的总体方向。它仅供参考&#xff0c;不应作为做出任何决定性的依据。devDept Eyeshot 2024软件产品描述的任何特性或功能的开发、发布和时间安排仍由 dev…...

教雅川学缠论05-线段

线段需要满足下面4个条件&#xff1a; 1.是由3条笔&#xff0c;或者3条以上组成&#xff0c;同笔一样&#xff0c;线段也是有方向的 2.如果线段起始于向上笔&#xff0c;则终止与向上笔&#xff08;一定不会终止与向下笔&#xff09; 3.如果线段起始于向下笔&#xff0c;则终止…...

SpringBoot 配置⽂件

1.配置文件作用 整个项⽬中所有重要的数据都是在配置⽂件中配置的&#xff0c;⽐如&#xff1a; 数据库的连接信息&#xff08;包含⽤户名和密码的设置&#xff09;&#xff1b;项⽬的启动端⼝&#xff1b;第三⽅系统的调⽤秘钥等信息&#xff1b;⽤于发现和定位问题的普通⽇…...

基于Python的电影票房爬取与可视化系统的设计与实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…...

Packet Tracer – 配置系统日志和 NTP

Packet Tracer – 配置系统日志和 NTP 目标 第 1 部分&#xff1a;配置系统日志服务 第 2 部分&#xff1a;生成日志记录事件 第 3 部分&#xff1a;手动设置交换机时钟 第 4 部分&#xff1a;配置 NTP 服务 第 5 部分&#xff1a;验证带时间戳的日志 拓扑图 场景 在本…...

TypeScript 联合类型,类型推断,类型断言

联合类型 取值可以为多种类型中的一个 function func(str: number | string):void{}类型断言 当变量需要调用某属性的时候&#xff0c;有不确定当前的类型是什么&#xff0c;可以使用类型断言&#xff1b; 类型断言的两种方式&#xff1a; 1&#xff0c;<类型> 变量名…...

到底叫 集合还是数组还是list还是列表?

1 总体上可以将数据结构分为数组和集合两种&#xff0c;而列表是一个泛指 数组&#xff1a;在Java中&#xff0c;数组是一种基本数据类型&#xff0c;可以用来存储同一类型的多个元素&#xff0c;数组的长度是固定的。例如&#xff1a;int[] arr new int[10];List&#xff1a…...

LBERT论文详解

论文地址&#xff1a;https://arxiv.org/abs/2105.07148 代码地址&#xff1a;https://github.com/liuwei1206/LEBERT 模型创新 LEBRT采用句子中的词语对&#xff08;论文中称为Char-Word Pair&#xff09;的特征作为输入作者设计Lexicon adapter&#xff0c;在BERT的中间某一…...

C++终止cin输入while循环时多读取^Z或^D的问题

原代码&#xff1a; istream& operator>>(istream& is, map<string, int>&mm) {string ss"";int ii0;is >> ss>>ii;mm[ss]ii;return is; }int main() {map<string,int>msi;while(cin>>msi);return 0; } 问题&…...

c#[WebMethod]方法接收前端传入的JsonArray的方法

一、第一种方法&#xff1a;可以这样接收前端传入的jsonArray字符串到一个类的数组中&#xff0c;然后遍历该数组取值 这种方法需要创建PointConfig类 class PointConfig{public string ptcrossing { get; set; }public string ptcrossingId { get; set; }public string camId …...

WebService 报错 集锦

报错1&#xff1a;url错误 我的是调用的url的端口错误。调用esb的url的端口错了&#xff0c;导致报错。有的人是uri错了。例如&#xff1a; www.globalcoding.com:9001/SAP_saveProduct/1.0.0 写成了 www.globalcoding.com:9001/SAP_savePoduct/1.0.0 报错如下&#xff1a;…...

C++--菱形继承

1.什么是菱形继承 单继承&#xff1a;一个子类只有一个直接父类时称这个继承关系为单继承 多继承&#xff1a;一个子类有两个或以上直接父类时称这个继承关系为多继承 菱形继承的问题&#xff1a;菱形继承有数据冗余和二义性的问题&#xff0c;数据冗余是由于创建多个相同类型的…...