news 2026/3/29 7:47:08

WebSocket 实时聊天功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WebSocket 实时聊天功能

在上一讲中,Spring Boot 后端实现 WebSocket 已创建过后端项目,现在开始补充前端
在项目下新增一个模块frontend【与后端src目录平级】
在前端目录下执行npm install

不看上一讲也可以,直接创建一个前后端项目即可,下面会给出完整代码

后端

依赖

pom.xml内容如下

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> </dependency> </dependencies>

文件位置与代码

src/main/java/.../config/WebSocketConfig.java

package your.package.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }

代码功能
让Spring Boot项目能够支持WebSocket,实现浏览器和服务器之间的双向实时通信

注解作用
@Configuration:告诉Spring这是一个配置类
@Bean:创建一个可被Spring管理的对象

方法解释
serverEndpointExporter():创建WebSocket服务器端点导出器,它会自动注册所有带有@ServerEndpoint注解的类,让它们能处理WebSocket连接

被Spring管理的好处:

被Spring管理的对象,Spring会自动帮你:

创建对象 - 不用自己写new ServerEndpointExporter()

保存起来 - Spring把对象放进自己的“容器”里,随时可用

自动使用 - 其他地方需要时,Spring会自动送过去(自动注入)

简单说:
有了@Bean,Spring就会说:“这个对象我管了,谁要用就找我要,不用你们自己操心怎么创建和传递。”

src/main/java/.../ws/ChatEndpoint.java

