Websocket详细介绍
需求背景
在某个资产平台,在不了解需求的情况下,我突然接到了一个任务,让我做某个页面窗口的即时通讯,想到了用websocket技术,我从来没用过,被迫接受了这个任务,我带着浓烈的兴趣,就去研究了一下,网上资料那么多,我们必须找到适合自己的方案,我们开发的时候一定要基于现有框架的基础上去做扩展,不然会引发很多问题,比如:运行不稳定、项目无法启动等,废话不多说,直接上代码
WebScoekt介绍
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

特点
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。 - 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。 - 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
解决方案介绍

PS:基于websocket的特点,我们打算放弃Ajax轮询,因为当客户端过多的时候,会导致消息收发有延迟、服务器压力增大。
API介绍


思路解析
首先,我们既然要发送消息,客户端和客户端是无法建立连接的,我们可以这样做,我们搭建服务端,所有的客户端都在服务端注册会话,我们把消息发送给服务端,然后由服务端转发给其他客户端,这样就可以和其他用户通讯了。
示例代码
服务端配置
package unicom.assetMarket.websocket.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import unicom.assetMarket.websocket.handler.MyMessageHandler;
import unicom.assetMarket.websocket.interceptor.WebSocketInterceptor;/*** @Author 庞国庆* @Date 2023/02/15/15:36* @Description*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {webSocketHandlerRegistry.addHandler(new MyMessageHandler(), "/accept").addInterceptors(new WebSocketInterceptor())//允许跨域.setAllowedOrigins("*");webSocketHandlerRegistry.addHandler(new MyMessageHandler(),"/http/accept").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*").withSockJS();}}
Interceptor
package unicom.assetMarket.websocket.interceptor;import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import java.util.Map;/*** @Author 庞国庆* @Date 2023/02/15/15:52* @Description*/
@Slf4j
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {if (request instanceof ServletServerHttpRequest) {ServletServerHttpRequest request1 = (ServletServerHttpRequest) request;String userId = request1.getServletRequest().getParameter("userId");attributes.put("currentUser", userId);log.info("用户{}正在尝试与服务端建立链接········", userId);}return super.beforeHandshake(request, response, wsHandler, attributes);}@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {super.afterHandshake(request, response, wsHandler, ex);}
}
Handler
package unicom.assetMarket.websocket.handler;import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import unicom.assetMarket.assetChat.service.CamsMarketChatMessageService;import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** @Author 庞国庆* @Date 2023/02/15/15:52* @Description*/
@Slf4j
@Component
public class MyMessageHandler extends TextWebSocketHandler {//存储所有客户端的会话信息(线程安全)private final static Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();@Autowired(required = false)private CamsMarketChatMessageService service;@Overridepublic void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {String userId = this.getUserId(webSocketSession);if (StringUtils.isNotBlank(userId)) {sessions.put(userId, webSocketSession);log.info("用户{}已经建立链接", userId);}}@Overridepublic void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {String message = webSocketMessage.toString();String userId = this.getUserId(webSocketSession);log.info("服务器收到用户{}发送的消息:{}", userId, message);//webSocketSession.sendMessage(webSocketMessage);if (StringUtils.isNotBlank(message)) {//保存用户发送的消息数据service.saveData(message);//发送消息给指定用户doMessage(message);}}@Overridepublic void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {WebSocketMessage message = new TextMessage("发送异常:" + throwable.getMessage());//webSocketSession.sendMessage(message);}@Overridepublic void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {String userId = this.getUserId(webSocketSession);if (StringUtils.isNotBlank(userId)) {sessions.remove(userId);log.info("用户{}已经关闭会话", userId);} else {log.error("没有找到用户{}的会话", userId);}}@Overridepublic boolean supportsPartialMessages() {return false;}/*** 根据会话查找已经注册的用户id** @param session* @return*/private String getUserId(WebSocketSession session) {String userId = (String) session.getAttributes().get("currentUser");return userId;}/*** 发送消息给指定用户** @param userId* @param contents*/public void sendMessageUser(String userId, String contents) throws Exception {WebSocketSession session = sessions.get(userId);if (session != null && session.isOpen()) {WebSocketMessage message = new TextMessage(contents);session.sendMessage(message);}}/*** 接收用户消息,转发给指定用户* @param msg* @throws Exception*/public void doMessage(String msg) throws Exception {JSONObject jsonObject = JSONObject.parseObject(msg);String sendStaffId = jsonObject.getString("sendStaffId");String reciveStaffId = jsonObject.getString("reciveStaffId");String message = jsonObject.getString("message");//替换敏感字message = service.replaceSomething(message);this.sendMessageUser(reciveStaffId,message);}}
JSP代码
<%@ page language="java" pageEncoding="UTF-8" %>
<%@ include file="/WEB-INF/jsp/common/common.jsp" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %><html>
<head><title>聊天窗口</title>
</head>
<body style="background: #f0f3fa">
<div class="dividerBox dividerBox-spinner dividerBox-blue rightBox-blue"><div class="right-container-header"><div class="dividerBox-title"><span>${name}</span></div></div><div class="dividerBox-body rightBox-body"><div class="row"><div class="col-md-4 col-sm-4 padding-r-0"><div class="form-group"><div class="col-md-9 col-sm-8"><div class="data-parent"><input type="text" class="form-control input-sm" id="message" maxlength="200" /><button type="button" class="btn btn-primary btn-sm" onclick="sendMessage()">发 送</button></div></div></div></div></div></div>
</div>
<div class="tableWrapper rightBox-table"><div class="table-content"><ul id="talkcontent"></ul></div>
</div>
<!-- 消息发送者id-->
<input type="hidden" id="sendStaffId" value="${sendStaffId}"/>
<!-- 消息接收者id-->
<input type="hidden" id="reciveStaffId" value="${reciveStaffId}" />
<script src="${prcs}/js/unicom/assetMarket/assetChat/talk.js?time=<%=new Date().getTime() %>"></script>
</body>
JS代码
$(function() {connectWebSocket();
});/*** 和服务器建立链接*/
function connectWebSocket() {let userId = $("#sendStaffId").val();let host = window.location.host;if ('WebSocket' in window) {if (userId) {websocketClient = new WebSocket( "ws://"+host+"/frm/websocket/accept?userId=" + userId);connecting();}}
}function connecting() {websocketClient.onopen = function (event) {console.log("连接成功");}websocketClient.onmessage = function (event) {appendContent(false,event.data);}websocketClient.onerror = function (event) {console.log("连接失败");}websocketClient.onclose = function (event) {console.log("与服务器断开连接,状态码:" + event.code + ",原因:" + event.reason);}
}/*** 发送消息*/
function sendMessage() {if (websocketClient) {let message = $("#message").val();if(message) {let sendMsg = concatMsg(message);sendMsg = JSON.stringify(sendMsg)websocketClient.send(sendMsg);appendContent(true,message);}} else {console.log("发送失败");}
}/*** 在消息框内追加消息* @param flag*/
function appendContent(flag,data){if(flag){$("#talkcontent").append("<li style='float: right'>" + data + "</li><br/>");}else{$("#talkcontent").append("<li style='float: left'>" + data + "</li><br/>");}
}
/*** 组装消息*/
function concatMsg(message) {//发送人let sendStaffId = $("#sendStaffId").val();//接收人let reciveStaffId = $("#reciveStaffId").val();let json = '{"sendStaffId": "' + sendStaffId + '","reciveStaffId": "' + reciveStaffId + '","message": "' + message + '"}';return JSON.parse(json);
}
PS:这里我遇到了1个坑,就是在连接服务端的时候老是连接不上,我们在配置的代码中指定的匹配URL为 /accept,但是我发现就是连不上,后来找了很多资料,原来是忘了加个url,这个url就是我们在web.xml中配置的DispatcherServlet的拦截url,如下:

运行效果如下:

PS:项目框架中配置的过滤器、拦截器都有可能把webscoket建立连接的请求作处理,尤其是权限验证的过滤器,所以记得要对websocket的请求加白名单。
运行效果:

PS:有啥问题,欢迎大家留言,我非常乐意帮助大家解决问题。
相关文章:
Websocket详细介绍
需求背景 在某个资产平台,在不了解需求的情况下,我突然接到了一个任务,让我做某个页面窗口的即时通讯,想到了用websocket技术,我从来没用过,被迫接受了这个任务,我带着浓烈的兴趣,就…...
大数据书单(100本)
大数据书单(100本) 序号 书名 作者 出版社 1 Hadoop权威指南:大数据的存储与分析(第4版)(修订版)(升级版) Tom White 清华大学出版社 2 Hive编程指南 卡普廖洛 (Edward Capriolo) / 万普勒 (Dean Wampler) / 卢森格林 (Jason Rutherglen) / 曹坤 人民邮…...
python实战应用讲解-【语法基础篇】初识Python(附示例代码)
目录 前言 Python基础 基本概念: 为什么使用Python? Python2.x与3.x版本区别...
【2023保研夏令营】网安、CS(西交、华师、科、南等)
文章目录一、基本情况二、投递和入营情况三、考核情况1. 西交软院(面试)2. 川大网安(笔试面试)3. 华东师范数据学院(机试面试)4. 人大信息学院专硕(机试面试,保密)5. 南大…...
Qt COM组件导出源文件
文章目录摘要dumpcpp.exe注册COM组件COM 组件转CPP参考关键字: Qt、 COM、 组件、 源文件、 dumpcpp摘要 由于厂家提供的库不是纯净C库,是基于COM组件开的库,在和厂家友好交流无果下,只能研究下Qt 如何调用,好在Qt 的…...
各数据库数据类型的介绍和匹配
各数据库数据类型的介绍和匹配1. Oracle的数据类型2. Mysql的数据类型3. Sql server的数据类型4. 类型匹配5. Awakening1. Oracle的数据类型 数据类型介绍 VARCHAR2 :可变长度的字符串 最大长度4000 bytes 可做索引的最大长度749; NCHAR :根据字符集而定的固定长度字…...
Rancher 部署 MySQL
文章目录创建 pvc部署 MySQL前置条件:安装 rancher,可参考文章 docker 部署 rancher 创建 pvc MySQL 数据库是需要存储的,所以必须先准备 pvc 创建 pvc 自定义 pvc 名称选择已经新建好的 storageclass,storageclass 的创建可参考…...
Python语言零基础入门教程(二十五)
Python OS 文件/目录方法 Python语言零基础入门教程(二十四) 39、Python os.openpty() 方法 概述 os.openpty() 方法用于打开一个新的伪终端对。返回 pty 和 tty的文件描述符。 语法 openpty()方法语法格式如下: os.openpty()参数 无 返…...
蓝桥杯算法训练合集十五 1.打翻的闹钟2.智斗锅鸡3.文件列表
目录 1.打翻的闹钟 2.智斗锅鸡 3.文件列表 1.打翻的闹钟 问题描述 冯迭伊曼晚上刷吉米多维奇刷的太勤奋了,几乎天天迟到。崔神为了让VonDieEman改掉迟到的坏毛病,给他买了个闹钟。 一天早上,老冯被闹钟吵醒,他随手将闹钟按掉丢…...
CPU扫盲-CPU与指令集
指令集架构就像是特定的CPU的设计图纸,它规定了这个CPU需要支持那些指令、寄存器有那些状态以及输入输出模型。根据指令集结构的设计,在CPU上通过硬件电路进行实现,就得到了支持该指令集的CPU。指令集就像是我们编程语言中的接口,…...
VINS-Mono/Fusion与OpenCV去畸变对比
VINS中没有直接使用opencv的去畸变函数,而是自己编写了迭代函数完成去畸变操作,主要是为了加快去畸变计算速度 本文对二者的结果精度和耗时进行了对比 VINS-Mono/Fusion与OpenCV去畸变对比1 去畸变原理2 代码实现2.1 OpenCV去畸变2.2 VINS去畸变3 二者对…...
jmx prometheus引起的一次cpu飙高
用户接入了jmx agent进行prometheus监控后,在某个时间点出现cpu飙高 排查思路: 1、top,找到java进程ID 2、top -Hp 进程ID,找到java进程下占用高CPU的线程ID 3、jstack 进程ID,找到那个高CPU的线程ID的堆栈。 4、分析堆…...
Android 虚拟 A/B 详解(六) SnapshotManager 之状态数据
本文为洛奇看世界(guyongqiangx)原创,转载请注明出处。 原文链接:https://blog.csdn.net/guyongqiangx/article/details/129094203 Android 虚拟 A/B 分区《AAndroid 虚拟 A/B 分区》系列,更新中,文章列表: Android 虚拟分区详解(一) 参考资料推荐Android 虚拟分区详解(二…...
Python快速入门系列之一:Python对象
Python对象1. 列表(list)2. 元组(tuple)3. 字典(dict)4. 集合(set)5. 字符串(string)6. BIF (Built-in Function)7. 列表、集合以及字…...
【博客626】不同类型的ARP报文作用以及ARP老化机制
不同类型的ARP报文作用以及ARP老化机制 1、ARP协议及报文 2、不同类型的ARP报文作用 3、ARP工作原理 4、ARP老化机制 5、Linux ARP老化机制 ARP状态机: 在上图中,我们看到只有arp缓存项的reachable状态对于外发包是可用的,对于stale状态的…...
nacos discovery和config
微服务和nacos版本都在2.x及之后。1、discovery用于服务注册,将想要注册的服务注册到nacos中,被naocs发现。pom引入的依赖是:yml配置文件中:2、config用于获取nacos配置管理->配置列表下配置文件中的内容pom引入的依赖是&#…...
【算法数据结构体系篇class06】:堆、大根堆、小根堆、优先队列
一、堆结构1)堆结构就是用数组实现的完全二叉树结构2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆4)堆结构的heapInsert与heapify操作5)堆结构的增大ad…...
试题 算法提高 最小字符串
资源限制内存限制:256.0MB C/C时间限制:2.0s Java时间限制:6.0s Python时间限制:10.0s问题描述给定一些字符串(只包含小写字母),要求将他们串起来构成一个字典序最小的字符串。输入格式第一行T,表示有T组数据。接下来T…...
已解决ImportError: cannot import name ‘featureextractor‘ from ‘radiomics‘
已解决from radiomics import featureextractor导包,抛出ImportError: cannot import name ‘featureextractor‘ from ‘radiomics‘异常的正确解决方法,亲测有效!!! 文章目录报错问题报错翻译报错原因解决方法联系博…...
乡村振兴研究:全网最全指标农村经济面板数据(2000-2021年)
数据来源:国家统计局 时间跨度:2000-2021年 区域范围:全国31省 指标说明: 部分样例数据: 行政区划代码地区年份经度纬度乡镇数(个)乡数(个)镇数(个)村民委员会数(个)乡村户数(万户)乡村人口(万人)乡村从业人员(万人…...
使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...
【大模型RAG】Docker 一键部署 Milvus 完整攻略
本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装;只需暴露 19530(gRPC)与 9091(HTTP/WebUI)两个端口,即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...
Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...
UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)
UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中,UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
