【技术派后端篇】整合WebSocket长连接实现消息实时推送
在技术派平台中,实现了文章被点赞或评论后,在右上角实时弹出消息提醒的功能。相较于之前仅有的消息通知红色标识,这种实时通知在交互体验上有显著提升。本文将详细介绍如何借助WebSocket实现消息的实时通知。
1 基础知识点
1.1 相关概念
- WebSocket:WebSocket是一种在单个TCP连接上进行全双工通信的协议,能使客户端和服务器实时地双向传输数据,无需频繁建立和关闭连接,可提高数据传输的效率和性能。
- STOMP:
Simple Text Oriented Messaging Protocol
,即简单文本定向消息协议,是在HTTP之上实现的简单灵活的消息传递协议,定义了一套简单的命令和帧格式用于客户端和服务器间的消息传递,可实现双向通讯。需注意,STOMP协议属于WebSocket的子协议。
1.2 WebSocket整合STOMP
WebSocket整合STOMP协议实现双向通讯的主要步骤如下:
- 建立WebSocket连接:客户端通过JavaScript的WebSocket API与服务器建立连接。
- 发送STOMP帧:连接建立后,客户端和服务器通过发送STOMP帧进行通信,STOMP帧是STOMP协议的基本单位,定义了订阅、发布等消息操作。
- 处理STOMP帧:服务器收到STOMP帧后,根据帧类型进行相应处理,如收到SUBSCRIBE帧为客户端创建订阅,收到SEND帧将消息发送到指定目的地。
- 关闭WebSocket连接:通信完成后,通过调用WebSocket API的close方法关闭连接。
1.3 SpringBoot整合STOMP流程
SpringBoot对WebSocket提供了友好封装,便于搭建基于STOMP协议的WebSocket应用工程。其基本工作流程如下:
-
步骤1:初始化
- 服务端:定义接收WebSocket连接的端点EndPoint;配置消息代理Broker,用于前端订阅,后端向Broker写入消息后,订阅该Broker的前端会收到相应消息;配置路由转发规则,将用户信息转发给相应处理器(类似
RequestMappingHandlerMapping
和@RequestMapping
注解,WebSocket中使用Destination + @MessageMapping
)。 - 客户端:与EndPoint建立连接;订阅Broker并注册消息回调。
- 服务端:定义接收WebSocket连接的端点EndPoint;配置消息代理Broker,用于前端订阅,后端向Broker写入消息后,订阅该Broker的前端会收到相应消息;配置路由转发规则,将用户信息转发给相应处理器(类似
-
步骤2:通讯
- 服务端:主动向Broker写入消息,使用
simpMessagingTemplate
;消息应答使用@SendTo
注解。 - 客户端:发送消息调用
send(xxxx)
方法;消息应答触发订阅的回调函数。
- 服务端:主动向Broker写入消息,使用
2 WebSocket集成
相关示例demo可在https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/203-websocket获取。
2.1 依赖配置
在Spring Boot应用的pom.xml配置文件中,引入WebSocket的核心依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 WebSocket配置
后端通过实现配置类StompConfiguration
来定义相关信息:
@Configuration
@EnableWebSocketMessageBroker
public class StompConfiguration implements WebSocketMessageBrokerConfigurer {// 配置消息代理@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {registry.enableSimpleBroker("/topic"); // 客户端订阅地址前缀registry.setApplicationDestinationPrefixes("/app"); // 服务端接收地址前缀}// 注册STOMP端点@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/ws/hello").withSockJS(); // 客户端连接端点}
}
- 定义端点:使用
registerStompEndpoints()
方法。 - 定义客户端与服务端通讯信息:使用
configureMessageBroker()
方法,配置消息代理registry.enableSimpleBroker
,配置消息转发registry.setApplicationDestinationPrefixes
(转发前缀可为多个)。
配置完成后,客户端的相关路径为:建立连接路径是/ws/hello
;订阅消息路径是/topic/xxx
(由@SendTo
的路径确定);接收前端消息路径是/app/xxx
(由@MessageMapping
中的路径补齐)。
2.3 消息接收应答
实现简单消息应答,接收客户端向/app/hello发送的消息,将结果写回到/topic/hello对应的Broker,订阅该Broker的客户端会收到应答消息。
@Controller
public class HelloController {// 处理/app/hello路径的STOMP消息@MessageMapping("/hello")@SendTo("/topic/hello") // 将返回结果广播到指定主题public String greeting(String message) {return "[" + LocalDateTime.now() + "]: " + message;}
}
同时,编写定时器,由后端定时向/topic/hello这个Broker中写入消息,模拟后台主动下发消息场景。
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;// 定时推送示例(当前被注释)
@Scheduled(cron = "0/10 * * * * ?")
public void autoPush() {simpMessagingTemplate.convertAndSend("/topic/hello", "系统自动消息");
}
2.4 执行流程解析
var stompClient = null;function setConnected(connected) {$("#connect").prop("disabled", connected);$("#disconnect").prop("disabled", !connected);if (connected) {$("#conversation").show();}else {$("#conversation").hide();}$("#greetings").html("");
}function connect() {var socket = new SockJS('/ws/hello');stompClient = Stomp.over(socket);stompClient.connect({}, function (frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/hello', function (greeting) {console.log("resp: ", greeting.body)showGreeting(greeting.body);});});
}function disconnect() {if (stompClient !== null) {stompClient.disconnect();}setConnected(false);console.log("Disconnected");
}function sendName() {stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
}function showGreeting(message) {$("#greetings").append("<tr><td>" + message + "</td></tr>");
}$(function () {$("form").on('submit', function (e) {e.preventDefault();});$("#connect").click(function () {connect();});$("#disconnect").click(function () {disconnect();});$("#send").click(function () {sendName();});
});
核心js逻辑实现WebSocket连接建立与通讯:
- 建立连接connect():通过
new SockJS('/ws/hello')
与后端端点建立连接;连接成功后,使用stompClient.subscribe('/topic/hello', 消息应答回调)
订阅Broker并接收消息回传。 - 发送消息:调用
stompClient.send("/app/hello", xxx)
方法。 - 断开连接:调用
stompClient.disconnect()
方法。
2.5 效果演示
完成基于Spring Boot整合WebSocket与STOMP协议的示例应用搭建后,启动应用,可看到前端建立连接后向后端发送信息并接收后端广播消息的过程,多个客户端订阅同一Broker时,能收到后端发送的消息。
3 技术派基于WebSocket的消息通知
3.1 消息推送配置
WsChatConfig
是一个 Spring 配置类,其主要功能是配置基于 STOMP(Simple Text Oriented Messaging Protocol)协议的 WebSocket 通信。下面按照代码结构详细说明其逻辑:
- 类定义与注解
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WsChatConfig implements WebSocketMessageBrokerConfigurer {
@Slf4j
:Lombok 提供的注解,会自动为该类生成一个日志记录器 log,方便日志输出。@Configuration
:Spring 注解,表明该类是一个配置类,Spring 容器会扫描并加载其中定义的 Bean。@EnableWebSocketMessageBroker
:启用基于消息代理的 WebSocket 支持,允许应用使用 STOMP 协议进行消息传递。implements WebSocketMessageBrokerConfigurer
:实现该接口,需要重写其中的方法来定制 WebSocket 消息代理的配置。
- 配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/chat");config.setApplicationDestinationPrefixes("/app");
}
config.enableSimpleBroker("/chat")
:启用一个简单的基于内存的消息代理,客户端订阅以/chat
开头的目的地后,就可以接收从服务端广播的消息。config.setApplicationDestinationPrefixes("/app")
:设置应用程序的目标前缀,以/app
开头的消息会被路由到带有@MessageMapping
注解的控制器方法进行处理。
- 注册 STOMP 端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/gpt/{id}/{aiType}").setHandshakeHandler(new AuthHandshakeHandler()).addInterceptors(new AuthHandshakeInterceptor()).setAllowedOriginPatterns("*");
}
registry.addEndpoint("/gpt/{id}/{aiType}")
:注册一个 WebSocket 端点,客户端可以通过这个端点连接到服务端。{id}
和{aiType}
是路径变量,可用于标识不同的用户和 AI 类型。.setHandshakeHandler(new AuthHandshakeHandler())
:设置自定义的握手处理器,用于在 WebSocket 握手阶段进行身份验证等操作。.addInterceptors(new AuthHandshakeInterceptor())
:添加自定义的握手拦截器,进一步处理握手过程中的逻辑。.setAllowedOriginPatterns("*")
:允许所有来源的跨域请求,确保不同域名的客户端都能连接到该端点。
- 配置消息通道拦截器
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {registration.interceptors(channelInInterceptor());
}@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {registration.interceptors(channelOutInterceptor());
}
configureClientInboundChannel
:配置客户端发送到服务端的消息通道,添加入站拦截器channelInInterceptor()
,用于处理客户端发来的消息。configureClientOutboundChannel
:配置服务端发送到客户端的消息通道,添加出站拦截器channelOutInterceptor()
,用于处理服务端返回给客户端的消息。
- 定义 Bean
@Bean
public HandshakeHandler handshakeHandler() {return new AuthHandshakeHandler();
}@Bean
public HttpSessionHandshakeInterceptor handshakeInterceptor() {return new AuthHandshakeInterceptor();
}@Bean
public ChannelInterceptor channelInInterceptor() {return new AuthInChannelInterceptor();
}@Bean
public ChannelInterceptor channelOutInterceptor() {return new AuthOutChannelInterceptor();
}
- 使用
@Bean
注解将自定义的握手处理器、握手拦截器、入站拦截器和出站拦截器注册为 Spring Bean,方便 Spring 容器进行管理和使用。
综上所述,WsChatConfig
类通过实现 WebSocketMessageBrokerConfigurer
接口,完成了 WebSocket 端点的注册、消息代理的配置以及消息通道拦截器的设置,从而实现了基于 STOMP 协议的 WebSocket 通信功能,同时加入了身份验证等安全机制。
3.2 身份鉴权拦截器
AuthHandshakeInterceptor
类继承自 HttpSessionHandshakeInterceptor
,主要作用是在 WebSocket 握手阶段进行用户身份验证和识别。下面详细解释其代码逻辑:
- 类定义和继承
public class AuthHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
这行代码定义了 AuthHandshakeInterceptor
类,并继承自 HttpSessionHandshakeInterceptor
。HttpSessionHandshakeInterceptor
是 Spring 提供的用于在 WebSocket 握手过程中处理 HTTP 会话的拦截器,继承它可以利用其已有的功能,同时自定义握手逻辑。
- 握手前的身份验证 (
beforeHandshake
方法)
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {log.info("准备开始握手了!");String session = SessionUtil.findCookieByName(request, LoginService.SESSION_KEY);ReqInfoContext.ReqInfo reqInfo = new ReqInfoContext.ReqInfo();SpringUtil.getBean(GlobalInitService.class).initLoginUser(session, reqInfo);if (reqInfo.getUser() == null) {log.info("websocket 握手失败,请登录之后再试");return false;}// 将用户信息写入到属性中attributes.put(MdcUtil.TRACE_ID_KEY, SelfTraceIdGenerator.generate());attributes.put(LoginService.SESSION_KEY, reqInfo);attributes.put(WsAnswerHelper.AI_SOURCE_PARAM, initAiSource(request.getURI().getPath()));return true;
}
- 日志记录:使用
log.info
记录开始握手的信息。 - 获取会话信息:通过
SessionUtil.findCookieByName
方法从请求中获取名为LoginService.SESSION_KEY
的Cookie
值。 - 初始化用户信息:创建
ReqInfoContext.ReqInfo
对象,并调用GlobalInitService
的initLoginUser
方法,根据会话信息初始化用户信息。 - 身份验证:检查
reqInfo
中的用户信息是否为空,如果为空则记录握手失败的日志并返回false
,表示握手失败。 - 设置属性:如果身份验证通过,将生成的跟踪 ID、用户请求信息和 AI 来源信息添加到
attributes
中,这些信息将在后续的 WebSocket 通信中使用。最后返回true
,表示握手成功。
- 初始化 AI 来源 (
initAiSource
方法)
private String initAiSource(String path) {int index = path.lastIndexOf("/");return path.substring(index + 1);
}
该方法从请求的 URI 路径中提取 AI 来源信息。通过查找最后一个 / 的位置,然后截取该位置之后的字符串作为 AI 来源。
- 握手后的处理 (
afterHandshake
方法)
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {log.info("握手成功了!!!");super.afterHandshake(request, response, wsHandler, ex);
}
在握手成功后,使用 log.info
记录握手成功的日志,并调用父类的 afterHandshake
方法完成后续处理。
综上所述,AuthHandshakeInterceptor
类在 WebSocket 握手过程中进行用户身份验证,确保只有登录用户可以建立 WebSocket 连接,并在握手前后记录日志和处理相关信息。
3.3 握手处理器
AuthHandshakeHandler
类继承自 DefaultHandshakeHandler
,主要用于在 WebSocket 握手过程中确定连接的用户身份。下面详细解释其代码逻辑:
- 类定义和注解
@Slf4j
public class AuthHandshakeHandler extends DefaultHandshakeHandler {
@Slf4j
:Lombok 注解,用于自动生成日志记录器 log,方便在类中记录日志。extends DefaultHandshakeHandler
:继承DefaultHandshakeHandler
类,该类是 Spring 提供的默认握手处理器,AuthHandshakeHandler
可以重写其方法来实现自定义的握手逻辑。
- 重写
determineUser
方法
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {// case1: 根据cookie来识别用户,即可以实现所有用户连相同的ws地址,然后再 AuthHandshakeChannelInterceptor 中进行destination的转发ReqInfoContext.ReqInfo reqInfo = (ReqInfoContext.ReqInfo) attributes.get(LoginService.SESSION_KEY);if (reqInfo != null) {return reqInfo;}// case2: 根据路径来区分用户// 获取例如 ws://localhost/gpt/id 订阅地址中的最后一个用户 id 参数作为用户的标识, 为实现发送信息给指定用户做准备String uri = request.getURI().toString();String uid = uri.substring(uri.lastIndexOf("/") + 1);log.info("{} -> {}", uri, uid);return () -> uid;
}
determineUser
方法是DefaultHandshakeHandler
中的一个受保护方法,用于在握手过程中确定连接的用户身份。重写该方法可以实现自定义的用户身份识别逻辑。- 身份识别方式一:通过
Cookie
识别- 从
attributes
中获取LoginService.SESSION_KEY
对应的值,将其转换为ReqInfoContext.ReqInfo
对象。 - 如果
reqInfo
不为空,则将其作为Principal
返回,表示用户身份已通过Cookie
识别。
- 从
- 身份识别方式二:通过路径识别
- 如果通过
Cookie
未能识别用户身份,则从请求的 URI 中提取最后一个/
之后的部分作为用户 ID。 - 使用 Lambda 表达式创建一个
Principal
对象,其getName()
方法返回提取的用户 ID。 - 记录请求 URI 和提取的用户 ID 的日志信息。
- 如果通过
综上所述,AuthHandshakeHandler
类在 WebSocket 握手过程中,通过两种方式确定连接的用户身份,确保后续的消息可以准确地发送给指定用户。
3.4 消息发送与处理
WsAnswerHelper
是一个 Spring 组件,主要职责是借助 WebSocket 向用户实时推送聊天相关消息,在技术派项目里实现消息实时推送功能。下面对其代码逻辑展开详细说明:
3.4.1 消息发送方法
sendMsgToUser(String session, String question)
public void sendMsgToUser(String session, String question) {ChatRecordsVo res = chatFacade.autoChat(question, vo -> response(session, vo));log.info("AI直接返回:{}", res);
}
- 调用
chatFacade.autoChat
方法发起自动聊天,传入用户提问question
以及一个回调函数。 - 回调函数
vo -> response(session, vo)
会在获取到聊天结果后,调用response
方法把结果推送给指定用户。 - 记录 AI 返回结果的日志。
sendMsgToUser(AISourceEnum ai, String session, String question)
public void sendMsgToUser(AISourceEnum ai, String session, String question) {if (ai == null) {// 自动选择AI类型sendMsgToUser(session, question);} else {ChatRecordsVo res = chatFacade.autoChat(ai, question, vo -> response(session, vo));log.info("AI直接返回:{}", res);}
}
- 若传入的 ai 为
null
,则调用sendMsgToUser(session, question)
方法自动选择 AI 类型进行聊天。 - 若 ai 不为
null
,则调用chatFacade.autoChat
方法,指定 AI 类型进行聊天,并将结果推送给用户,同时记录日志。
sendMsgHistoryToUser(String session, AISourceEnum ai)
public void sendMsgHistoryToUser(String session, AISourceEnum ai) {ChatRecordsVo vo = chatFacade.history(ai);response(session, vo);}
- 调用
chatFacade.history
方法获取指定 AI 类型的聊天历史记录。 - 调用
response
方法将聊天历史记录推送给指定用户。
3.4.2 消息推送方法
/*** 将返回结果推送给用户** @param session* @param response*/public void response(String session, ChatRecordsVo response) {// convertAndSendToUser 方法可以发送信给给指定用户,// 底层会自动将第二个参数目的地址 /chat/rsp 拼接为// /user/username/chat/rsp,其中第二个参数 username 即为这里的第一个参数 session// username 也是AuthHandshakeHandler中配置的 Principal 用户识别标志simpMessagingTemplate.convertAndSendToUser(session, "/chat/rsp", response);}
- 借助 SimpMessagingTemplate 的
convertAndSendToUser
方法,把聊天结果response
推送给指定用户 session。 - 目标地址
/chat/rsp
会自动拼接成/user/username/chat/rsp
,这里的username
就是 session。
3.4.3 业务逻辑执行方法
public void execute(Map<String, Object> attributes, Runnable func) {try {ReqInfoContext.ReqInfo reqInfo = (ReqInfoContext.ReqInfo) attributes.get(LoginService.SESSION_KEY);ReqInfoContext.addReqInfo(reqInfo);String traceId = (String) attributes.get(MdcUtil.TRACE_ID_KEY);MdcUtil.add(MdcUtil.TRACE_ID_KEY, traceId);// 执行具体的业务逻辑func.run();} finally {ReqInfoContext.clear();MdcUtil.clear();}}
- 从
attributes
里获取用户请求信息reqInfo
和跟踪 IDtraceId
,并分别添加到ReqInfoContext
和MdcUtil
中。 - 执行传入的业务逻辑
func.run()
。 - 无论业务逻辑是否执行成功,最后都要清除
ReqInfoContext
和MdcUtil
中的信息。
综上所述,WsAnswerHelper
类整合了聊天服务和 WebSocket 消息推送功能,实现了消息的实时发送和推送,同时对请求信息和跟踪 ID 进行管理。
3.5 前端页面集成
在前端页面 paicoding-ui/src/main/resources/templates/views/chat-home/index.html
中,可以使用 JavaScript 代码来连接 WebSocket 并接收消息。
3.5.1 连接 WebSocket
<script th:inline="javascript">// ... 其他代码 ...const isLogin = [[${global.isLogin}]], user = [[${global.user}]];let stompClient = null;$(function () {if (isLogin) {initWs();} else {console.log("请先登录");}});function initWs() {let protocol = window.location.protocol.replace("http", "ws");let host = window.location.host;let aiType = $('#chat-type').val();const session = getCookie("f-session");let socket = new WebSocket(`${protocol}//${host}/gpt/${session}/${aiType}`);stompClient = Stomp.over(socket);stompClient.connect({}, function (frame) {console.log('ws连接成功: ' + frame);// 其他操作...});// 关闭链接socket.onclose = disconnect;}// ... 其他代码 ...
</script>
- 登录状态判断:借助 Thymeleaf 模板引擎获取后端传递的登录状态
isLogin
和用户信息user
。页面加载完成后,若用户已登录,则调用initWs
方法来初始化 WebSocket 连接;反之,在控制台输出提示信息。 - 构建 WebSocket 地址:
initWs
方法先把当前页面的协议从http
替换成ws
,接着获取当前页面的主机名,再获取下拉框选中的 AI 类型aiType
以及用户的会话session
,最后构建出完整的 WebSocket 连接地址。 - 创建 WebSocket 连接:使用
new WebSocket
创建 WebSocket 连接,再用Stomp.over
方法将其包装成基于 STOMP 协议的客户端。 - 连接服务器:调用
stompClient.connect
方法连接服务器,连接成功后会执行回调函数,在控制台输出连接成功信息。 - 处理关闭事件:为 WebSocket 的
onclose
事件绑定disconnect
方法,当连接关闭时会触发该方法。
3.5.2 订阅消息
<script th:inline="javascript">// ... 其他代码 ...function initWs() {// ... 其他代码 ...stompClient.connect({}, function (frame) {console.log('ws连接成功: ' + frame);// 订阅消息stompClient.subscribe(`/user/chat/rsp`, function (message) {// 解析 JSON 字符串console.log("rsp:", message);let res = JSON.parse(message.body);console.log("res:", res);// 记录聊天次数$("#chatCnt").html(` ${res.usedCnt}/${res.maxCnt} `);const data = res.records;// 移除 loading 元素$(".home_chat-message-actions__loading").remove();if (data.length > 1) {// 返回历史全部信息chatContent.html('');for (let i = data.length - 1; i >= 0; i--) {if (data[i].question) {addClientMsg(data[i].question, false);}if (i == 0) {addSplit();}appendServerMessage(data[i]);}} else {appendServerMessage(data[0]);}// 除流式持续返回场景,恢复按钮状态if(data[data.length - 1].answerType != 'STREAM') {sendBtn.removeAttr("disabled");}});});// ... 其他代码 ...}// ... 其他代码 ...
</script>
- 订阅目的地:在
stompClient.connect
的回调函数里,调用stompClient.subscribe
方法订阅/user/chat/rsp
目的地,该目的地是服务器推送消息的目标地址。 - 处理接收到的消息:当服务器向该目的地推送消息时,会触发回调函数。在回调函数中,先将接收到的消息体解析成 JSON 对象,然后更新页面上的聊天次数,移除加载中的提示元素,根据消息数据的长度不同,采用不同的方式将消息添加到聊天界面,最后根据消息类型恢复发送按钮的状态。
3.5.3 发送消息
<script th:inline="javascript">// ... 其他代码 ...function doSend() {const qa = inputField.val();if (qa.length > 512) {toastr.info("提问长度请不要超过512字符哦~");return;}stompClient.send("/app/chat/" + session, {'s-uid': session}, qa);inputField.val("");addClientMsg(qa.replace(/\n/g, "<br/>"), true);sendBtn.attr("disabled", true);}sendBtn.click(function () {if (stompClient == null) {initWs();} else {if (inputField.val() === "") {inputField.focus();} else {doSend();}}});inputField.keydown(function (e) {if (e.keyCode === 13) {sendBtn.click();e.preventDefault();}});// ... 其他代码 ...
</script>
doSend
方法:获取输入框中的内容,若内容长度超过 512 字符,则给出提示并返回;否则,调用stompClient.send
方法将消息发送到/app/chat/
加上会话 ID 的地址,同时传递自定义头信息{'s-uid': session}
。发送消息后,清空输入框,在聊天界面添加用户消息,并禁用发送按钮。- 按钮点击事件:为发送按钮绑定点击事件,若
stompClient
为空,则调用initWs
方法初始化连接;否则,若输入框内容为空,则将焦点聚焦到输入框,反之调用doSend
方法发送消息。 - 回车键事件:为输入框绑定按键事件,当按下回车键时,触发发送按钮的点击事件,并阻止回车键的默认行为。
综上所述,前端代码通过上述步骤实现了 WebSocket 的连接、消息的订阅和发送功能,从而与服务器进行实时通信。
4 小结
本文聚焦WebSocket的应用,涵盖其基础概念、Spring整合STOMP的流程图、Spring Boot应用集成WebSocket实现双向通信,以及技术派中基于WebSocket实现消息推送的全过程。
5 参考链接
- 技术派整合WebSocket实现消息实时推送
- 项目仓库(GitHub)
相关文章:
【技术派后端篇】整合WebSocket长连接实现消息实时推送
在技术派平台中,实现了文章被点赞或评论后,在右上角实时弹出消息提醒的功能。相较于之前仅有的消息通知红色标识,这种实时通知在交互体验上有显著提升。本文将详细介绍如何借助WebSocket实现消息的实时通知。 1 基础知识点 1.1 相关概念 W…...
Janus Pro
目录 一、模型概述与开源情况 二、模型能力与性能 三、竞品分析 四、部署成本与个人部署成本比较 五、其他维度比较 1. 模型架构与创新性 2. 社区支持与生态系统 3. 更新频率与维护 4. 适用场景与灵活性 5. 商业化潜力 六、总结 Janus Pro 是中国初创公司 DeepSeek …...
[密码学实战]在Linux中实现SDF密码设备接口
[密码学实战]在Linux中实现SDF密码设备接口 引言 在密码学应用开发中,SDF(Security Device Interface)作为中国国家密码管理局制定的密码设备接口标准,被广泛应用于金融、政务等领域的安全系统中。本文将以GmSSL国产密码库为基础,手把手指导在Linux系统中部署SoftSDF——…...
机器学习基础 - 分类模型之SVM
SVM:支持向量机 文章目录 SVM:支持向量机简介基础准备1. 线性可分2. 最大间隔超平面3. 什么是支持向量?4. SVM 能解决哪些问题?5. 支持向量机的分类硬间隔 SVM0. 几何间隔与函数间隔1. SVM 最优化问题2. 对偶问题1. 拉格朗日乘数法 - 等式约束优化问题2. 拉格朗日乘数法 - …...
PostgreSQL 中的权限视图
PostgreSQL 中的权限视图 PostgreSQL 提供了多个系统视图来查询权限信息,虽然不像 Oracle 的 DBA_SYS_PRIVS 那样集中在一个视图中,但可以通过组合以下视图获取完整的系统权限信息。 一 主要权限相关视图 Oracle 视图PostgreSQL 对应视图描述DBA_SYS_…...
pnpm install报错:此系统上禁止运行脚本
依赖安装 报错信息: pnpm : 无法加载文件 C:\Users\XXX\AppData\Roaming\npm\pnpm.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。 所在位置 行:1 …...
解决yarn install 报错 error \node_modules\electron: Command failed.
在电脑重装系统后,重新安装项目依赖,遇到这一报错 完整报错信息如下: error D:\xxxxx\xxxxxx\node_modules\electron: Command failed. Exit code: 1 Command: node install.js Arguments: Directory: D:\xxxxx\xxxxx\node_modules\electron Output: HTTPError: Response cod…...
深度学习3.7 softmax回归的简洁实现
import torch from torch import nn from d2l import torch as d2lbatch_size 256 train_iter, test_iter d2l.load_data_fashion_mnist(batch_size)3.7.1 初始化模型参数 net nn.Sequential(nn.Flatten(), nn.Linear(784, 10))def init_weights(m):if type(m) nn.Linear:…...
Linux424 chage密码信息 gpasswd 附属组
https://chat.deepseek.com/a/chat/s/e55a5e85-de97-450d-a19e-2c48f6669234...
Spring Boot单元测试实战指南:从零到高效测试
在Spring Boot开发中,单元测试是保障代码质量的核心环节。本文将基于实际开发场景,手把手教你如何快速实现分层测试、模拟依赖、编写高效断言,并分享最佳实践! 一、5分钟环境搭建 添加依赖 在pom.xml中引入spring-boot-starter-te…...
Netty线上如何做性能调优?
大家好,我是锋哥。今天分享关于【Netty线上如何做性能调优?】面试题。希望对大家有帮助; Netty线上如何做性能调优? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在使用 Netty 进行线上服务时,性能调优是…...
总结-SQL注入分类手法注入漏洞总结性分化说明
目录 关键要点 按参数类型分类 按提交方式分类 按反馈结果分类 其他高级注入类型 最新漏洞动态 防御措施 调查笔记:SQL注入类型与详细分析 一、按参数类型分类 二、按提交方式分类 三、按反馈结果分类 四、其他高级注入类型 五、最新漏洞动态 六、防御…...
Linux:进程的创建进程的终止
进程的创建 fork fork是c语言中的一个函数,用于创建新的子进程,它存放在<unistd.h>的头文件中 当我们运行程序时,如果使用了fork函数那么就会为这个进程创建一个它的子进程,这个子进程会继承父进程的所有数据和代码&…...
[C#]反射的实战应用,实际数据模拟
long? value null; // 看它是不是 HEX_STRING var dtAttr prop.GetCustomAttribute<DataTypeAttribute>(); if (dtAttr ! null && dtAttr.DataType DataType.HEX_STRING) {// 去掉可能的 "0x" 前缀string txt attribute.Value.StartsWith("0…...
机器人灵巧手有刷与无刷空心杯电机解析
一、电机结构分析 (一)有刷空心杯电机结构 有刷空心杯电机主要由上壳、碳刷、连接板、换向器线圈、外壳、轴承、永磁体、下壳及轴承密封圈组成。碳刷与换向器直接接触,负责传导电流,使线圈在永磁体产生的磁场中受力转动。这种机械换向方式虽直接,但碳刷磨损会影响电机寿命…...
JetBrains GoLang IDE无限重置试用期,适用最新2025版
注意本文仅用于学习使用!!! 本文在重置2024.3.5版本亲测有效,环境为window(mac下应该也一样奏效) 之前eval-reset插件只能在比较低的版本才能起作用。 总结起来就一句:卸载重装,额外要删掉旧安装文件和注册…...
【网络应用程序设计】实验四:物联网监控系统
个人博客:https://alive0103.github.io/ 代码在GitHub:https://github.com/Alive0103/XDU-CS-lab 能点个Star就更好了,欢迎来逛逛哇~❣ 主播写的刚够满足基本功能,多有不足,仅供参考,还请提PR指正ÿ…...
第六章:安全最佳实践
Chapter 6: 安全最佳实践 🌟 从上一章到本章 在第五章:框架/工具抽象中,我们学会了如何用框架快速搭建MCP服务器。现在想象这样一个场景:你的文件服务器已经开发完成,但突然发现恶意用户能通过路径遍历攻击访问系统文…...
最高支持高速L3商用,华为发布ADS 4智驾系统
作者 |张马也 编辑 |德新 4月22日,华为在上海召开乾崑智能技术大会。 会上,华为正式推出乾崑智驾ADS 4、鸿蒙座舱HarmonySpace 5、乾崑车控XMC数字底盘引擎等一系列智能汽车解决方案。 其中最为重磅的是,华为正式发布高速L3商用解决方案&a…...
[创业之路-382]:企业法务 - 企业如何通过技术专利与技术秘密保护自己
企业通过技术专利与技术秘密保护自身创新成果是构建核心竞争力的关键策略。以下从技术专利和技术秘密两大维度,系统阐述其保护路径及实施要点: 一、技术专利保护策略 1. 专利布局规划 核心专利:针对核心技术进行专利申请,构建基…...
多路转接epoll原理详解
目录 从epoll接口入手 创建epoll模型 用户告诉内核关心的事件 内核告诉用户就绪的事件 epoll的原理 整体思路 如何判断事件是否就绪 事件就绪后如何实现将节点插入就绪队列 从epoll接口入手 本篇文章从epoll的三个接口入手介绍epoll的具体工作原理 创建epoll模型 #in…...
基于 MCP用 Python 搭建 “大模型网关”在 MCP 服务器端聚合多个大模型的 API,将其统一为 MCP 协议接口
下面给出基于 MCP(Model-Connection-Protocol)设计思想,用 Python 搭建 “大模型网关” 的典型开发流程。整体思路是:在 MCP 服务器端聚合多个大模型的 API,将其统一为 MCP 协议接口;在客户端按需调用这些统一后的接口。总结如下: 概要: 需求与架构定位:Clarify 要接入…...
Linux的时间函数
ucos中有systick这个系统时间滴答,那linux中有没有这种系统时间滴答呢?有,jiffies,但是用户空间不可以使用。那么在linux中除了使用timer定时器进行定时,可以通过时间滴答的方式来进行粗略的计时吗?下面介绍…...
JCE cannot authenticate the provider BC
本地使用了加密类、并且运行正常、 用hutool做RSA加密时候出现这个问题的! import cn.hutool.core.codec.Base64; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SmUtil; import…...
4.1 融合架构设计:LLM与Agent的协同工作模型
大型语言模型(Large Language Models, LLMs)与智能代理(Agent)的融合架构已成为人工智能领域推动企业智能化的核心技术。这种协同工作模型利用LLM的语言理解、推理和生成能力,为Agent提供强大的知识支持,而…...
【Spec2MP:项目管理之项目风险管理】
在半导体行业竞争白热化的今天,一颗芯片从设计到量产的旅程犹如跨越重重险峰。据行业数据显示,30%的芯片项目因未及时识别风险导致延期交付,而55%的成本超支源于前期风险评估不足。这背后折射出一个核心命题:如何在复杂的技术攻关…...
【Axure教程】表格嵌套卡片
今天教大家制作表格嵌套卡片的原型模版,可以点击加号或减号展开或收起对应部门下的员工卡片信息。这个表格是用中继器制作的,所以使用也很方便,在中继器表格里维护数据,即可自动生成交互效果,具体效果可以打开下方原型…...
无人机动力核心测评:CKESC STONE 180A-M 电调
一、核心技术优势:全场景适配的智能控制方案 作为南昌长空科技的工业级产品,南昌长空的STONE 180A-M 电调以高可靠启动算法为核心,支持 6-14S 锂电输入,具备逆风启动稳定性与剧变油门抗丢相能力,实测油门响应时间 300…...
【回眸】Aurix TC397 IST 以太网 UDP 相关开发
前言 关于移植IST功能至 Infineon TC397上主要涉及到UDP发送报文及接收。IST是安全诊断相关的工作 Nvidia IST介绍 Orin系列芯片会提供一种机制来检测由系统内测(IST)所产生的永久的故障,IST 应该在 Orin-x 功能安全系统中使用期间被启动。…...
C语言别踩白块附源码
复制即可使用 #define _CRT_SECURE_NO_WARNINGS//一定要放在第一行 #include<stdio.h>//引用输入输出头文件,每一次都需要引用这个文件 #include<math.h> #include<string.h> #include<ctype.h> #include<stdlib.h> #include<io…...
centos7里memcached 的安装使用
memcahced 的概述 Memcached是一个自由开源的,高性能,分布式内存对象缓存系统。 Memcached是以LiveJournal旗下Danga Interactive公司的Brad Fitzpatric为首开发的一款软件。现在已成为mixi、hatena、Facebook、Vox、LiveJournal等众多服务中提高Web应…...
深入理解指针(3)
1.指针的使⽤和传址调⽤ 1.strlen的模拟实现 库函数strlen的功能是求字符串⻓度,统计的是字符串中\0 之前的字符的个数。函数原型如下: 参数str接收⼀个字符串的起始地址,然后开始统计字符串中 \0 之前的字符个数,最终返回⻓度。…...
第十届电气、电子和计算机工程研究国际学术研讨会(ISAEECE 2025)
重要信息 官网:www.isaeece.com(点击了解参会投稿等) 时间:2025年6月20-22日 地点:中国 ▪ 西安 征稿主题 电气、电子和计算机工程(Electrical, Electronics and Computer Engineering, EECE)…...
RabbitMQ 中的队列声明
目录 一、为什么要声明队列?二、声明队列的基本语法参数说明 三、声明队列的示例代码示例 1:声明一个普通的队列示例 2:声明一个持久化队列示例 3:声明一个带 TTL 的队列 四、注意事项五、总结 在 RabbitMQ 中,队列是消…...
unity Animation学习,精准控制模型动画播放
unity 控制模型动画播放,Animation学习。 此脚本挂载在带有动画的模型上。 using System.Collections; using System.Collections.Generic; using UnityEngine;public class AnimationCtrl : MonoBehaviour {void Start(){PlayAnimation();//开始的时候调用播放动…...
大模型面经 | 春招、秋招算法面试常考八股文附答案(六)
大家好,我是皮先生!! 今天给大家分享一些关于大模型面试常见的面试题,希望对大家的面试有所帮助。 往期回顾: 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题一) 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题二) 大模型面经 | 春招、秋招算法…...
【深度学习】#9 现代循环神经网络
主要参考学习资料: 《动手学深度学习》阿斯顿张 等 著 【动手学深度学习 PyTorch版】哔哩哔哩跟李牧学AI 概述 门控循环单元和长短期记忆网络利用门控机制实现对序列输入的选择性记忆。深度循环神经网络堆叠多个循环神经网络层以实现更强的表达能力和特征提取能力。…...
《CBOW 词向量转化实战:让自然语言处理 “读懂” 文字背后的含义》
文章目录 前言一、自然语言模型统计语言模型存在的问题总结:这两个问题的本质,第一个是"容量问题":模型记忆力有限;第二个是"理解力问题":模型缺乏抽象能力。 二、词向量转换1.onehot编码编码过程…...
网络变更:APIC 节点替换
Draft 一、同版本硬件更换 1. 查看 APIC 状态 System > Controllers > (any APIC) > Cluster APIC1> acidiag avread // APIC 参数 2. 下线故障设备 Actions > Decommission 3. 物理移除故障设备,连接目标 APIC 4. 根据第一步中的配置参数配置目…...
Java在excel中导出动态曲线图DEMO
1、环境 JDK8 POI 5.2.3 Springboot2.7 2、DEMO pom <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.3</version></dependency><dependency><groupId>commons…...
Python爬虫爬取图片并存储到MongoDB(注意:仅尝试存储一条空的示例数据到MongoDB,验证MongoDB的联通性)
以下是一个使用Python爬取图片并存储到MongoDB的示例实现,包含详细步骤说明: import requests from bs4 import BeautifulSoup from pymongo import MongoClient from datetime import datetime import os import re# 配置信息 mongoIP mongodb://root…...
Qt —— 在Linux下试用QWebEngingView出现的Js错误问题解决(附上四种解决办法)
错误提示:js: A parser-blocking, cross site (i.e. different eTLD+1) script, https:xxxx, is invoked via document.write. The network request for this script MAY be blocked by the browser in this or a future page load due to poor network connectivity. If bloc…...
240424 leetcode exercises II
240424 leetcode exercises II jarringslee 文章目录 240424 leetcode exercises II[148. 排序链表](https://leetcode.cn/problems/sort-list/)🔁分治 & 归并排序法1. 找中点并断开2. 合并两个有序链表3. 主函数:递归拆分与合并 [24. 两两交换链表…...
STM32实现2小时延时的最佳方法探讨
在嵌入式系统开发中,特别是使用STM32这类微控制器时,实现精确的长时间延时是一项常见但具有挑战性的任务。延时的方法选择不仅影响系统的性能和功耗,还关系到系统的稳定性和可靠性。本文将探讨在STM32上实现2小时延时的几种方法,并…...
G3学习笔记
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 准备工作 import torch import numpy as np import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torc…...
初识Redis · 主从复制(上)
目录 前言: 主从模式 模拟主从模式 连接信息 slaveof命令 nagle算法 Nagle算法的工作原理: 具体实现: 优点: 缺点: 使用场景: 拓扑结构 前言: 主从复制这里算得上是一个大头了&…...
欧拉计划 Project Euler55(利克瑞尔数)题解
欧拉计划 Project Euler 55 题解 题干思路code 题干 思路 直接暴力找即可,若使用其他语言要注意溢出的问题,这里我使用的手写大数加法 code // 249 #include <bits/stdc.h>using namespace std;using ll long long;string add(const string&am…...
关于nginx,负载均衡是什么?它能给我们的业务带来什么?怎么去配置它?
User 关于nginx,我还想知道,负载均衡是什么?它能为我的业务带来什么?怎么去配置它? Assistant 负载均衡是 Nginx 另一个非常强大的功能,也是构建高可用、高性能应用的关键技术之一。我们来详细了解一下。 …...
【项目管理】进度网络图 笔记
项目管理-相关文档,希望互相学习,共同进步 风123456789~-CSDN博客 (一)知识总览 项目管理知识域 知识点: (项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域) 对应&…...
【C++QT】Buttons 按钮控件详解
文章目录 一、QPushButton 基础按钮控件二、QToolButton 轻量工具按钮控件三、QRadioButton 互斥选择控件四、QCheckBox 状态选择控件五、QCommandLinkButton 引导式按钮控件六、QDialogButtonBox 对话框按钮布局控件七、实践与选型建议八、总结如果这篇文章对你有所帮助&#…...