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也发送成功了

相关推荐
weixin_439706252 小时前
spring boot+nacos+gateway+sentinel的简单例子
spring boot·gateway·sentinel
予枫的编程笔记2 小时前
从入门到精通:RabbitMQ全面解析与实战指南
java·开发语言·后端·rabbitmq·ruby
superman超哥2 小时前
Rust 异步性能最佳实践:高并发场景的极致优化
开发语言·后端·rust·最佳实践·异步性能·高并发场景
未来之窗软件服务2 小时前
幽冥大陆(八十一)安检原理识别管制刀具原理go语言—东方仙盟练气期
开发语言·后端·golang·安检系统
Object~2 小时前
1.golang项目结构
开发语言·后端·golang
BingoGo2 小时前
2026 年 PHP 开发者进阶 快速高效开发学习习惯
后端·php
IT_陈寒2 小时前
Vite 3实战:我用这5个优化技巧让HMR构建速度提升了40%
前端·人工智能·后端
csdnZCjava2 小时前
Spring MVC工作原理 及注解说明
java·后端·spring·mvc
代码方舟2 小时前
Java Spring Boot 实战:构建天远高并发个人消费能力评估系统
java·大数据·spring boot·python