WebSocket实现多人实时在线聊天

发布于:2025-07-10 ⋅ 阅读:(18) ⋅ 点赞:(0)

      最近公司在做一个婚恋app,需要增加一个功能,实现多人实时在线聊天。基于WebSocket在Springboot中的使用,前端使用vue开发。

一:后端 

1. 引入 websocket 的 maven 依赖

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

2. 进行 config 配置 ServerEndpointExporter 确保【后续在使用 @ServerEndpoint 】时候能被 SpringBoot 自动检测并注册

 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration // 这个类为配置类,Spring 将扫描这个类中定义的 Beans
public class WebSocketConfig {
    /**
     * serverEndpointExporter 方法的作用是将 ServerEndpointExporter 注册为一个 Bean,
     * 这个 Bean 负责自动检测带有 @ServerEndpoint 注解的类,并将它们注册为 WebSocket 服务器端点,
     * 这样,这些端点就可以接收和处理 WebSocket 请求
     **/
    @Bean // 这个方法返回的对象应该被注册为一个 Bean 在 Spring 应用上下文中
    public ServerEndpointExporter serverEndpointExporter() {
        // 创建并返回 ServerEndpointExporter 的实例,其中ServerEndpointExporter 是用来处理 WebSocket 连接的关键组件
        return new ServerEndpointExporter();
    }

}

3.后端注册webSocket服务

   后端:广播给所有客户端 ,在@OnMessage 将单一广播,切换为群体广播

   存储所有用户会话userId



import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;

//为 ConcurrentHashMap<String,WebSocketServer> 加入一个会话userId
@ServerEndpoint("/chatWebSocket/{userId}")
@Component
@Slf4j
public class WebSocketServer {

    /**
     *  [关于@OnOpen、@OnMessage、@OnClose、@OnError 中 Session session 的用意]
     *
     *  Session session: 主要用于代表一个单独的 WebSocket 连接会话.每当一个 WebSocket 客户端与服务器端点建立连接时,都会创建一个新的 Session 实例
     *      标识连接:每个 Session 对象都有一个唯一的 ID,可以用来识别和跟踪每个单独的连接。   ——>     可以使用 session.getId() 方法来获取这个 ID.对于日志记录、跟踪用户会话等方面非常有用。
     *      管理连接:可以通过 Session 对象来管理对应的 WebSocket 连接,例如发送消息给客户端、关闭连接等    ——>     session.getBasicRemote().sendText(message) 同步地发送文本消息,
     *                                                                                                 或者使用 session.getAsyncRemote().sendText(message) 异步地发送.可以调用 session.close() 来关闭 WebSocket 连接。
     *      获取连接信息:Session 对象提供了方法来获取连接的详细信息,比如连接的 URI、用户属性等。    ——>     可以使用 session.getRequestURI() 获取请求的 URI
     * **/

    //存储所有用户会话
    //ConcurrentHashMap<String,WebSocketServer> 中String 键(String类型)通常是用户ID或其他唯一标识符。允许服务器通过这个唯一标识符快速定位到对应的 WebSocketServer 实例,从而进行消息发送、接收或其他与特定客户端相关的操作
    //ConcurrentHashMap<String,WebSocketServer> 中为什么写 WebSocketServer 而不是其他,因为 WebSocketServer 作为一个实例,用于存储每个客户端连接。
    //所以在接下来@Onopen等使用中,当使用 ConcurrentHashMap<String,WebSocketServer> 时候,就不能单独使用 session, 需要添加一个诸如 userId 这样的会话来作为键。
    private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    private Session session;
    private String userId="";

    //建立连接时
    @OnOpen
    //获取会话userId
    //@PathParam: 是Java JAX-RS API(Java API for RESTful Web Services)的一部分,用于WebSocket和RESTful Web服务. 在WebSocket服务器端,@PathParam 注解用于提取客户端连接URL中的参数值。
    public void onOpen(Session session, @PathParam("userId") String userId){
        this.session = session;             //当前WebSocket连接的 Session 对象存储在 WebSocketServer 实例 【这样做是为了在后续的通信过程中(例如在处理消息、关闭连接时),您可以使用 this.session 来引用当前连接的 Session 对象。】
        this.userId = userId;               //存储前端传来的 userId;
        webSocketMap.put(userId,this);      //WebSocketServer 实例与用户userId关联,并将这个关联存储在 webSocketMap 中。【其中this: 指的是当前的 WebSocketServer 实例】
        log.info("会话id:" + session.getId() + "对应的会话用户:" + userId + "【进行链接】");
        log.info("【websocket消息】有新的连接, 总数:{}", webSocketMap.size());
        System.out.println("会话id:" + session.getId() + " 对应的会话用户:" + userId + " 【进行链接】");
        System.out.println("【websocket消息】有新的连接, 总数: "+webSocketMap.size());
    }

