在浏览器(客户端)和服务器交互过程中,大部分是浏览器向服务器发送请求然后服务器响应数据给浏览器。但是在一些特定场景中(实时通信系统)需要服务器在数据更新或特定事件发生时,立即将信息推送给客户端,而无需客户端轮询(即定期请求)服务器。比如我现在就有一个需要在前端实时展示用户未读消息的需求。
要实现服务器实时推送数据给前端常用的解决方案就是使用WebSocket通信。
WebSocket 协议是一种在单个 TCP 连接上提供全双工通信的协议,它允许客户端和服务器之间进行实时的双向数据传输。WebSocket 协议通过 HTTP/HTTPS 协议的握手阶段建立连接,之后在相同的连接上进行数据传输,避免了 HTTP 的请求-响应模型的限制。
WebSocket 协议的特点
-
双向通信: WebSocket 允许客户端和服务器之间在同一个连接上进行双向通信,客户端和服务器均可发送和接收数据,实现实时交互和推送功能。
-
低延迟: 由于WebSocket建立在TCP上,而不是HTTP,因此它减少了每个消息的开销,提供了更低的延迟。
-
较少的数据传输开销: WebSocket 协议采用二进制传输,相较于基于文本的 HTTP 请求,传输效率更高,减少了数据传输时的开销。
-
跨域支持: WebSocket 协议支持跨域通信,通过 HTTP 头部的 Upgrade 机制升级到 WebSocket 连接,从而解决了浏览器的同源策略限制。
-
持久连接: WebSocket 连接一旦建立,可以持久存在,服务器和客户端可以随时发送或接收数据,而无需重新建立连接。
-
适用于实时应用: WebSocket 协议特别适用于实时性要求较高的应用,如在线游戏、即时聊天、实时协作工具等。
WebSocket使用场景
-
实时通信应用:
- 即时聊天和通讯:如在线客服、即时消息应用等。
- 协作工具:如实时协同编辑、在线白板等。
- 实时数据更新:如股票市场、实时报警系统等。
-
实时游戏:
- 多人在线游戏(MMOG):WebSocket 提供了低延迟的双向通信,非常适合实时多人游戏的数据传输和状态同步。
-
实时数据展示和监控:
- 实时数据展示:如实时图表、实时监控数据展示。
- 实时事件通知:如系统告警、实时事件发布和订阅。
-
推送服务:
- 广播通知和推送:服务器端可以实时向多个客户端推送信息,如新闻推送、社交网络更新等。
-
在线会议和视频聊天:
- WebSocket 可以支持实时的音视频传输和会议控制。
不适用场景
在一些不需要实时通信,或者客户端和服务端的交互只是偶尔性的数据传输场景不适合使用WebSocket通信。WebSocket 连接是持久的全双工连接,服务器端和客户端需要消耗更多的资源来维护连接状态。
代码实操
后端
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.配置 WebSocket
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
}
}
3. 业务逻辑
@Resource
private SimpMessagingTemplate simpMessagingTemplate;
public void saveMsg(Messages messages) {
this.save(messages);
long count = getUnreadCount(messages.getRecvId());
if (count > 0) {
// 发送通知
simpMessagingTemplate.convertAndSend("/topic/message/" + messages.getRecvId(), count);
}
}
前端
1.安装依赖
npm install sockjs-client @stomp/stompjs
2.配置websocketService
import SockJS from "sockjs-client";
import { Client } from "@stomp/stompjs";
const WebSocketService = {
stompClient: null,
messageCallback: null, // 回调函数用来处理接收到的消息
connect(roomId) {
const socket = new SockJS("http://localhost:8080/ws");
this.stompClient = new Client({
webSocketFactory: () => socket,
debug: (str) => {
console.log(str);
},
onConnect: (frame) => {
console.log("Connected: " + frame);
this.stompClient.subscribe("/topic/message/" + roomId, (message) => {
console.log("Received: " + message.body);
if (this.messageCallback) {
this.messageCallback(message.body); // 调用回调函数处理消息
}
});
},
onStompError: (frame) => {
console.error("Broker reported error: " + frame.headers['message']);
console.error("Additional details: " + frame.body);
},
});
this.stompClient.activate();
},
sendMessage(roomId, message) {
if (this.stompClient && this.stompClient.connected) {
this.stompClient.publish({
destination: "/app/sendMessage/" + roomId,
body: message
});
}
},
// 设置回调函数来处理接收到的消息
setMessageCallback(callback) {
this.messageCallback = callback;
}
};
export default WebSocketService;
3.在组件中使用
<template>
<!-- 消息按钮,带未读消息数量 -->
<div class="message-button-container" v-if="userInfo">
<button class="message-button" @click="goToMessages">未读消息:{{ this.unreadMessagesCount }}</button>
</div>
</template>
<script>
import WebSocketService from "@/WebSocketService";
export default {
name: "IndexPage",
data() {
return {
unreadMessagesCount: 0, // 初始未读消息数
};
},
mounted() {
WebSocketService.connect(this.userInfo.id);
// 设置接收消息的回调函数
WebSocketService.setMessageCallback(this.handleReceivedMessage);
},
methods: {
handleReceivedMessage(message) {
this.unreadMessagesCount = message;
},
}
}
</script>
其他方式
除了使用 WebSocket 技术之外,还有几种实现服务器向客户端推送消息的常见方式:
1. Server-Sent Events (SSE)
Server-Sent Events 是一种轻量级的服务器推送技术,它允许服务器单向推送消息到客户端,客户端通过 EventSource API 接收推送的消息。与 WebSocket 不同的是,SSE 是基于 HTTP 的,只支持服务器到客户端的单向通信。
@RestController
public class SSEController {
@GetMapping("/sse")
public SseEmitter serverSentEvents() {
SseEmitter emitter = new SseEmitter();
// 异步处理发送消息
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
emitter.send(SseEmitter.event().data("Message " + i));
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
}
const eventSource = new EventSource('http://localhost:8080/sse');
eventSource.onmessage = function(event) {
console.log('Received: ' + event.data);
// 处理接收到的消息
};
eventSource.onerror = function(error) {
console.error('Error: ' + error);
};
2. Long Polling
Long Polling 是一种通过客户端定期向服务器发送请求,服务器保持请求打开直到有新消息或超时才响应的技术。虽然 Long Polling 不是真正的推送技术,但在某些情况下可以模拟实时通信效果。
@RestController
public class LongPollingController {
@GetMapping("/poll")
public ResponseEntity<String> pollForMessage() throws InterruptedException {
// 模拟异步获取消息
Thread.sleep(1000);
return ResponseEntity.ok("New message");
}
}
async function pollForMessage() {
try {
const response = await axios.get('http://localhost:8080/poll');
console.log('Received: ' + response.data);
// 处理接收到的消息
} catch (error) {
console.error('Error: ' + error);
} finally {
// 定时轮询
setTimeout(pollForMessage, 1000);
}
}
pollForMessage();