在实际开发中,我们经常需要处理用户交互的即时性,例如在社交、直播或在线协作场景中,实时感知到“谁正在看我”或“谁关注了我”是一个典型需求。虽然输入材料“当你突然看我的时候”更像一个感性的描述,但从技术实现角度,它可以映射为一个实时在线状态感知与事件通知系统。这类系统需要解决的核心问题是:如何低延迟、高可靠地让一个用户(观察者)的行为(如进入页面、聚焦窗口)被另一个特定用户(被观察者)实时感知。
本文将围绕构建这样一个最小化的实时感知系统展开。我们将使用 WebSocket 作为双向通信协议,Spring Boot 作为后端框架,Vue 3 作为前端框架,实现一个可运行的示例。文章将不仅包含连接建立和消息发送,还会深入探讨连接状态管理、心跳机制、用户映射、事件定义以及生产环境需要考虑的扩展性问题。通过本文,你将掌握从零搭建一个具备基础实时感知能力服务的关键步骤、常见陷阱以及优化方向。
1. 理解实时感知系统的核心机制与设计取舍
在开始编码之前,必须理解我们要构建的系统与传统请求-响应模式(如 HTTP API)的本质区别。实时感知的核心是服务器需要主动向客户端推送数据,并且这种推送是基于某个特定事件(例如用户A打开了用户B的主页)触发的。
1.1 为什么选择 WebSocket 而非轮询或 Server-Sent Events
实现服务器推送主要有几种技术:短轮询、长轮询、Server-Sent Events (SSE) 和 WebSocket。
- 短轮询:客户端定期(如每秒)向服务器发送 HTTP 请求询问“有更新吗?”。简单但效率极低,会产生大量无效请求,延迟等于轮询间隔。
- 长轮询:客户端发起请求,服务器持有连接直到有数据或超时。比短轮询好,但每次通信仍需重建 HTTP 连接,开销较大。
- Server-Sent Events (SSE):允许服务器通过一个持久的 HTTP 连接向客户端单向推送数据。非常适合服务器向客户端发送通知流,但不支持客户端向服务器的双向通信。对于“看”这个动作,需要客户端先上报,因此 SSE 不完全适用。
- WebSocket:在单个 TCP 连接上提供全双工通信通道。连接建立后,客户端和服务器可以随时相互发送消息,延迟极低,开销小。它是实现双向实时交互的理想选择。
因此,对于“当你突然看我的时候”这种需要客户端(观察者)主动上报事件,并由服务器实时转发给另一客户端(被观察者)的场景,WebSocket 是目前最合适的基础协议。
1.2 系统架构与数据流设计
一个最小化的系统包含以下组件和数据流:
- 客户端 (观察者):打开目标页面,建立 WebSocket 连接,并发送一个“开始观看”事件。
- WebSocket 服务器:维护所有活跃连接,并建立用户 ID 与 WebSocket 会话的映射关系。当收到“开始观看”事件时,解析出观察者和被观察者 ID,并向被观察者的连接推送“被观看”通知。
- 客户端 (被观察者):保持 WebSocket 连接,监听服务器推送的消息。收到“被观看”通知后,更新 UI(例如弹出提示)。
关键设计点在于如何管理用户 -> 连接的映射。一个用户可能从多个设备登录,因此通常是一对多的关系。在我们的最小示例中,为简化处理,我们假设一个用户只有一个活跃连接。
2. 环境准备与项目结构搭建
我们将创建一个 Spring Boot 后端和一个 Vue 3 前端。确保你的开发环境满足以下要求:
| 组件 | 要求 | 说明 |
|---|---|---|
| JDK | 17 或更高版本 | Spring Boot 3.x 需要 JDK 17+ |
| Node.js | 18.x 或更高版本 | 用于运行 Vue 开发服务器和构建 |
| Maven | 3.6+ 或 Gradle | 用于管理 Spring Boot 项目依赖 |
| IDE | IntelliJ IDEA, VS Code 等 | 任意你熟悉的开发环境 |
2.1 创建 Spring Boot 后端项目
使用 Spring Initializr 或 IDE 的创建向导,生成一个项目,依赖选择:
- Spring Web:提供基础的 Web 能力。
- Spring Boot DevTools(可选):开发时热加载。
- Lombok(可选):简化 POJO 代码。
生成后,在pom.xml中手动添加spring-boot-starter-websocket依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>项目基础结构如下:
realtime-notification-backend ├── src/main/java/com/example/demo │ ├── config/ # 配置类目录 │ ├── controller/ # HTTP控制器(可选,用于测试) │ ├── dto/ # 数据传输对象 │ ├── handler/ # WebSocket消息处理器 │ └── DemoApplication.java # 主启动类 ├── src/main/resources │ └── application.properties └── pom.xml2.2 创建 Vue 3 前端项目
使用 Vue CLI 或 Vite 创建项目。这里使用 Vite:
npm create vue@latest realtime-notification-frontend # 按照提示选择项目特性,确保包含 TypeScript 和 Router(可选) cd realtime-notification-frontend npm install此外,我们需要安装 WebSocket 客户端库。虽然浏览器原生支持WebSocketAPI,但使用vue-use-webSocket或SockJS-client+StompJS可以更方便地集成到 Vue 的响应式系统中。为了更接近原生并保持示例简洁,我们将使用原生 API 并配合 Vue 的 Composition API 进行封装。
安装一个轻量级的工具库(可选,用于处理 JSON):
npm install axios # 用于可能的HTTP API调用前端项目结构:
realtime-notification-frontend ├── src/ │ ├── components/ # Vue组件 │ │ └── NotificationDisplay.vue │ ├── composables/ # 组合式函数 │ │ └── useWebSocket.ts │ ├── views/ # 页面视图 │ │ ├── HomeView.vue # 被观察者页面 │ │ └── ViewerPage.vue # 观察者页面 │ ├── App.vue │ └── main.ts ├── index.html ├── package.json └── vite.config.ts3. 实现后端 WebSocket 服务
后端需要完成三件事:启用 WebSocket 支持、处理连接生命周期、路由并处理消息。
3.1 启用 WebSocket 配置
创建一个配置类WebSocketConfig,启用基于 STOMP 子协议的 WebSocket 支持。STOMP 是一个简单的消息协议,为 WebSocket 提供了消息格式和目的地的约定,比直接处理原始 WebSocket 帧更方便。
package com.example.demo.config; 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 // 启用WebSocket消息代理 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 定义客户端连接WebSocket的端点,允许跨域(用于前端开发) registry.addEndpoint("/ws-notification") .setAllowedOriginPatterns("*") // 生产环境应指定具体域名 .withSockJS(); // 启用SockJS回退选项,以应对不支持WebSocket的浏览器 } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 启用一个简单的内存消息代理,将消息路由到以`/topic`为前缀的目的地(用于广播) registry.enableSimpleBroker("/topic"); // 设置应用程序自身消息的目的地前缀,客户端发送消息到以`/app`为前缀的地址 registry.setApplicationDestinationPrefixes("/app"); } }3.2 定义消息模型和事件类型
我们需要定义客户端和服务器之间传递的消息格式。通常使用 JSON。
package com.example.demo.dto; import lombok.Data; import java.time.Instant; @Data public class NotificationMessage { // 事件类型,例如:VIEW_START, VIEW_END, HEARTBEAT private String eventType; // 触发事件的用户ID(观察者) private String fromUserId; // 事件目标用户ID(被观察者) private String toUserId; // 事件发生时间戳 private Instant timestamp; // 可选:附加数据,如观看的页面、设备信息等 private Object payload; }3.3 实现消息控制器
创建一个控制器来处理客户端发送的特定应用消息。当观察者发送“开始观看”事件时,控制器负责将其转发给被观察者。
package com.example.demo.controller; import com.example.demo.dto.NotificationMessage; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; import java.time.Instant; @Controller public class NotificationController { private final SimpMessagingTemplate messagingTemplate; public NotificationController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } /** * 处理客户端发送到 `/app/view` 的消息。 * 客户端消息体应是一个 NotificationMessage,其中 eventType="VIEW_START", toUserId 指定被观看者。 * @param message 客户端发送的消息 */ @MessageMapping("/view") // 对应 /app/view public void handleViewEvent(NotificationMessage message) { // 设置或补充时间戳 message.setTimestamp(Instant.now()); // 构造目标地址:/topic/notifications/{userId} String destination = String.format("/topic/notifications/%s", message.getToUserId()); // 使用 messagingTemplate 直接发送到指定目的地 messagingTemplate.convertAndSend(destination, message); // 注意:这里没有返回值,因为我们不是广播给所有订阅者,而是点对点发送。 } }关键解释:
@MessageMapping(“/view”):监听发送到/app/view目的地的消息。SimpMessagingTemplate:Spring 提供的用于发送消息的模板类。convertAndSend(destination, message):将消息对象转换为 JSON(通过配置的 MessageConverter)并发送到指定的destination。- 目的地格式
/topic/notifications/{userId}是一种约定,允许前端订阅特定用户的通知频道。
3.4 维护用户-会话映射(进阶)
上面的简单实现有一个问题:它依赖于消息体中的toUserId来路由,但无法验证fromUserId的真实性,也无法处理用户离线的情况。一个更健壮的系统需要在用户连接时,将其用户 ID 与 WebSocket 会话绑定。
我们可以实现一个WebSocketEventListener来监听连接和断开事件:
package com.example.demo.handler; import org.springframework.context.event.EventListener; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionConnectedEvent; import org.springframework.web.socket.messaging.SessionDisconnectEvent; import java.util.concurrent.ConcurrentHashMap; @Component public class WebSocketSessionHandler { // 存储 userId -> sessionId 的映射。实际生产环境可能用Redis。 private final ConcurrentHashMap<String, String> userSessionMap = new ConcurrentHashMap<>(); @EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); // 假设连接建立时,客户端通过连接参数传递了userId String userId = headers.getFirstNativeHeader("userId"); String sessionId = headers.getSessionId(); if (userId != null && sessionId != null) { userSessionMap.put(userId, sessionId); System.out.println("用户连接: " + userId + ", session: " + sessionId); } } @EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(event.getMessage()); String sessionId = headers.getSessionId(); // 遍历map,移除对应sessionId的条目 userSessionMap.entrySet().removeIf(entry -> entry.getValue().equals(sessionId)); System.out.println("用户断开连接,session: " + sessionId); } public String getSessionIdByUserId(String userId) { return userSessionMap.get(userId); } }然后,在NotificationController中注入WebSocketSessionHandler,在转发消息前,可以先检查被观察者是否在线(getSessionIdByUserId是否返回非空),再进行精准发送(使用convertAndSendToUser方法)。这涉及到更复杂的用户认证和会话管理,本例为简化,暂不展开。
4. 实现前端 WebSocket 客户端
前端需要实现:建立连接、订阅个人通知频道、发送“观看”事件、接收并显示通知。
4.1 封装 WebSocket 工具函数
创建一个组合式函数useWebSocket.ts,管理连接状态和消息收发。
// src/composables/useWebSocket.ts import { ref, onUnmounted } from 'vue'; interface NotificationMessage { eventType: string; fromUserId: string; toUserId: string; timestamp: string; payload?: any; } export function useWebSocket(userId: string) { const socket = ref<WebSocket | null>(null); const isConnected = ref(false); const notifications = ref<NotificationMessage[]>([]); const stompClient = ref<any>(null); // 如果使用STOMP,这里会是Stomp.Client类型 // 使用SockJS和STOMP的连接方式(推荐,与后端SockJS兼容) const connectWithStomp = () => { const socketUrl = `http://localhost:8080/ws-notification`; const socket = new SockJS(socketUrl); const stomp = Stomp.over(socket); stomp.connect({ userId: userId }, () => { console.log('WebSocket connected via STOMP'); isConnected.value = true; stompClient.value = stomp; // 订阅个人通知频道 const subscription = stomp.subscribe(`/topic/notifications/${userId}`, (message) => { const notification: NotificationMessage = JSON.parse(message.body); console.log('收到通知:', notification); notifications.value.unshift(notification); // 添加到列表开头 // 可以在这里触发UI提示,如播放声音、显示Toast showNotificationAlert(notification); }); // 存储subscription以便断开时取消订阅 }, (error: any) => { console.error('STOMP连接失败:', error); isConnected.value = false; }); }; // 发送“开始观看”事件 const sendViewEvent = (targetUserId: string) => { if (!stompClient.value || !isConnected.value) { console.error('WebSocket未连接'); return; } const message: NotificationMessage = { eventType: 'VIEW_START', fromUserId: userId, toUserId: targetUserId, timestamp: new Date().toISOString(), payload: { page: window.location.pathname } }; stompClient.value.send('/app/view', {}, JSON.stringify(message)); console.log('发送观看事件:', message); }; // 断开连接 const disconnect = () => { if (stompClient.value) { stompClient.value.disconnect(); } isConnected.value = false; socket.value = null; stompClient.value = null; console.log('WebSocket disconnected'); }; // 简单的通知提示 const showNotificationAlert = (notification: NotificationMessage) => { if (notification.eventType === 'VIEW_START') { alert(`用户 ${notification.fromUserId} 开始观看你!`); } // 可以替换为更优雅的UI组件 }; // 组件卸载时自动断开连接 onUnmounted(() => { disconnect(); }); return { isConnected, notifications, connect: connectWithStomp, disconnect, sendViewEvent }; }注意:上述代码使用了SockJS和Stomp。需要在index.html中引入相关库,或通过 npm 安装:
npm install sockjs-client @stomp/stompjs并在useWebSocket.ts中导入:
import SockJS from 'sockjs-client'; import { Client } from '@stomp/stompjs'; // 使用新的Client API,上述示例是兼容旧版的写法,实际建议使用新版API。4.2 创建被观察者主页
被观察者页面需要连接 WebSocket 并等待通知。
<!-- src/views/HomeView.vue --> <template> <div class="home"> <h1>我的主页(被观察者)</h1> <p>当前用户ID: <strong>{{ currentUserId }}</strong></p> <p>连接状态: <span :class="connectionClass">{{ isConnected ? '已连接' : '未连接' }}</span></p> <button @click="connectWebSocket" :disabled="isConnected">连接WebSocket</button> <button @click="disconnectWebSocket" :disabled="!isConnected">断开连接</button> <hr> <h3>实时通知列表</h3> <ul v-if="notifications.length > 0"> <li v-for="(note, index) in notifications" :key="index"> [{{ formatTime(note.timestamp) }}] 用户 <strong>{{ note.fromUserId }}</strong> {{ getEventText(note.eventType) }} </li> </ul> <p v-else>暂无通知。</p> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import { useWebSocket } from '@/composables/useWebSocket'; // 假设当前登录用户ID是固定的,实际应从登录状态获取 const currentUserId = ref('user_observed_123'); const { isConnected, notifications, connect, disconnect } = useWebSocket(currentUserId.value); const connectionClass = computed(() => isConnected.value ? 'connected' : 'disconnected'); const connectWebSocket = () => { connect(); }; const disconnectWebSocket = () => { disconnect(); }; const formatTime = (isoString: string) => { return new Date(isoString).toLocaleTimeString(); }; const getEventText = (eventType: string) => { const map: Record<string, string> = { 'VIEW_START': '开始观看你', 'VIEW_END': '停止观看你', }; return map[eventType] || eventType; }; </script> <style scoped> .connected { color: green; } .disconnected { color: red; } </style>4.3 创建观察者页面
观察者页面需要能够输入被观察者的 ID 并发送“观看”事件。
<!-- src/views/ViewerPage.vue --> <template> <div class="viewer"> <h1>观察者页面</h1> <p>当前观察者ID: <strong>{{ viewerUserId }}</strong></p> <div> <label for="targetUserId">输入被观察者ID: </label> <input id="targetUserId" v-model="targetUserId" placeholder="例如: user_observed_123" /> <button @click="sendViewEvent" :disabled="!isConnected || !targetUserId">发送“开始观看”事件</button> </div> <p>连接状态: <span :class="connectionClass">{{ isConnected ? '已连接' : '未连接' }}</span></p> <button @click="connectWebSocket" :disabled="isConnected">连接WebSocket</button> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import { useWebSocket } from '@/composables/useWebSocket'; const viewerUserId = ref('user_viewer_456'); const targetUserId = ref(''); const { isConnected, connect, sendViewEvent: sendView } = useWebSocket(viewerUserId.value); const connectionClass = computed(() => isConnected.value ? 'connected' : 'disconnected'); const connectWebSocket = () => { connect(); }; const sendViewEvent = () => { if (targetUserId.value) { sendView(targetUserId.value); targetUserId.value = ''; // 发送后清空 } }; </script>5. 运行验证与测试
5.1 启动后端服务
- 确保在
application.properties中配置了服务器端口(默认 8080)。 - 运行
DemoApplication主类。 - 观察控制台日志,看到 Tomcat 启动在 8080 端口,并且 WebSocket 端点已注册。
5.2 启动前端服务
- 进入前端项目目录,运行
npm run dev。 - Vite 通常会启动在
http://localhost:5173(或其他端口)。
5.3 测试流程
- 打开两个浏览器窗口(或匿名窗口),模拟两个用户。
- 窗口 A:访问
http://localhost:5173(被观察者主页)。点击“连接 WebSocket”。控制台应显示连接成功。 - 窗口 B:访问
http://localhost:5173/viewer(观察者页面)。点击“连接 WebSocket”。 - 在窗口 B的输入框中,输入窗口 A 中显示的用户 ID(如
user_observed_123),点击“发送‘开始观看’事件”。 - 观察窗口 A:应该立即弹出一个提示框,显示“用户 user_viewer_456 开始观看你!”,同时通知列表会更新这条记录。
- 检查后端控制台:应该能看到连接日志和消息处理日志。
预期结果:被观察者能近乎实时地收到观察者“观看”行为的通知。这模拟了“当你突然看我的时候”这一交互的底层技术实现。
6. 常见问题排查与优化
在实际部署和开发中,你会遇到各种问题。以下是典型问题及其排查路径。
6.1 连接建立失败
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
前端控制台报WebSocket connection to ‘ws://...‘ failed | 1. 后端服务未启动。 2. 后端 WebSocket 端点路径错误。 3. 跨域问题(前端与后端域名/端口不同)。 4. 网络策略(如防火墙)阻止。 | 1. 确认后端应用日志是否正常启动。 2. 检查前端连接 URL 是否与后端 registerStompEndpoints注册的路径一致。3. 查看浏览器开发者工具 Network 面板,看 WebSocket 连接请求的响应状态码。 4. 检查后端 CORS 配置( setAllowedOriginPatterns)。 | 1. 启动后端服务。 2. 修正连接 URL。 3. 开发阶段可暂时设置 setAllowedOriginPatterns(“*”),生产环境必须指定具体前端域名。4. 检查服务器安全组和防火墙设置。 |
连接使用 SockJS 时,一直停留在Opening状态 | SockJS 在尝试多种传输协议(WebSocket, xhr-streaming等)。 | 查看浏览器 Network 面板,会看到一系列info、xhr请求。 | 这通常是正常的降级过程。如果最终连接成功,则无需处理。如果失败,检查后端是否支持 SockJS(添加了.withSockJS())。 |
6.2 消息发送成功但接收不到
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 前端发送消息后,后端控制器收到并处理了,但目标客户端没反应。 | 1. 目标客户端未订阅正确的目的地。 2. 消息目的地路径拼写错误。 3. 后端消息转换器未正确序列化/反序列化 JSON。 | 1. 在后端handleViewEvent方法内打印日志,确认destination字符串。2. 在前端订阅代码处打印日志,确认订阅的路径。 3. 检查浏览器开发者工具 WebSocket 帧内容,看服务器推送的消息格式是否正确。 | 1. 确保订阅路径/topic/notifications/{userId}与发送路径完全匹配。2. 使用 Spring Boot 默认的 Jackson 库,通常能自动处理 JSON。检查对象是否有正确的 getter 方法(或 Lombok @Data)。3. 可以在前端 STOMP 客户端启用调试模式,查看所有帧。 |
6.3 连接不稳定,经常断开
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 连接一段时间后自动断开,前端触发重连。 | 1. 网络波动。 2. 代理服务器(如 Nginx)超时设置过短。 3. 缺乏心跳机制,连接被中间设备清理。 | 1. 查看后端日志是否有断开事件。 2. 检查 Nginx 等代理的 proxy_read_timeout,proxy_send_timeout配置(通常需要设置得较长,如 1小时)。3. 检查 STOMP 客户端和服务端是否配置了心跳。 | 1. 在 STOMP 配置中启用心跳。后端registry.enableSimpleBroker(...)后可链式调用.setHeartbeatValue(...)。2. 前端 STOMP 客户端连接时,配置 heartbeatIn/Out。3. 在前端实现自动重连逻辑。 |
6.4 生产环境扩展性问题
上述示例使用内存消息代理和内存 Session 映射,仅适用于单机开发。生产环境需要考虑:
- 分布式扩展:当有多台后端服务器时,内存中的 Session 映射和订阅信息无法共享。需要使用外部消息代理(如 RabbitMQ, Redis, Kafka)作为 STOMP broker,并集成 Spring Session 和 Redis 来管理分布式 WebSocket 会话。
- 用户认证与授权:连接建立时,不能信任客户端传递的
userId。应集成 Spring Security,在握手阶段进行身份验证(如基于 JWT),并将认证信息绑定到 WebSocket 会话。 - 连接数管理与限流:防止单个用户建立过多连接耗尽资源。可以在握手拦截器中进行限制。
- 消息持久化:对于重要的通知,如果用户离线,需要存储起来,待其上线后推送。这需要引入消息队列和离线存储。
- 前端重连与状态恢复:网络中断后,前端应自动重连,并重新订阅之前的频道。需要更完善的状态管理。
7. 最佳实践与扩展方向
7.1 连接管理最佳实践
- 始终启用心跳:在服务器和客户端配置心跳,保持连接活跃,及时发现死连接。
// 后端配置示例 @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic") .setHeartbeatValue(new long[]{10000, 10000}); // 发送间隔,接收间隔(毫秒) } - 实现前端自动重连:在连接断开时,使用指数退避策略进行重连。
- 连接状态可视化:在 UI 上清晰显示连接状态(连接中、已连接、断开、重连中)。
7.2 消息设计最佳实践
- 定义清晰的事件类型枚举:如
VIEW_START,VIEW_END,LIKE,COMMENT等,避免魔法字符串。 - 消息体版本化:考虑在消息中加入版本字段,便于后续兼容性升级。
- 添加消息去重与幂等性:对于可能重复发送的消息(如网络重试),在客户端或服务器端根据唯一 ID 进行去重处理。
7.3 扩展功能方向
- “正在看”列表:被观察者可以实时查看当前有哪些人在看自己。这需要服务器维护一个“观看关系”的集合,并在连接断开时清理。
- 观看时长统计:结合
VIEW_START和VIEW_END事件,计算观察者的观看时长。 - 匿名观看:观察者可以不暴露自己的真实 ID,由服务器生成临时标识。
- 富媒体通知:通知可以包含头像、观看的页面截图(需前端配合)等信息。
- 离线消息:集成 APNs(iOS)、FCM(Android)或 Web Push,在用户关闭网页时仍能送达通知。
构建“当你突然看我的时候”这样的实时感知功能,技术核心在于稳定、高效的 WebSocket 连接管理和精准的消息路由。从最小原型到生产系统,需要逐步解决认证、扩展、可靠性和用户体验等问题。本文提供的示例是一个完整的起点,你可以在此基础上,根据实际业务需求,引入更强大的消息中间件、完善安全机制并优化前端交互,最终打造出体验流畅的实时交互功能。