SpringBoot整合WebSocket服务,处理不同的消息

WebSocketWebSocket 是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信(允许服务器主动发送信息给客户端)。

Springboot中整合WebSocket

pom依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.21</version>
        </dependency>

配置类

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

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

/**
 * 开启WebSocket支持
 */

@Configuration
public class WebSocketConfig implements ServletContextInitializer {

    /**
     * 这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {

    }

}
 

webSocket服务类

import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
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.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;


@ServerEndpoint("/websocket/{id}")
@Component
@Slf4j
public class MessagePushSever {

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private ConnectedUserWrapper connectedUserWrapper;

    @Data
    @AllArgsConstructor
    public  static  class ConnectedUserWrapper{
       private Session session;
        private String id;
    }


    // session集合,存放对应的session
    private static ConcurrentHashMap<String, ConnectedUserWrapper> sessionPool = new ConcurrentHashMap<>();

    // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
    private static CopyOnWriteArraySet<MessagePushSever> webSocketSet = new CopyOnWriteArraySet<>();

    /**
     * 建立WebSocket连接
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session,@PathParam("id")String id) {
        System.out.println("on Open!");
        try {
            try {
                ConnectedUserWrapper userWrapper = sessionPool.get(id);
                // connectedUserWrapper不为空,已经有人登录相同账号,则销毁并移除
                if (userWrapper != null) {
                    sendKidOfflineMessage(userWrapper.getSession());
                    userWrapper.getSession().close();
                }
            } catch (IOException e) {
                log.error("重复登录异常,错误信息:" + e.getMessage(), e);
            }
            // 建立连接
            ConnectedUserWrapper connectedUserWrapper = new ConnectedUserWrapper(session, id);
            sessionPool.put(id, connectedUserWrapper);
            this.connectedUserWrapper = connectedUserWrapper;
            webSocketSet.add(this);
            log.info("建立连接完成,当前在线人数为:{}", webSocketSet.size());
        }catch (Exception e){
            e.printStackTrace();
            try {
                session.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    /**
     * 发送当前用户被挤掉线消息
     * @param session
     * @throws IOException
     */
    private void sendKidOfflineMessage(Session session) throws IOException {
        if(session!=null){
            MessageWrapper<String> partsFeedbackMessageWrapper = new MessageWrapper<>("error", "抱歉,您的账号已在其他位置登录,如果不是您操作,请修改密码!");
            session.getBasicRemote().sendText(JSONUtil.toJsonStr(partsFeedbackMessageWrapper));
        }
    }

    /**
     * 发送错误消息
     * @param session
     */
    private void sendNotVerifyAndClose(Session session){
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发生错误
     *
     * @param e
     */
    @OnError
    public void onError(Throwable e) {
        log.error("出现错误:"+e.getMessage(),e);
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null){
            //避免挤掉他人时,上面已经移除掉了,导致把刚登陆的账号也移除掉了,
            String id = this.connectedUserWrapper.getId();
            sessionPool.remove(id);
            log.info("下线用户:{},剩余在线人数:{}人",id,webSocketSet.size());
        }
    }

    /**
     * 接收客户端消息
     *
     * @param message 接收的消息
     */
    @OnMessage
    public void onMessage(String message) {

        if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
            log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
        }
    }





    public static List<String> getOnlineUser(){
        return  sessionPool.values().stream().map(ConnectedUserWrapper::getId).collect(Collectors.toList());
    }

    public static ConcurrentHashMap<String, ConnectedUserWrapper> getOnlineUserMap(){
        return  sessionPool;
    }

    /**
     * 推送消息到指定用户
     *
     * @param userId  用户ID
     * @param message 发送的消息
     */
    public static void sendMessageByUser(String userId, String message) {
        log.info("用户ID:" + userId + ",推送内容:" + message);
        ConnectedUserWrapper connectedUserWrapper = sessionPool.get(userId);
        if(connectedUserWrapper!=null) {
            try {
                connectedUserWrapper.getSession().getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("推送消息到指定用户发生错误:" + e.getMessage(), e);
            }
        }
    }

    /**
     * 群发消息
     *
     * @param message 发送的消息
     */
    public static void sendAllMessage(String message) {
        log.info("发送消息:{}", message);
        for (MessagePushSever webSocket : webSocketSet) {
            try {
                webSocket.connectedUserWrapper.getSession().getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("群发消息发生错误:" + e.getMessage(), e);
            }
        }
    }

}

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket Test</title>

    <script  src="./config.js"></script>