    //接收客户端消息
    @OnMessage
    public void onMessage(String message,Session session) throws IOException {
        //当从客户端接收到消息时调用
        log.info("会话id"+ session.getId() +"对应的会话用户:" + userId + "的消息:" + message);
        System.out.println("会话id: "+ session.getId() +" 对应的会话用户:" + userId + " 的消息: " + message);
        //修改 onMessage 方法来实现广播: 当服务器接收到消息时,不是只发送给消息的发送者,而是广播给所有连接的客户端。 ——> (实现群聊)
        //判断message传来的消息不为空时,才能在页面上进行显示
        if(message != null && !message.isEmpty()){
            JSONObject obj = new JSONObject();
            obj.put("userId", userId);
            obj.put("message", message);
            // 封装成 JSON (Java对象转换成JSON格式的字符串。)
            String json = new ObjectMapper().writeValueAsString(obj);
            for(WebSocketServer client :webSocketMap.values()){
                client.session.getBasicRemote().sendText(json);
            }
        }
    }

    //链接关闭时
    @OnClose
    public void onClose(Session session){
        //关闭浏览器时清除存储在 webSocketMap 中的会话对象。
        webSocketMap.remove(userId);
        log.info("会话id:" + session.getId() + "对应的会话用户:" + userId + "【退出链接】");
        log.info("【websocket消息】有新的连接, 总数:{}", webSocketMap.size());
        System.out.println("会话id:" + session.getId() + " 对应的会话用户:" + userId + " 【退出链接】");
        System.out.println("【websocket消息】有新的连接, 总数: "+ webSocketMap.size());
    }


    //链接出错时
    @OnError
    public void onError(Session session,Throwable throwable){
        //错误提示
        log.error("出错原因 " + throwable.getMessage());
        System.out.println("出错原因 " + throwable.getMessage());
        //抛出异常
        throwable.printStackTrace();
    }
}

如果不是群发,一对一对话 单一广播, onMessage方法如下:

//接收客户端消息
    @OnMessage
    public void onMessage(String message,Session session) throws IOException {
        //当从客户端接收到消息时调用
        log.info("会话id:" + session.getId() + ": 的消息" + message);
        session.getBasicRemote().sendText("回应" + "[" + message + "]");
}

 

二:前端

在 Vue 中使用 WebSocket 并不需要引入专门的库或框架,因为 WebSocket 是一个浏览器内置的 API,可以直接在任何现代浏览器中使用。但是,你可能需要编写一些代码来适当地处理 WebSocket 连接、消息的发送与接收、错误处理以及连接的关闭。

前端: (前端整体没有发生太大变化,只加了一个userId用于向后端传输)

