Bootstrap

WebSocket简易聊天室实现(有详细解释)

 完整代码

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);
    }
}

解释:

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_linkicon-default.png?t=O83Ahttps://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
     */
    @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
;