package com.websocket.ws; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.websocket.*; import jakarta.websocket.server.PathParam; import jakarta.websocket.server.ServerEndpoint; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @ServerEndpoint("/ws/chat/{username}") @Component public class ChatEndpoint { private static final Map<String, Session> ONLINE = new ConcurrentHashMap<>(); private static final ObjectMapper MAPPER = new ObjectMapper(); // ====== 消息结构(最小可用)====== public static class Msg { public String type; // SYSTEM / CHAT public String from; // username public String content; // 文本 public long time; // 时间戳 } @OnOpen public void onOpen(Session session, @PathParam("username") String username) { ONLINE.put(username, session); System.out.println("🟢 onOpen username=" + username + ", sessionId=" + session.getId()); broadcast(system(username + " 加入聊天室", username)); } @OnMessage public void onMessage(String payload, Session session, @PathParam("username") String username) { System.out.println("📩 onMessage from=" + username + ", payload=" + payload); // 允许前端发纯文本或 JSON:最小可用、兼容调试 String content = payload; try { Msg incoming = MAPPER.readValue(payload, Msg.class); if (incoming != null && incoming.content != null && !incoming.content.isBlank()) { content = incoming.content; } } catch (Exception ignore) { /* payload 不是 JSON 就当纯文本 */ } broadcast(chat(content, username)); } @OnClose public void onClose(Session session, @PathParam("username") String username) { ONLINE.remove(username); System.out.println("🔴 onClose username=" + username + ", sessionId=" + session.getId()); broadcast(system(username + " 离开聊天室", username)); } @OnError public void onError(Session session, Throwable t, @PathParam("username") String username) { System.out.println("⚠️ onError username=" + username + ", sessionId=" + session.getId()); t.printStackTrace(); } // ====== 群发(最小可用)====== private void broadcast(String json) { ONLINE.forEach((u, s) -> { if (s == null || !s.isOpen()) return; try { s.getBasicRemote().sendText(json); } catch (Exception e) { e.printStackTrace(); } }); } private String system(String content, String from) { return toJson("SYSTEM", from, content); } private String chat(String content, String from) { return toJson("CHAT", from, content); } private String toJson(String type, String from, String content) { try { Msg msg = new Msg(); msg.type = type; msg.from = from; msg.content = content; msg.time = System.currentTimeMillis(); return MAPPER.writeValueAsString(msg); } catch (Exception e) { // 兜底:极端情况下也别让广播炸掉 return "{\"type\":\"" + type + "\",\"from\":\"" + from + "\",\"content\":\"" + content + "\",\"time\":" + System.currentTimeMillis() + "}"; } } }

代码功能
这是一个 WebSocket 聊天服务器端点,实现了:
用户连接/断开管理 - 记录在线用户
消息广播 - 一人发消息,全员都能收到
消息格式化 - 将消息转为 JSON 格式发送

注解作用
@ServerEndpoint("/ws/chat/{username}"):声明这是一个 WebSocket 端点,路径中包含用户名参数
@Component:让 Spring 管理这个类的实例
@OnOpen:用户连接时自动调用
@OnMessage:收到用户消息时自动调用
@OnClose:用户断开时自动调用
@OnError:发生错误时自动调用
@PathParam("username"):从 URL 路径中获取用户名参数

主要方法
onOpen():用户连接时,记录到在线列表并通知所有人
onMessage():收到消息时,转发给所有在线用户
onClose():用户离开时,从在线列表移除并通知所有人
broadcast():遍历所有在线用户发送消息
toJson():将消息对象转为 JSON 字符串

前端

配置 Vite 代理

frontend/vite.config.js

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { // 让 ws://localhost:5173/ws/... 代理到 Spring Boot :8080 '/ws': { target: 'http://localhost:8080', ws: true, changeOrigin: true, }, // 如果你后面还有 REST 接口,也可以统一走 /api '/api': { target: 'http://localhost:8080', changeOrigin: true, } } } })

代码功能
这是 Vite前端开发服务器的代理配置,主要解决前端开发时访问后端API的问题。
这个配置让前端开发时能连接到后端,特别是让WebSocket聊天功能正常工作。

核心函数功能
defineConfig():定义Vite的配置
vue():启用Vue框架支持

配置项功能
plugins: [vue()]:使用Vite的Vue插件
server.proxy:设置代理服务器,将前端请求转发到后端

特殊参数含义
'/ws' 配置:
作用:所有以 /ws 开头的请求
target: 'http://localhost:8080' → 转发到Spring Boot后端
ws: true → 支持WebSocket连接(关键!让聊天功能生效)
changeOrigin: true → 改变请求头中的Origin,避免跨域问题

'/api' 配置:
作用:所有以 /api 开头的请求
转发REST API请求到后端
不包括ws: true,因为REST API不用WebSocket

代理服务器会:
1. 收到前端的请求(Origin: http://localhost:5173)
2. 转发请求到后端时
3. 把请求头中的Origin改为目标服务器的地址
4. 发送到后端:Origin: http://localhost:8080

WebSocket 客户端封装

frontend/src/utils/chatWs.js

export function createChatWs({ username, onMessage, onOpen, onClose, onError }) { // 关键:用 location.host,这样开发期是 5173(走代理),生产期是 8080(同域) const proto = location.protocol === 'https:' ? 'wss' : 'ws' const url = `${proto}://${location.host}/ws/chat/${encodeURIComponent(username)}` const ws = new WebSocket(url) ws.onopen = () => onOpen && onOpen() ws.onclose = () => onClose && onClose() ws.onerror = (e) => onError && onError(e) ws.onmessage = (e) => { try { const data = JSON.parse(e.data) onMessage && onMessage(data) } catch { // 兜底:如果后端发的不是 JSON(理论上不会),也能显示 onMessage && onMessage({ type: 'CHAT', from: 'server', content: e.data, time: Date.now() }) } } return { sendChat(content) { if (ws.readyState !== WebSocket.OPEN) return ws.send(JSON.stringify({ type: 'CHAT', content, time: Date.now() })) }, close() { ws.close() } } }

代码整体功能
这是一个创建WebSocket聊天连接的工厂函数,封装了连接、发送、接收消息等操作,让使用更简单。

核心变量/常量功能
proto:判断用 ws(普通)还是 wss(加密)协议
url:生成WebSocket连接地址,例如:ws://localhost:5173/ws/chat/张三
ws:WebSocket连接对象,负责实际通信

WebSocket核心事件功能
onopen:连接成功时触发 → 调用 onOpen 回调
onclose:连接关闭时触发 → 调用 onClose 回调
onerror:连接出错时触发 → 调用 onError 回调
onmessage:收到消息时触发 → 解析消息并调用 onMessage 回调

返回方法功能
sendChat(content):发送聊天消息(自动格式化为JSON)
close():主动关闭WebSocket连接

关键逻辑含义
协议判断:
const proto = location.protocol === 'https:' ? 'wss' : 'ws'
如果网页是 https:// 就用 wss://(加密WebSocket)
如果网页是 http:// 就用 ws://(普通WebSocket)

自动适应环境:
const url = `${proto}://${location.host}/ws/chat/...`
开发时:ws://localhost:5173/ws/chat/...(走代理到8080)
生产时:ws://你的域名:8080/ws/chat/...(直接连接)

JSON解析兜底:
try { JSON.parse(e.data) } catch { /* 备用处理 */ }
优先解析为JSON格式
如果解析失败(理论上不会),就用原始数据

连接状态判断:
if (ws.readyState !== WebSocket.OPEN) return
确保只有在连接成功时才能发送消息
避免连接中断时发送消息出错

聊天页面

frontend/src/App.vue

<template> <div style="max-width: 720px; margin: 24px auto; font-family: Arial, sans-serif;"> <h2>WebSocket 最小聊天室(Spring Boot + Vue)</h2> <div style="display:flex; gap: 8px; align-items:center; margin-bottom: 12px;"> <input v-model="username" placeholder="输入用户名,如 Tom" style="flex:1; padding: 8px;" /> <button @click="connect" :disabled="connected" style="padding: 8px 12px;">连接</button> <button @click="disconnect" :disabled="!connected" style="padding: 8px 12px;">断开</button> </div> <div style="border: 1px solid #ddd; padding: 12px; height: 320px; overflow:auto; background:#fafafa;"> <div v-for="(m, idx) in messages" :key="idx" style="margin-bottom: 8px;"> <div v-if="m.type === 'SYSTEM'" style="color:#666;"> [系统] {{ m.content }} </div> <div v-else> <b>{{ m.from }}</b>:{{ m.content }} </div> </div> </div> <div style="display:flex; gap: 8px; margin-top: 12px;"> <input v-model="text" placeholder="输入消息回车或点击发送" style="flex:1; padding: 8px;" @keyup.enter="send" :disabled="!connected" /> <button @click="send" :disabled="!connected || !text.trim()" style="padding: 8px 12px;">发送</button> </div> <div style="margin-top: 10px; color:#666;"> 状态:{{ connected ? '已连接' : '未连接' }} </div> </div> </template> <script setup> import { ref } from 'vue' import { createChatWs } from './utils/chatWs' const username = ref('Tom') const text = ref('') const connected = ref(false) const messages = ref([]) let client = null function connect() { if (!username.value.trim()) return client = createChatWs({ username: username.value.trim(), onOpen: () => { connected.value = true }, onClose: () => { connected.value = false }, onError: () => { connected.value = false }, onMessage: (msg) => { messages.value.push(msg) } }) } function disconnect() { if (client) client.close() client = null } function send() { const c = text.value.trim() if (!c || !client) return client.sendChat(c) text.value = '' } </script>

代码整体功能
这是一个完整的WebSocket聊天室前端界面,用户可连接/断开聊天室、发送/接收消息。

模板部分布局和交互元素
用户名输入+连接按钮:设置用户名并连接聊天室
消息显示区域:滚动显示系统消息和用户聊天消息
消息输入框+发送按钮:输入和发送聊天消息
状态显示:显示当前连接状态

Vue指令功能
v-model:双向绑定输入框内容和变量(如 username ↔ 输入框)
v-for:循环显示消息列表中的每条消息
v-if/v-else:判断显示系统消息还是普通消息
@click:点击按钮时触发函数
@keyup.enter:按回车键时触发函数(快速发送)
:disabled:根据条件禁用按钮(如未连接时禁用发送)
:key:为循环项提供唯一标识(提高渲染效率)

脚本核心变量作用
username:存储当前用户的名字
text:存储要发送的消息内容
connected:记录是否已连接到WebSocket(true/false)
messages:存储所有接收到的消息(数组)
client:存储WebSocket连接对象(用于发送/关闭)

核心函数功能
connect():
检查用户名是否为空
调用 createChatWs 创建WebSocket连接
设置回调函数(连接/断开/收消息时更新界面)

disconnect():
调用WebSocket的 close() 方法断开连接
清空连接对象

send():
检查消息是否为空、是否已连接
调用 client.sendChat() 发送消息
清空输入框

createChatWs使用逻辑
client = createChatWs({
username: username.value, // 当前用户名
onOpen: () => { connected.value = true }, // 连接成功时更新状态
onClose: () => { connected.value = false }, // 断开连接时更新状态
onError: () => { connected.value = false }, // 出错时也更新状态
onMessage: (msg) => { messages.value.push(msg) } // 收到消息时添加到列表
})

frontend/src/main.js

import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')

这是Vue应用的入口文件,像"启动器"一样,创建一个Vue应用实例并挂载到网页上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/13 20:51:03

Java毕设项目:基于JAVA的北京市公交管理系统基于Java+Vue+SpringBoot的北京市公交管理系统(源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/23 2:19:01

C++医学图像处理经典ITK库用法详解<三>: 图像配准模块功能

1、ITK库概述ITK (Insight Segmentation and Registration Toolkit) 是一个开源的跨平台软件开发工具包&#xff0c;主要用于图像处理&#xff0c;特别是生物医学图像处理领域。该工具包提供了一套丰富的图像处理算法&#xff0c;特别是在图像分割和配准方面具有强大的功能。IT…

作者头像 李华
网站建设 2026/3/16 18:05:12

为什么XGBoost在绝大多数情况下都比深度学习算法效果好?甚至秒杀各种新提出的算法!原创未发表!!基于非线性二次分解的Ridge-RF-XGBoost时间序列预测

近年来&#xff0c;尽管深度学习在图像识别、自然语言处理等领域取得了显著成功&#xff0c;但在结构化数据&#xff08;tabular data&#xff09;上的回归与分类任务中&#xff0c;梯度提升树模型——特别是XGBoost&#xff08;eXtreme Gradient Boosting&#xff09;——在绝…

作者头像 李华
网站建设 2026/3/27 0:50:58

小程序毕设项目:基于springboot+微信小程序的选修课管理系统的设计与实现(源码+文档,讲解、 调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/19 10:16:50

小程序毕设项目:基于springboot+微信小程序的智能医疗管理系统设计与实现(源码+文档,讲解、 调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华