完整代码
Arata08/online-chat-demo
服务端:
1.编写配置类,扫描有 @ServerEndpoint 注解的 Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
解释:
@Configuration 注解表示该类是一个配置类,用于定义Spring容器中的bean。配置类可以替代传统的XML配置文件,通过Java代码来声明和管理bean。
@ServerEndpointExporter 是Spring WebSocket提供的一个类,用于自动注册使用 @ServerEndpoint 注解标注的WebSocket端点。它会扫描应用程序中的所有 @ServerEndpoint 注解的类,并将它们注册为WebSocket端点。
2.编写配置类,用于获取 HttpSession 对象
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.HandshakeResponse;
import jakarta.websocket.server.HandshakeRequest;
import jakarta.websocket.server.ServerEndpointConfig;
/*** 获取HttpSession,这样的话,ChatEndpoint类就能操作HttpSession*/
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {@Overridepublic void modifyHandshake(ServerEndpointConfig serverEndpointConfig, HandshakeRequest request, HandshakeResponse response) {// 获取 HttpSession 对象HttpSession httpSession = (HttpSession) request.getHttpSession();// 将 httpSession 对象保存起来,存到 ServerEndpointConfig 对象中// 在 ChatEndpoint 类的 onOpen 方法就能通过 EndpointConfig 对象获取在这里存入的数据serverEndpointConfig.getUserProperties().put(HttpSession.class.getName(), httpSession);}
}
解释:
EndpointConfig端点配置类使用-CSDN博客文章浏览阅读2次。接口位于包中,它是Java WebSocket API(JSR 356)的一部分。Spring WebSocket框架也提供了对这个接口的支持。https://blog.csdn.net/m0_61160520/article/details/143819154?fromshare=blogdetail&sharetype=blogdetail&sharerId=143819154&sharerefer=PC&sharesource=m0_61160520&sharefrom=from_link
modifyHandshake 方法在WebSocket握手过程中被调用。它允许你在握手阶段修改 ServerEndpointConfig 对象,并访问HTTP请求和响应对象。这个方法的签名如下:
public void modifyHandshake(ServerEndpointConfig serverEndpointConfig, HandshakeRequest request, HandshakeResponse response)
serverEndpointConfig
:当前WebSocket端点的配置对象。request
:握手请求对象,包含客户端发起握手请求的信息。response
:握手响应对象,包含服务器对握手请求的响应信息。
3.注册一个WebSocket端点类
解释:https://blog.csdn.net/m0_61160520/article/details/143818152?fromshare=blogdetail&sharetype=blogdetail&sharerId=143818152&sharerefer=PC&sharesource=m0_61160520&sharefrom=from_linkhttps://blog.csdn.net/m0_61160520/article/details/143818152?fromshare=blogdetail&sharetype=blogdetail&sharerId=143818152&sharerefer=PC&sharesource=m0_61160520&sharefrom=from_link
import cn.edu.scau.config.GetHttpSessionConfig;
import cn.edu.scau.utils.MessageUtils;
import cn.edu.scau.websocket.pojo.Message;
import com.alibaba.fastjson2.JSON;
import jakarta.servlet.http.HttpSession;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfig.class)
@Component
public class ChatEndpoint {// 保存在线的用户,key为用户名,value为 Session 对象private static final Map<String, Session> onlineUsers = new ConcurrentHashMap<>();private HttpSession httpSession;/*** 建立websocket连接后,被调用** @param session Session*/@OnOpenpublic void onOpen(Session session, EndpointConfig config) {this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());String user = (String) this.httpSession.getAttribute("currentUser");if (user != null) {onlineUsers.put(user, session);}// 通知所有用户,当前用户上线了String message = MessageUtils.getMessage(true, null, getFriends());broadcastAllUsers(message);}private Set<String> getFriends() {return onlineUsers.keySet();}private void broadcastAllUsers(String message) {try {Set<Map.Entry<String, Session>> entries = onlineUsers.entrySet();for (Map.Entry<String, Session> entry : entries) {// 获取到所有用户对应的 session 对象Session session = entry.getValue();// 使用 getBasicRemote() 方法发送同步消息session.getBasicRemote().sendText(message);}} catch (Exception exception) {exception.printStackTrace();}}/*** 浏览器发送消息到服务端时该方法会被调用,也就是私聊* 张三 --> 李四** @param message String*/@OnMessagepublic void onMessage(String message) {try {// 将消息推送给指定的用户Message msg = JSON.parseObject(message, Message.class);// 获取消息接收方的用户名String toName = msg.getToName();String tempMessage = msg.getMessage();// 获取消息接收方用户对象的 session 对象Session session = onlineUsers.get(toName);String currentUser = (String) this.httpSession.getAttribute("currentUser");String messageToSend = MessageUtils.getMessage(false, currentUser, tempMessage);session.getBasicRemote().sendText(messageToSend);} catch (Exception exception) {exception.printStackTrace();}}/*** 断开 websocket 连接时被调用** @param session Session*/@OnClosepublic void onClose(Session session) throws IOException {// 1.从 onlineUsers 中删除当前用户的 session 对象,表示当前用户已下线String user = (String) this.httpSession.getAttribute("currentUser");if (user != null) {Session remove = onlineUsers.remove(user);if (remove != null) {remove.close();}session.close();}// 2.通知其他用户,当前用户已下线// 注意:不是发送类似于 xxx 已下线的消息,而是向在线用户重新发送一次当前在线的所有用户String message = MessageUtils.getMessage(true, null, getFriends());broadcastAllUsers(message);}}
客户端
1.创建一个 axios 实例
向后端发送登录请求需要使用这个 axios 实例
import axios from 'axios'const request = axios.create({baseURL: '/api',timeout: 60000,headers: {'Content-Type': 'application/json;charset=UTF-8'}
})request.interceptors.request.use()request.interceptors.response.use(response => {if (response.data) {return response.data}return response
}, (error) => {return Promise.reject(error)
})export default request
2.编写代理规则
vite.config.js
import {fileURLToPath, URL} from 'node:url'import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias: {'@': fileURLToPath(new URL('./src', import.meta.url))}},server: {proxy: {'/api': {target: 'http://localhost:7024',changeOrigin: true,rewrite: (path) => {return path.replace('/api', '')}}}}
})
3.创建 WebSocket 对象
webSocket.value = new WebSocket('ws://localhost:7024/chat')
4.为 WebSocket 对象绑定事件
webSocket.value.onopen = onOpen// 接收到服务端推送的消息后触发
webSocket.value.onmessage = onMessagewebSocket.value.onclose = onClose