</head>
<body>
    <h1>WebSocket Test</h1>
    <input type="text" id="messageInput" placeholder="Enter your message">
    <button onclick="sendMessage()">Send</button>
  
    <script>

        var ws = new WebSocket("ws://localhost:8090/websocket/a");

        ws.onopen = function() {
            console.log("WebSocket connection established");
        };

        ws.onmessage = function(event) {
            
            document.getElementById("response").innerText = event.data;
           
        };

        ws.onclose = function() {
            console.log("WebSocket connection closed");
        };

        function sendMessage() {
            var message = document.getElementById("messageInput").value;
            ws.send(message);
        }


    </script>
</body>
</html>

处理不同的消息

修改前端

定义一个事件注册器  config.js 文件


/**
 * 发布订阅
 */

class EventDispatcher {
    list = {}
    constructor() {
        this.list = {}
    }

    /**
     * 注册事件
     * @param event
     * @param callback
     */
    on = (event, callback) => {
        if (!this.list[event]) {
            this.list[event] = []
        }
        this.list[event].push(callback)
    };

    /**
     * 发布消息
     * @param event
     * @param args
     */
    emit = (event, ...args) => {
        if (!this.list[event]) {
            console.log('事件未注册', event, args);
            return;
        }
        this.list[event].forEach((callback) => {
            callback.apply(this, args)
        });
    };
}


完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>WebSocket Test</title>

    <script  src="./config.js"></script>
</head>
<body>
    <h1>WebSocket Test</h1>
    <input type="text" id="messageInput" placeholder="Enter your message">
    <button onclick="sendMessage()">Send</button>
    <p id="response"></p>

    <script>


        let dispatcher = new EventDispatcher();

        // 注册 test1 事件
        dispatcher.on("test1", (data) => {
            console.log("test1", data)
        })
        // 注册 test2 事件
        dispatcher.on("test2", (data) => {
            console.log("test2", data)
        })

        var ws = new WebSocket("ws://localhost:8090/websocket/a");

        ws.onopen = function() {
            console.log("WebSocket connection established");
        };

        ws.onmessage = function(event) {
            // 接收的消息类型为 {type:"", data:""}
            let data = JSON.parse(event.data);
            document.getElementById("response").innerText = data.data;
            // 触发消息事件
            dispatcher.emit(data.type, data)
        };

        ws.onclose = function() {
            console.log("WebSocket connection closed");
        };

    </script>
</body>
</html>

服务发送消息

1. 定义一个消息实体类  用于接收和发送消息

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;

@Data
@ToString
@AllArgsConstructor
public class MessageWrapper<T>{
    // 使用 type 区分消息
    private String type;
    private T data;
}

2. 调用上述 sendMessageByUser() 方法进行发生消息

       MessageWrapper<String> partsFeedbackMessageWrapper = new MessageWrapper<>("test1", "发送test1类型的消息");
        sendMessageByUser(id, JSONUtil.toJsonStr(partsFeedbackMessageWrapper));

        partsFeedbackMessageWrapper = new MessageWrapper<>("test2", "发送test2类型的消息");
        sendMessageByUser(id, JSONUtil.toJsonStr(partsFeedbackMessageWrapper));

修改服务端

前端发送消息 使用 type 定义不同的消息类型

function sendMessage() {
	var message = document.getElementById("messageInput").value;
	let json = {type: 'test1', data: "向test1发送消息:" + message};
	ws.send(JSON.stringify(json));
	json = {type: 'test2', data: "向test2发送消息:" + message};
	ws.send(JSON.stringify(json));
	json = {type: 'test3', data: "向test3发送消息:" + message};
	ws.send(JSON.stringify(json));
	json = {type: 'test4', data: "向test4发送消息:" + message};
	ws.send(JSON.stringify(json));
}

1. 使用 bean 名称处理不同的消息

/**
 * 接收客户端消息
 *
 * @param message 接收的消息
 */
@OnMessage
public void onMessage(String message) {

	if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
		log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
		MessageWrapper messageWrapper = JSONUtil.toBean(message, MessageWrapper.class);
		ListenerBean listenerBean = SpringUtil.getBean(messageWrapper.getType(), ListenerBean.class);
		listenerBean.receive(messageWrapper);
	}
}



// 定义一个 接口
public interface ListenerBean {

    void receive(MessageWrapper messageWrapper);

}


// 不同的消息类型实现该接口 使用 不同的 bean 名称


/**
    消息类型 1 
*/
@Slf4j
@Service("test1")
public class Test1Listener implements ListenerBean {

