SpringBoot实现WebSocket实现用户一对一和一对多信息的发送

SpringBoot实现WebSocket实现用户一对一和一对多信息的发送

(个人学习记录)

这里我是通过controller、service、和serviceImpl的结构来实现的,但是还多了一个核心端点类 (WebSocketEndpoint):这个类是专门负责管理 WebSocket 协议的连接生命周期(建立、关闭、接收消息、处理错误),并且对接业务层(WebSocketService)。

一、首先我们导入相关依赖
1、导入websocket的maven依赖:
xml 复制代码
//websocket依赖导入    
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>

因为后面我们要用到hutool工具来处理json格式的数据,所以我们这里也导入hutool的一个json处理maven:

xml 复制代码
    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-json</artifactId>
      <version>5.8.28</version>
    </dependency>

下面是我的一个xml配置,可以一下:只是一些非常简单的maven,主要是为了简单的学习websocket在springboot方面该怎么实现消息的发送和接收。

xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>websocketChat</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>websocketChat</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
  </parent>


  <dependencies>

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


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

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.22</version>
    </dependency>

    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-json</artifactId>
      <version>5.8.28</version>
    </dependency>

  </dependencies>
</project>
二、构建消息接收类

就是一个普通的bean

java 复制代码
package org.example.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Chat {

    //主键id
    private Integer id;
    //发送者id
    private Integer userId;
    //接收者id
    private Integer targetUserId;
    //创建时间
    private LocalDateTime createTime;
    //发送的内容
    private String content;
    // broadcast为广播,single为一对一(不传则默认一对一);
    private String msgType;

}
三、websocketConfig配置
java 复制代码
package org.example.config;

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

/**
 * WebSocket配置类:启用@ServerEndpoint注解的支持
 */
@Configuration
public class WebSocketConfig {
    /**
     * 注入ServerEndpointExporter,自动注册@ServerEndpoint标记的类
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
四、构建核心端点类(这个是最重要的部分)

这个类是最重要的,这个类负责处理客户端发送的websocket连接请求。

这个类的核心职责是:作为 WebSocket 服务端的入口,接收前端的 WebSocket 连接请求,触发连接建立 / 关闭 / 消息 / 异常等生命周期事件,并将具体的业务逻辑委托给 WebSocketService 处理,实现前端与后端的实时双向通信(比如聊天、消息推送等场景)。

java 复制代码
package org.example.chat;

import lombok.extern.slf4j.Slf4j;
import org.example.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.SpringConfigurator;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;


@ServerEndpoint(value = "/ws/{userId}")
@Component
@Slf4j
public class WebSocketEndpoint {

    // 解决WebSocket中注入Spring Bean的问题(核心)
    private static WebSocketService webSocketService;

    @Autowired
    public void setWebSocketService(WebSocketService webSocketService) {
        WebSocketEndpoint.webSocketService = webSocketService;
    }

    private Integer userId;

    /**
     * 连接建立时触发
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") Integer userId) {
        this.userId = userId;
        webSocketService.connect(userId, session);
    }

    /**
     * 连接关闭时触发
     */
    @OnClose
    public void onClose() {
        webSocketService.disconnect(this.userId);
    }

    /**
     * 收到客户端消息时触发
     */
    @OnMessage
    public void onMessage(String message) {
        webSocketService.handleMessage(message);
    }