<template>
  <div class="iChat">
    <div class="container">
      <div class="content">
        <div class="item item-center"><span>今天 10:08</span></div>
        <div class="item" v-for="(item, index) in receivedMessage" :key="index" :class="{'item-right':isCurrentUser(item),'item-left':!isCurrentUser(item)}">
          <!-- 右结构 -->
          <div v-if="isCurrentUser(item)" style="display: flex">
            <div class="bubble" :class="{'bubble-right':isCurrentUser(item),'bubble-left':!isCurrentUser(item)}">{{item.message}}</div>
            <div class="avatar">
              <img
                src="http://192.168.0.134/img/20250701114048Tclu5k.png"
              />
              {{item.userId}}
            </div>
          </div>
          <!-- 左结构 -->
          <div v-else style="display: flex">
            <div class="avatar">
              {{item.userId}}
              <img
                src="http://192.168.0.134/img/202507031603386lQ4ft.png"
              />
            </div>
            <div class="bubble" :class="{'bubble-right':isCurrentUser(item),'bubble-left':!isCurrentUser(item)}">{{item.message}}</div>
          </div>
        </div>
      </div>
      <div class="input-area">
        <!-- 文本框 -->
        <textarea v-model="message" id="textarea"></textarea>
        <div class="button-area">
          <button id="send-btn" @click="sendMessage()">发 送</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      ws:null,
      message:'',
      receivedMessage:[],
      currentUserId:"用户1" + Math.floor(Math.random() * 1000)
    };
  },
  mounted() {
    this.initWebSocket()
  },
  methods: {
    //建立webSocket连接
    initWebSocket(){

      //定义用户的,并加入到下述链接中,且记不要少了/
      const userId = this.currentUserId;
      //链接接口
      this.ws = new WebSocket('ws://localhost:9000/jsonflow/chatWebSocket/' + userId)

      console.log('ws://localhost:9000/jsonflow/chatWebSocket/' + userId);

      //打开事件
      this.ws.onopen = function(){
        console.log("websocket已打开");
      }

      //消息事件
      this.ws.onmessage = (event) => {
        //接到后端传来数据 - 并对其解析
        this.receivedMessage.push(JSON.parse(event.data));
        console.log(this.receivedMessage)
      }

      //关闭事件
      this.ws.onclose = function() {
        console.log("websocket已关闭");
      };

      //错误事件
      this.ws.onerror = function() {
        console.log("websocket发生了错误");
      };

    },

    //发送消息到服务器
    sendMessage(){
      this.ws.send(this.message);
      this.message = '';
    },

    //判断是否是当前用户(boolean值)
    isCurrentUser(item){
      return item.userId == this.currentUserId
    }
  },
};
</script>
<style lang="scss" scoped>
.container{
  height: 666px;
  border-radius: 4px;
  border: 0.5px solid #e0e0e0;
  background-color: #f5f5f5;
  display: flex;
  flex-flow: column;
  overflow: hidden;
}
.content{
  width: calc(100% - 40px);
  padding: 20px;
  overflow-y: scroll;
  flex: 1;
}
.content:hover::-webkit-scrollbar-thumb{
  background:rgba(0,0,0,0.1);
}
.bubble{
  max-width: 400px;
  padding: 10px;
  border-radius: 5px;
  position: relative;
  color: #000;
  word-wrap:break-word;
  word-break:normal;
}
.item-left .bubble{
  margin-left: 15px;
  background-color: #fff;
}
.item-left .bubble:before{
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-left: 10px solid transparent;
  border-top: 10px solid transparent;
  border-right: 10px solid #fff;
  border-bottom: 10px solid transparent;
  left: -20px;
}
.item-right .bubble{
  margin-right: 15px;
  background-color: #9eea6a;
}
.item-right .bubble:before{
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-left: 10px solid #9eea6a;
  border-top: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 10px solid transparent;
  right: -20px;
}
.item{
  margin-top: 15px;
  display: flex;
  width: 100%;
}
.item.item-right{
  justify-content: flex-end;
}
.item.item-center{
  justify-content: center;
}
.item.item-center span{
  font-size: 12px;
  padding: 2px 4px;
  color: #fff;
  background-color: #dadada;
  border-radius: 3px;
  -moz-user-select:none; /*火狐*/
  -webkit-user-select:none; /*webkit浏览器*/
  -ms-user-select:none; /*IE10*/
  -khtml-user-select:none; /*早期浏览器*/
  user-select:none;
}

.avatar img{
  width: 42px;
  height: 42px;
  border-radius: 50%;
}
.input-area{
  border-top:0.5px solid #e0e0e0;
  height: 150px;
  display: flex;
  flex-flow: column;
  background-color: #fff;
}
textarea{
  flex: 1;
  padding: 5px;
  font-size: 14px;
  border: none;
  cursor: pointer;
  overflow-y: auto;
  overflow-x: hidden;
  outline:none;
  resize:none;
}
.button-area{
  display: flex;
  height: 40px;
  margin-right: 10px;
  line-height: 40px;
  padding: 5px;
  justify-content: flex-end;
}
.button-area button{
  width: 80px;
  border: none;
  outline: none;
  border-radius: 4px;
  float: right;
  cursor: pointer;
}

/* 设置滚动条的样式 */
::-webkit-scrollbar {
  width:10px;
}
/* 滚动槽 */
::-webkit-scrollbar-track {
  -webkit-box-shadow:inset006pxrgba(0,0,0,0.3);
  border-radius:8px;
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
  border-radius:10px;
  background:rgba(0,0,0,0);
  -webkit-box-shadow:inset006pxrgba(0,0,0,0.5);
}
</style>

三:最终效果

 页面效果:

 

 

后端日志:

四:后续:

       代码中用户头像我是用nginx代理的,写死了一个头像。后期前端可以根据系统当前登录人取他的头像,选中头像后,与某个人对话。总之基本对话功能都实现了,欢迎白嫖党一键三连,哈哈哈哈哈哈~~~~~~~~~~~


网站公告

今日签到

点亮在社区的每一天
去签到