WebSocket 实时聊天功能

在上一讲中,Spring Boot 后端实现 WebSocket 已创建过后端项目,现在开始补充前端

在项目下新增一个模块frontend【与后端src目录平级】

在前端目录下执行npm install

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

后端

依赖

pom.xml内容如下

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

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

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

javascript 复制代码
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

javascript 复制代码
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

html 复制代码
<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

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

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

相关推荐
专业开发者7 小时前
思科以终端产品解决方案提供商的身份实现效能提升
运维·服务器·网络
培培说证7 小时前
2026大专前端开发工程师入门证书推荐?
网络·web安全
管理大亨7 小时前
企业级ELK:从日志收集到业务驱动
java·大数据·网络·数据库·elk·elasticsearch
苏打水com7 小时前
第二十篇:Day58-60 前端性能优化进阶——从“能用”到“好用”(对标职场“体验优化”需求)
前端·css·vue·html·js
Jeking2177 小时前
进阶流程图绘制工具 Unione Flow Editor-- 直击行业痛点:高扩展性解决方案解析
vue·流程图·workflow·unione flow·flow editor·unione cloud
网络小白不怕黑8 小时前
SRv6技术完全指南(1):下一代网络的核心引擎
网络
乾元8 小时前
网络遥测(Telemetry/gNMI)的结构化建模与特征化体系—— 从“采集指标”到“可被 AI 推理的状态向量”
运维·服务器·网络·人工智能·网络协议·华为·ansible
网硕互联的小客服8 小时前
CC攻击对服务器正常运行会有什么影响?如何预防和解决CC攻击?
运维·服务器·网络·windows·安全
大白的编程日记.8 小时前
【计算网络学习笔记】TCP套接字介绍和使用
网络·笔记·学习