    /**
     * 发生错误时触发
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("【WebSocket】用户{}连接出错:{}", this.userId, error.getMessage());
        error.printStackTrace();
    }
}

下面是方法(注解)的一些解释:

方法 注解 触发时机 核心逻辑
onOpen @OnOpen 客户端成功建立 WebSocket 连接时 1. 保存当前连接的用户 ID;2. 调用 webSocketService.connect() 初始化连接(如记录在线用户)。
onClose @OnClose 客户端关闭 WebSocket 连接时(主动 / 被动) 调用 webSocketService.disconnect() 清理连接(如移除在线用户)。
onMessage @OnMessage 服务端收到客户端通过 WebSocket 发送的消息时 调用 webSocketService.handleMessage() 处理消息(如解析、转发消息)。
onError @OnError WebSocket 连接发生错误时(如断连、解析失败) 打印错误日志,定位连接异常原因。

**重点注意:**在这个端点类(WebSocketEndpoint),直接用 @Autowired 注入 WebSocketService 行不通,必须用「静态字段 + Setter 注入」的特殊方式,核心原因是 WebSocket 端点的实例管理方与 Spring 容器不一致,这是 WebSocket 结合 Spring 开发的核心痛点。

也就是必须用下面的方式注入webSocketService(在这个端点类里面):

java 复制代码
  // 解决WebSocket中注入Spring Bean的问题(核心)
    private static WebSocketService webSocketService;

    @Autowired
    public void setWebSocketService(WebSocketService webSocketService) {
        WebSocketEndpoint.webSocketService = webSocketService;
    }

不能用下面这种方式注入:(只是在这个端点类不可以这样,在controller里面是可以这样注入的)

java 复制代码
 @Autowired
 private  WebSocketService webSocketService;
五、构建WebSocketController
JAVA 复制代码
package org.example.controller;

import org.example.entity.Chat;
import org.example.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/websocket")
public class WebSocketController {

    @Autowired
    private WebSocketService webSocketService;

    /**
     * 单点发送消息
     */
    @PostMapping("/send")
    public String sendMessage(@RequestBody Chat chat) {
        try {
            if (chat.getReceiverId() == null || chat.getContent() == null) {
                return "参数错误:targetUserId和content不能为空";
            }
            //可以在这里存储数据信息
            webSocketService.sendOneMessage(chat.getReceiverId(), chat.getContent());
            return "一对一消息发送成功";
        } catch (Exception e) {
            return "一对一消息发送失败:" + e.getMessage();
        }
    }

    /**
     * 广播消息
     */
    @PostMapping("/sendall")
    public String sendAllMessage(@RequestParam String message) {
        try {
            webSocketService.sendAllMessage(message);
            return "广播消息发送成功";
        } catch (Exception e) {
            return "广播消息发送失败:" + e.getMessage();
        }
    }

    /**
     * 批量发送消息
     */
    @PostMapping("/sendmore")
    public String sendMoreMessage(@RequestParam Integer[] userIds, @RequestParam String message) {
        try {
            webSocketService.sendMoreMessage(userIds, message);
            return "批量消息发送成功";
        } catch (Exception e) {
            return "批量消息发送失败:" + e.getMessage();
        }
    }
}
六、构建service和serviceImpl

service:(因为端点类里也注入了这个service,所以里面有一些端点类的实现,当然也有controller类的实现)

java 复制代码
package org.example.service;


public interface WebSocketService {

    /**
     * 建立连接时初始化用户会话
     * @param userId 用户ID
     * @param session WebSocket会话
     */
    void connect(Integer userId, javax.websocket.Session session);

    /**
     * 断开连接时清理用户会话
     * @param userId 用户ID
     */
    void disconnect(Integer userId);

    /**
     * 处理客户端发送的消息
     * @param message 客户端原始消息
     */
    void handleMessage(String message);

    /**
     * 广播消息给所有在线用户
     * @param message 消息内容
     */
    void sendAllMessage(String message);

    /**
     * 单点发送消息
     * @param userId 目标用户ID
     * @param message 消息内容
     */
    void sendOneMessage(Integer userId, String message);

    /**
     * 给多个用户发送消息
     * @param userIds 目标用户ID数组
     * @param message 消息内容
     */
    void sendMoreMessage(Integer[] userIds, String message);
}

serviceImpl:

java 复制代码
package org.example.service.impl;

import cn.hutool.json.JSONException;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.example.service.WebSocketService;
import org.springframework.stereotype.Service;

