完整代码
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 {
@Bean
public 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 {
@Override
public 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);
}
}
解释:
modifyHandshake 方法在WebSocket握手过程中被调用。它允许你在握手阶段修改 ServerEndpointConfig 对象,并访问HTTP请求和响应对象。这个方法的签名如下:
public void modifyHandshake(
ServerEndpointConfig serverEndpointConfig,
HandshakeRequest request,
HandshakeResponse response)
serverEndpointConfig
:当前WebSocket端点的配置对象。request
:握手请求对象,包含客户端发起握手请求的信息。response
:握手响应对象,包含服务器对握手请求的响应信息。
3.注册一个WebSocket端点类
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
*/
@OnOpen
public 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
*/
@OnMessage
public 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
*/
@OnClose
public 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 = onMessage
webSocket.value.onclose = onClose