    @Override
    public void receive(MessageWrapper messageWrapper) {
        log.info("test1接收消息:{}", messageWrapper);
    }
}

/**
    消息类型 2 
*/
@Slf4j
@Service("test2")
public class Test2Listener implements ListenerBean{

    @Override
    public void receive(MessageWrapper messageWrapper) {
        log.info("test2接收消息:{}", messageWrapper);
    }
}



2. 使用  ApplicationEventPublisher 事件监听处理不同的消息

// 1. 在 MessagePushSever 中 注入事件转发器

private static ApplicationEventPublisher applicationEventPublisher;


@Autowired
public void setSysUserService(ApplicationEventPublisher applicationEventPublisher){
	MessagePushSever.applicationEventPublisher = applicationEventPublisher;
}

2. 修改 消息接收
/**
 * 接收客户端消息
 *
 * @param message 接收的消息
 */
@OnMessage
public void onMessage(String message) {

	if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
		log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
		MessageWrapper messageWrapper = JSONUtil.toBean(message, MessageWrapper.class);
		MessagePushSever.applicationEventPublisher.publishEvent(messageWrapper);
	}
}



3. 消息监听器 使用注解 @EventListener()  使用参数 condition 来监听特定消息
/**
    消息监听
*/
@Slf4j
@Component
public class Test3Listener {

    @EventListener(condition = "#messageWrapper.type == 'test3'")
    public void receive(MessageWrapper messageWrapper) {
        log.info("test3接收消息:{}", messageWrapper);
    }
}

/**
    消息监听
*/

@Slf4j
@Component
public class Test4Listener {

    @EventListener(condition = "#messageWrapper.type == 'test4'")
    public void receive(MessageWrapper messageWrapper) {
        log.info("test4接收消息:{}", messageWrapper);
    }

}

服务端完整代码


@ServerEndpoint("/websocket/{id}")
@Component
@Slf4j
public class MessagePushSever {

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private ConnectedUserWrapper connectedUserWrapper;

    @Data
    @AllArgsConstructor
    public  static  class ConnectedUserWrapper{
       private Session session;
        private String id;
    }

    private static ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    public void setSysUserService(ApplicationEventPublisher applicationEventPublisher){
        MessagePushSever.applicationEventPublisher = applicationEventPublisher;
    }

    // session集合,存放对应的session
    private static ConcurrentHashMap<String, ConnectedUserWrapper> sessionPool = new ConcurrentHashMap<>();

    // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
    private static CopyOnWriteArraySet<MessagePushSever> webSocketSet = new CopyOnWriteArraySet<>();

    /**
     * 建立WebSocket连接
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session,@PathParam("id")String id) {
        System.out.println("on Open!");
        try {
            try {
                ConnectedUserWrapper userWrapper = sessionPool.get(id);
                // connectedUserWrapper不为空,已经有人登录相同账号,则销毁并移除
                if (userWrapper != null) {
                    sendKidOfflineMessage(userWrapper.getSession());
                    userWrapper.getSession().close();
                }
            } catch (IOException e) {
                log.error("重复登录异常,错误信息:" + e.getMessage(), e);
            }
            // 建立连接
            ConnectedUserWrapper connectedUserWrapper = new ConnectedUserWrapper(session, id);
            sessionPool.put(id, connectedUserWrapper);
            this.connectedUserWrapper = connectedUserWrapper;
            webSocketSet.add(this);
            log.info("建立连接完成,当前在线人数为:{}", webSocketSet.size());
        }catch (Exception e){
            e.printStackTrace();
            try {
                session.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    /**
     * 发送当前用户被挤掉线消息
     * @param session
     * @throws IOException
     */
    private void sendKidOfflineMessage(Session session) throws IOException {
        if(session!=null){
            MessageWrapper<String> partsFeedbackMessageWrapper = new MessageWrapper<>("error", "抱歉,您的账号已在其他位置登录,如果不是您操作,请修改密码!");
            session.getBasicRemote().sendText(JSONUtil.toJsonStr(partsFeedbackMessageWrapper));
        }
    }

    /**
     * 发送错误消息
     * @param session
     */
    private void sendNotVerifyAndClose(Session session){
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 发生错误
     *
     * @param e
     */
    @OnError
    public void onError(Throwable e) {
        log.error("出现错误:"+e.getMessage(),e);
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null){
            //避免挤掉他人时,上面已经移除掉了,导致把刚登陆的账号也移除掉了,
            String id = this.connectedUserWrapper.getId();
            sessionPool.remove(id);
            log.info("下线用户:{},剩余在线人数:{}人",id,webSocketSet.size());
        }
    }