import javax.websocket.Session;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {

    // 存放所有WebSocket连接实例
    private static CopyOnWriteArraySet<Object> webSocketSet = new CopyOnWriteArraySet<>();
    // 存放用户ID与Session的映射(核心:保证线程安全)
    private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<>();

    /**
     * 用户连接时初始化
     */
    @Override
    public void connect(Integer userId, Session session) {
        try {
            webSocketSet.add(this);
            sessionPool.put(userId, session);
            log.info("【WebSocket】用户{}连接成功,当前在线总数:{}", userId, webSocketSet.size());
        } catch (Exception e) {
            log.error("【WebSocket】用户{}连接初始化失败", userId, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 用户断开连接时清理
     */
    @Override
    public void disconnect(Integer userId) {
        try {
            webSocketSet.remove(this);
            sessionPool.remove(userId);
            log.info("【WebSocket】用户{}断开连接,当前在线总数:{}", userId, webSocketSet.size());
        } catch (Exception e) {
            log.error("【WebSocket】用户{}断开连接失败", userId, e);
        }
    }

    /**
     * 处理客户端通过WebSocket发送的消息
     */
    @Override
    public void handleMessage(String message) {
        log.info("【WebSocket】收到客户端消息:{}", message);
        try {
            JSONObject msgJson = new JSONObject(message);
            Integer senderId = msgJson.getInt("userId");
            Integer receiverId = msgJson.getInt("targetUserId");
            String content = msgJson.getStr("content");

            // 构造转发消息
            JSONObject sendMsg = new JSONObject();
            sendMsg.put("senderId", senderId);
            sendMsg.put("content", content);

            // 转发给目标用户
            this.sendOneMessage(receiverId, sendMsg.toString());
        } catch (JSONException e) {
            log.error("【WebSocket】JSON解析失败,消息格式错误", e);
        } catch (Exception e) {
            log.error("【WebSocket】消息转发失败", e);
        }
    }

    /**
     * 广播消息给所有在线用户
     */
    @Override
    public void sendAllMessage(String message) {
        log.info("【WebSocket】广播消息:{}", message);
        for (Object webSocket : webSocketSet) {
            try {
                // 遍历所有Session并发送
                for (Session session : sessionPool.values()) {
                    if (session.isOpen()) {
                        session.getAsyncRemote().sendText(message);
                    }
                }
            } catch (Exception e) {
                log.error("【WebSocket】广播消息发送失败", e);
            }
        }
    }

    /**
     * 单点发送消息
     */
    //接收者userId
    @Override
    public void sendOneMessage(Integer userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null && session.isOpen()) {
            try {
                log.info("【WebSocket】给用户{}发送单点消息:{}", userId, message);
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                log.error("【WebSocket】给用户{}发送单点消息失败", userId, e);
            }
        } else {
            log.warn("【WebSocket】用户{}未在线,消息发送失败:{}", userId, message);
        }
    }

    /**
     * 给多个用户发送消息(修正原代码String转Integer的问题)
     */
    @Override
    public void sendMoreMessage(Integer[] userIds, String message) {
        for (Integer userId : userIds) {
            Session session = sessionPool.get(userId);
            if (session != null && session.isOpen()) {
                try {
                    log.info("【WebSocket】给用户{}发送批量消息:{}", userId, message);
                    session.getAsyncRemote().sendText(message);
                } catch (Exception e) {
                    log.error("【WebSocket】给用户{}发送批量消息失败", userId, e);
                }
            }
        }
    }
}
七、进行测试:

我们先启动后端,

通过apifox来实现测试:

1、首先创建websocket连接:

这里我已经构建了三个用户了

2、发送websocket连接

注意websocket发送的请求是ws开头,不是http或https.

在这里我把要发送的消息的json格式也贴上期了

我们点击连接用户1和用户2,用户1的id为1001,用户2的id为1002.

可以看到上面我们是用户1给用户2发送连接,

下面的图片连接和发送的结果:(这里面的逻辑可以在后端修改,通过这个发送的信息默认调用@onMessage注解标注的方法,在端点类里面)

用户1:

可以看到当连接成功后我们的连接按钮变为断开,消息框点击发送按钮后,也发送了消息。

用户2:也接收到了信息,

最后我们通过http来实现消息的发送,也就是调用controller方法。

当然,在发送之前必须要websocket进行连接,不然端点类没有相关用户,也不知道给谁发消息了,

如果用户不在,我们可以在后端把数据存储起来,等用户上线后直接从数据库拉取。(当然需要实现后端的逻辑,这里没有写)

在用户2进行查看是否发送过去:

可以看到http也发送成功了

相关推荐
咚为8 小时前
Rust Print 终极指南:从底层原理到全场景实战
开发语言·后端·rust
二哈喇子!8 小时前
SpringBoot项目右上角选择ProjectNameApplication的配置
java·spring boot
二哈喇子!8 小时前
基于Spring Boot框架的车库停车管理系统的设计与实现
java·spring boot·后端·计算机毕业设计
二哈喇子!9 小时前
基于SpringBoot框架的水之森海底世界游玩系统
spring boot·旅游
二哈喇子!9 小时前
Java框架精品项目【用于个人学习】
java·spring boot·学习
二哈喇子!9 小时前
基于SpringBoot框架的网上购书系统的设计与实现
java·大数据·spring boot
二哈喇子!10 小时前
基于JavaSE的淘宝卖鞋后端管理系统的设计与实现
java·spring boot·spring
Coder_Boy_10 小时前
基于SpringAI的在线考试系统-智能考试系统-学习分析模块
java·开发语言·数据库·spring boot·ddd·tdd
S-X-S10 小时前
常用设计模式+集成websocket
websocket·设计模式
高山上有一只小老虎11 小时前
mybatisplus实现分页查询
java·spring boot·mybatis