WebSocket 是一种在单个TCP 连接上进行全双工 通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
参考原文:
websocket 实现后端主动前端推送数据、及时通讯(vue3 + springboot)
Vue3和SpringBoot实现Web实时消息推送
即时通讯,服务端主动推送数据 后端主动推送数据给前端的核心实现方案包括WebSocket、Server-Sent Events(SSE)、长轮询(Long Polling)和HTTP/2 Server Push。这些方案适用于不同场景,选择需综合考虑实时性要求、兼容性和实现复杂度。
主流实现方案对比
方案
协议基础
通信模式
延迟
实现复杂度
适用场景
WebSocket
独立TCP协议
全双工双向通信
低
较高
实时聊天、高频数据监控(如股票)
Server-Sent Events
HTTP
单向服务器推送
中等
较低
新闻推送、日志实时更新
长轮询
HTTP
半双工轮询
较高
低
兼容老旧浏览器、低频率更新场景
核心方案技术解析 WebSocket
实现原理: 通过一次HTTP握手建立持久化TCP连接,支持服务端与客户端双向通信
代码示例: Spring Boot中可通过@ServerEndpoint注解快速搭建服务端,前端通过WebSocket对象建立连接
优势: 低延迟、高吞吐量,适合实时交互场景
局限性: 需处理连接状态维护,不支持HTTP/1.0以下协议
Server-Sent Events (SSE)
实现原理: 基于HTTP长连接的单向推送,客户端通过EventSource监听事件流
代码示例: Java中使用SseEmitter发送事件流,前端通过addEventListener接收数据
优势: 天然支持断线重连,兼容现有HTTP基础设施
局限性: 仅支持文本数据传输,无法实现客户端主动请求
长轮询优化策略
实现原理: 客户端发起请求后,服务端阻塞直到数据更新或超时,减少无效请求
代码示例: Node.js通过维护回调队列实现延迟响应,前端循环调用接口
优势: 兼容性最佳,适用于简单场景
局限性: 高并发时服务器资源消耗较大
选型建议
强实时性场景 (如在线游戏、IM):优先选择WebSocket
单向数据同步场景 (如价格更新、通知推送):SSE更轻量高效
兼容性优先场景 :长轮询或结合HTTP/2 Server Push(需注意浏览器支持度)
WebSocket WebSocket 是一种全双工通信协议,用于在 Web 浏览器和服务器之间建立持久的连接。WebSocket 协议由 IETF 定为标准,WebSocket API 由 W3C 定为标准。一旦 Web 客户端与服务器建立连接,之后的全部数据通信都通过这个连接进行。可以互相发送 JSON、XML、HTML 或图片等任意格式的数据 。
WebSocket 与 HTTP 协议的异同 相同点
都是基于 TCP 的应用层协议 。
都使用 Request/Response 模型进行连接的建立。
可以在网络中传输数据。
不同点
WebSocket 使用 HTTP 来建立连接,但定义了一系列新的 header 域,这些域在 HTTP 中并不会使用。
WebSocket 支持持久连接 ,而 HTTP 协议不支持持久连接。
WebSocket 的优缺点 优点
高效性: 允许在一条 WebSocket 连接上同时并发多个请求,避免了 传统 HTTP 请求的多个 TCP 连接 。WebSocket 的长连接特性提高了效率,避免了 TCP 慢启动和连接握手的开销 。
节省带宽: HTTP 协议的头部较大,且请求中的大部分头部内容是重复的。WebSocket 复用长连接 ,避免了这一问题。
服务器推送: WebSocket 具备跨域通信 的能力,可以跨域进行实时通信,提供了低延迟的实时通信能力 ,能够在服务器端有新数据时立即推送给客户端。支持客户端和服务器之间的双向通信 ,可以实现实时聊天、实时数据更新等场景
缺点
长期维护成本: 服务器需要维护长连接,成本较高。
浏览器兼容性: 不同浏览器对 WebSocket 的支持程度不一致。
受网络限制: WebSocket 是长连接,受网络限制较大,需要处理好重连。
WebSocket 应用场景 实时通信领域(实时聊天应用、视频会议/聊天、社交聊天弹幕)、股票行情推送、体育实况更新、实时协作编辑、多人游戏、实时数据监控、智能家居、基于位置的应用、在线教育等需要实时双向通信的场景 。
WebSocket 的连接建立过程
客户端发送 WebSocket 握手请求
服务器收到握手请求后,验证请求头的字段,并返回握手响应
客户端收到握手响应后,验证响应头的字段,并生成一个 Sec-WebSocket-Accept 值进行验证
验证通过后,WebSocket 连接建立成功,客户端和服务器可以开始进行实时通信
如何处理错误和关闭连接 WebSocket 在出现错误时会触发 error 事件 ,可以通过设置 onerror 事件处理函数来处理错误。 例如:
1 2 3 socket.onerror = function (error ) { console .error ('WebSocket 错误:' , error); };
当 WebSocket 连接关闭时,会触发 close 事件 ,可以通过设置 onclose 事件处理函数 来执行一些清理操作或重新连接等操作,可以通过调用 close() 方法来显式地关闭 WebSocket 连接
实际应用中,如何处理连接状态的变化和重连机制 在 onopen 事件中,使用 setInterval 方法定时发送心跳数据包。如每 5000 毫秒发送一次心跳数据包 ,如下所示代码:
1 2 3 4 5 ws.onopen = function ( ){ heartcheck = setInterval (function ( ){ ws.send ('HeartBeat' ); },5000 ); };
在onmessage事件中,当接收到服务器返回的心跳响应或其他消息时,可以重置心跳定时器 ,以避免不必要的心跳发送。例如:
1 2 3 4 5 6 ws.onmessage = function ( ){ clearInterval (heartcheck); heartcheck=setInterval (function ( ){ ws.send ('HeartBeat' ); }, 5000 ); };
在onclose和onerror事件中,需要清除心跳定时器 ,以避免在连接关闭后继续发送心跳数据包。例如:
1 2 3 4 5 6 7 ws.onclose = function ( ){ clearInterval (heartcheck); }; ws.onerror = function ( ){ clearInterval (heartcheck); }
WebSocket实时通信(vue3 + springboot) 后端代码(SpringBoot) 安装核心jar包: spring-boot-starter-websocket 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 5.1.0</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 2.0.22</version > </dependency > </dependencies >
新建配置类注入 1 2 3 4 5 6 7 8 9 10 11 import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WebSocketConfig2 { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
写一个基础webSocket服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.concurrent.ConcurrentHashMap;@ServerEndpoint("/dev-api/websocket/{userId}") @Component public class WebSocketServer { static Log log = LogFactory.get(WebSocketServer.class); private static int onlineCount = 0 ; private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap <>(); private Session session; private String userId = "" ; @OnOpen public void onOpen (Session session, @PathParam("userId") String userId) { this .session = session; this .userId = userId; if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId); webSocketMap.put(userId, this ); } else { webSocketMap.put(userId, this ); addOnlineCount(); } log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount()); try { sendMessage("连接成功" ); } catch (IOException e) { log.error("用户:" + userId + ",网络异常!!!!!!" ); } } @OnClose public void onClose () { if (webSocketMap.containsKey(userId)) { webSocketMap.remove(userId); subOnlineCount(); } log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount()); } @OnMessage public void onMessage (String message, Session session) { log.info("用户消息:" + userId + ",报文:" + message); if (! StringUtils.isEmpty(message)) { try { JSONObject jsonObject = JSON.parseObject(message); } catch (Exception e) { e.printStackTrace(); } } } @OnError public void onError (Session session, Throwable error) { log.error("用户错误:" + this .userId + ",原因:" + error.getMessage()); error.printStackTrace(); } public void sendMessage (String message) throws IOException { this .session.getBasicRemote().sendText(message); } public static void sendAllMessage (String message) throws IOException { ConcurrentHashMap.KeySetView<String, WebSocketServer> userIds = webSocketMap.keySet(); for (String userId : userIds) { WebSocketServer webSocketServer = webSocketMap.get(userId); webSocketServer.session.getBasicRemote().sendText(message); System.out.println("webSocket实现服务器主动推送成功userIds====" + userIds); } } public static void sendInfo (String message, @PathParam("userId") String userId) throws IOException { log.info("发送消息到:" + userId + ",报文:" + message); if (!StringUtils.isEmpty(message) && webSocketMap.containsKey(userId)) { webSocketMap.get(userId).sendMessage(message); } else { log.error("用户" + userId + ",不在线!" ); } } public static synchronized int getOnlineCount () { return onlineCount; } public static synchronized void addOnlineCount () { WebSocketServer.onlineCount++; } public static synchronized void subOnlineCount () { WebSocketServer.onlineCount--; } }
测试类用于推送数据 定时向客户端推送数据或者可以发起请求推送
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController @RequestMapping("/money") public class Test { @Scheduled(cron = "0/10 * * * * ?") @PostMapping("/send") public String sendMessage () throws Exception { Map<String,Object> map = new HashMap <>(); LocalDateTime nowDateTime = LocalDateTime.now(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); System.out.println(dateTimeFormatter.format(nowDateTime)); map.put("server_time" ,dateTimeFormatter.format(nowDateTime)); map.put("server_code" ,"200" ); map.put("server_message" , "这是服务器推送到客户端的消息哦!!" ); JSONObject jsonObject = new JSONObject (map); WebSocketServer.sendAllMessage(jsonObject.toString()); return jsonObject.toString(); } }
启动springboot 1 2 3 4 5 6 7 8 9 10 11 12 13 import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.servlet.ServletComponentScan;import org.springframework.scheduling.annotation.EnableScheduling;@EnableScheduling @ServletComponentScan @SpringBootApplication public class WebSocketAppMain { public static void main (String[] args) { SpringApplication.run(WebSocketAppMain.class); } }
可以使用网上的测试工具试试:http://coolaf.com/tool/chattest 或者http://www.jsons.cn/websocket/
前端代码(vue3+WebSocket) 简单的WebSocket公共类 需求:commentUtil/WebsocketTool.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 class WebSocketReconnect { constructor (url, maxReconnectAttempts = 3 , reconnectInterval = 20000 , maxReconnectTime = 180000 ) { this .url = url this .maxReconnectAttempts = maxReconnectAttempts this .reconnectInterval = reconnectInterval this .maxReconnectTime = maxReconnectTime this .reconnectCount = 0 this .reconnectTimeout = null this .startTime = null this .socket = null this .connect () } connect ( ) { console .log ('connecting...' ) this .socket = new WebSocket (this .url ) this .socket .onopen = () => { console .log ('WebSocket Connection Opened!' ) this .clearReconnectTimeout () this .reconnectCount = 0 } this .socket .onclose = (event ) => { console .log ('WebSocket Connection Closed:' , event) this .handleClose () } this .socket .onerror = (error ) => { console .error ('WebSocket Connection Error:' , error) this .handleClose () } } handleClose ( ) { if (this .reconnectCount < this .maxReconnectAttempts && (this .startTime === null || Date .now () - this .startTime < this .maxReconnectTime )) { this .reconnectCount ++ console .log ('正在尝试重连 (${this.reconnectCount}/${this.maxReconnectAttempts})次...' ) this .reconnectTimeout = setTimeout (() => { this .connect () }, this .reconnectInterval ) if (this .startTime === null ) { this .startTime = Date .now () } } else { console .log ('超过最大重连次数或重连时间超时,已放弃连接!Max reconnect attempts reached or exceeded max reconnect time. Giving up.' ) this .reconnectCount = 0 this .startTime = null } } clearReconnectTimeout ( ) { if (this .reconnectTimeout ) { clearTimeout (this .reconnectTimeout ) this .reconnectTimeout = null } } close ( ) { if (this .socket && this .socket .readyState === WebSocket .OPEN ) { this .socket .close () } this .clearReconnectTimeout () this .reconnectCount = 0 this .startTime = null } } export default WebSocketReconnect
在任意Vue页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <template> <div> <el-input v-model="textarea1" :rows="5" type="textarea" placeholder="请输入" /> </div> </template> <script setup> import { ref, reactive, onMounted, onUnmounted } from 'vue' import WebSocketReconnect from '@/commentUtil/WebsocketTool' // -------------------------------------------- let textarea1 = ref('【消息】---->') let websocket = null // 判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { // 连接WebSocket节点 websocket = new WebSocketReconnect('ws://127.0.0.1:8080' + '/dev-api/websocket/1122334455') } else { alert('浏览器不支持webSocket') } // 接收到消息的回调方法 websocket.socket.onmessage = function (event) { let data = event.data console.log('后端传递的数据:' + data) // 将后端传递的数据渲染至页面 textarea1.value = textarea1.value + data + '' + '【消息】---->' } // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { websocket.close() } // 关闭连接 function closeWebSocket() { websocket.close() } // 发送消息 function send() { websocket.socket.send({ kk: 123 }) } // ------------------------------------ </script> <style scoped></style>
效果:
1 2 3 【消息】---->连接成功 【消息】---->{"server_time":"2024-03-07 15:53:40","server code":"200","server_message":"服务器推送客户端的消息!"} 【消息】---->{"server_time":"2024-03-07 15:53:50","server_code":"200","server_message":"服务器推送客户端的消息!"}