    /**
     * 接收客户端消息
     *
     * @param message 接收的消息
     */
    @OnMessage
    public void onMessage(String message) {

        if(this.connectedUserWrapper!=null && this.connectedUserWrapper.getId()!=null) {
            log.info("收到用户[{}]发来的消息::{}",this.connectedUserWrapper.getId(), message);
            MessageWrapper messageWrapper = JSONUtil.toBean(message, MessageWrapper.class);
     
            MessagePushSever.applicationEventPublisher.publishEvent(messageWrapper);
        }
    }





    public static List<String> getOnlineUser(){
        return  sessionPool.values().stream().map(ConnectedUserWrapper::getId).collect(Collectors.toList());
    }

    public static ConcurrentHashMap<String, ConnectedUserWrapper> getOnlineUserMap(){
        return  sessionPool;
    }

    /**
     * 推送消息到指定用户
     *
     * @param userId  用户ID
     * @param message 发送的消息
     */
    public static void sendMessageByUser(String userId, String message) {
        log.info("用户ID:" + userId + ",推送内容:" + message);
        ConnectedUserWrapper connectedUserWrapper = sessionPool.get(userId);
        if(connectedUserWrapper!=null) {
            try {
                connectedUserWrapper.getSession().getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("推送消息到指定用户发生错误:" + e.getMessage(), e);
            }
        }
    }

    /**
     * 群发消息
     *
     * @param message 发送的消息
     */
    public static void sendAllMessage(String message) {
        log.info("发送消息:{}", message);
        for (MessagePushSever webSocket : webSocketSet) {
            try {
                webSocket.connectedUserWrapper.getSession().getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("群发消息发生错误:" + e.getMessage(), e);
            }
        }
    }

}

参考:

SpringBoot整合WebSocket(session共享实现)_websocket session-优快云博客

WebSocket迷们请注意!你的集群方案来啦,火速围观!_idwebsocketserver-优快云博客

Springboot整合websocket(附详细案例代码)_springboot websocket-优快云博客

### Spring Boot 整合 WebSocket 示例教程 #### 3.1 添加依赖项 为了在 Spring Boot 中使用 WebSocket,需要引入相应的启动器。这可以通过修改 `build.gradle` 文件来完成: ```groovy dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-websocket' } ``` 上述代码片段展示了如何向项目中添加必要的库支持[^2]。 #### 3.2 创建配置类 接着定义一个 Java 类用于设置 WebSocket 的行为特性。此类应继承自 `WebSocketConfigurer` 接口并实现其方法以注册端点路径以及指定消息代理目的地前缀: ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry configurer) { configurer.enableSimpleBroker("/topic"); configurer.setApplicationDestinationPrefixes("/app"); } } ``` 此部分实现了 STOMP 协议的支持,并启用了简单的内存内消息代理功能。 #### 3.3 构建控制器处理请求 创建一个新的 REST 控制器用来接收来自客户端的消息并将它们广播给所有连接上的订阅者: ```java @Controller public class GreetingController { private final SimpMessagingTemplate messagingTemplate; @Autowired public GreetingController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } @MessageMapping("/hello") @SendTo("/topic/greetings") public String handle(String message) throws Exception { Thread.sleep(1000); // simulated delay return "Hello, " + HtmlUtils.htmlEscape(message); } } ``` 这里通过 `@MessageMapping` 注解指定了要监听的消息地址 `/hello` ,当收到新消息时会调用该方法进行响应处理[^1]。 #### 3.4 前端页面交互逻辑 最后,在 HTML 页面上编写 JavaScript 来建立 SockJS 连接并向服务器发送数据包: ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"/> <title>WebSocket Example</title> <script src="/webjars/jquery/3.4.1/jquery.min.js"></script> <script src="/webjars/sockjs-client/1.5.1/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/2.3.3-1/stomp.min.js"></script> <body> <div id="greeting">Greeting:</div> <button onclick="sendMessage()">Send Message</button> <script type="text/javascript"> var stompClient = null; function connect() { var socket = new SockJS('/ws'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/greetings', function (messageOutput) { document.getElementById("greeting").innerHTML += "<br>" + messageOutput.body; }); }); } function sendMessage() { stompClient.send("/app/hello", {}, JSON.stringify({'name': prompt("What's your name?")})); } connect(); </script> </body> </html> ``` 这段脚本负责初始化与后台的服务链接,并允许用户输入姓名后触发事件传递至服务端再由后者返回问候语显示于界面上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值