结合SockJS实现websocket长连接

426 阅读2分钟

本文参与「新人创作礼」活动,一起开启掘金创作之路。

我的变强之路

1、引入依赖

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

2、配置websocket连接端点及其他配置

package com.dwk.config;

import com.dwk.properties.WebSocketProperties;
import com.dwk.socket.manager.WebSocketManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;

import java.util.List;

/**
 * websocket配置
 */
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {


    @Autowired
    private WebSocketProperties webSocketProperties;

    @Autowired
    private WebSocketManager webSocketManager;

    @Value("${spring.application.name}")
    private String applicationName;

    /**
     * 用户发送请求url="127.0.0.1:12003/socket"与STOMP server进行连接。之后再转发到订阅url;
     * PS:端点的作用——客户端在订阅或发布消息到目的地址前,要连接该端点。
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {

        List<String> endPointLiist = webSocketProperties.getEndPoint();
        log.error(applicationName + "服务  websocket连接端点:" + endPointLiist);
        if (endPointLiist.size() == 0 || endPointLiist == null){
            throw new RuntimeException("请先配置websocket连接端点!websocket.endPoint");
        }
        String[] endPoints = endPointLiist.stream().toArray(String[]::new);
        //设置websocket连接端点
        //stompEndpointRegistry.addEndpoint(endPoints).setAllowedOrigins("*").withSockJS();
        //spring boot 2.4以上版本使用此行配置
        stompEndpointRegistry.addEndpoint(endPoints).setAllowedOriginPatterns("*").withSockJS();

    }

    /**
     * 配置了一个简单的消息代理,如果不重载,默认情况下会自动配置一个简单的内存消息代理,用来处理以"/topic"为前缀的消息。这里重载configureMessageBroker()方法,
     * 消息代理将会处理前缀为"/topic"和"/queue"的消息。
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        List<String> websocketPrefixList = webSocketProperties.getWebsocketPrefix();
        log.error(applicationName + "服务  websocket前缀:" + websocketPrefixList);
        if (websocketPrefixList == null || websocketPrefixList.size() == 0){
            throw new RuntimeException("请先配置websocket连接前缀:websocket.websocketPrefix");
        }
        //1、前端请求需要在请求前加上setApplicationDestinationPrefixes内设置的值,即socket连接前缀
        String[] websocketPrefixs = websocketPrefixList.stream().toArray(String[]::new);
        registry.setApplicationDestinationPrefixes(websocketPrefixs);
        //2、消息代理前缀,如果消息的前缀为"/topic"、"/queue",就会将消息转发给消息代理(broker)再由消息代理广播给当前连接的客户端
        registry.enableSimpleBroker("/topic","/queue");
        //3、给指定用户发送一对一的主题前缀    默认为/user,可修改
        registry.setUserDestinationPrefix("/user");
    }

    /**
     * 添加websocket连接管理
     * @param registry
     */
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
        registry.addDecoratorFactory(webSocketManager);
        WebSocketMessageBrokerConfigurer.super.configureWebSocketTransport(registry);
    }
}

3、自定义配置文件 WebSocketProperties:

package com.dwk.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Data
@ConfigurationProperties(prefix = "websocket")
public class WebSocketProperties {

    /**websocket前缀地址*/
    private List<String> websocketPrefix;
    /**websocket连接端点*/
    private List<String> endPoint;

}

4、定义websocket管理类

package com.dwk.socket.manager;

import com.dwk.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory;

import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * websocket 连接管理
 */
@Slf4j
@Component
public class WebSocketManager implements WebSocketHandlerDecoratorFactory {

    @Autowired
    private WebSocketService webSocketService;

    /**存储连接*/
    private static final ConcurrentHashMap<String,WebSocketSession> sessionMap = new ConcurrentHashMap<>();

    /**连接统计*/
    private static final AtomicLong size = new AtomicLong();

    /**添加*/
    public static void addSession(String key, WebSocketSession webSocketSession){
        log.info("添加websocket连接,key = {},session = {}",key,webSocketSession);
        size.compareAndSet(1,1);
        sessionMap.put(key,webSocketSession);
    }

    /**移除*/
    public static void removeSession(String key){
        log.info("移除websocket连接,key = {}",key);
        sessionMap.remove(key);
    }

    /**获取*/
    public static WebSocketSession getSession(String key){
        if (sessionMap.contains(key)){
            return sessionMap.get(key);
        }
        throw new RuntimeException("该session不存在");
    }


    /**websocket连接握手,此处用于监听连接是否成功*/
    @Override
    public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {
        return new WebSocketHandlerDecorator(webSocketHandler) {
            /**连接建立成功后执行*/
            @Override
            public void afterConnectionEstablished(WebSocketSession session) throws Exception {
                log.info("websocket connection key = {},request uri = {},request from = {}",session.getId(),session.getUri(),session.getRemoteAddress());
                webSocketService.responseAll(session.getRemoteAddress() + "连接成功!","/topic/greetings");
                //此处可以根据session中缓存的身份验证校验session
                addSession(session.getId(),session);
                super.afterConnectionEstablished(session);
            }
            /**连接关闭后执行*/
            @Override
            public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
                log.info("websocket close key = {},uri = {},request from = {}",session.getId(),session.getUri(),session.getRemoteAddress());
                removeSession(session.getId());
                super.afterConnectionClosed(session, closeStatus);
            }
        };
    }

}

5、编写websocket控制类

package com.dwk.socket;

import cn.hutool.json.JSON;
import com.dwk.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.web.bind.annotation.RestController;

/**
 * websocket 控制器
 */
@Slf4j
@RestController
public class WebSocketEndPoint {

    @Autowired
    private WebSocketService webSocketService;

    /**
     * 表示服务端可以接收客户端通过主题“/topic/greetings”发送过来的消息,客户端需要在主题"/topic/greetings"上监听并接收服务端发回的消息
     * @param
     * @param
     */
    @MessageMapping("/sendAllConnect")
    @SendTo("/topic/greetings")
    public void greeting(@Payload String message) {
        log.info("客户端消息:" + message);
        webSocketService.responseAll(message,"/topic/greetings");
    }

    /**
     * 这里用的是@SendToUser,发送给单一客户端注解。
     * 服务端和客户端点对点通信方式即限定请求地址:/user /izn5ursn /queue/message
     *                                      前缀  session-ID 【 通信主题  】
     * @return
     */
    @MessageMapping("/message")
    @SendToUser("/queue/message")
    public JSON handleSubscribe(@Payload String message) {
        return webSocketService.pointToPoint(message,"/queue/message");
    }

}

6、实现具体业务逻辑

package com.dwk.service;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import com.dwk.info.WebSocketInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class WebSocketService implements WebSocketInfo {

    @Autowired
    private SimpMessageSendingOperations simpMessageSendingOperations;

    /**点对点消息*/
    @Override
    public JSON pointToPoint(String message, String topic) {
        log.info("客户端请求message接口发来消息"+message);
        Map<String,String> response = new HashMap<>();
        response.put("message", "服务端收到:" + message);
        JSON parse = JSONUtil.parse(response);
        String now = DateUtil.now();
        log.info(now + "响应主题:" + topic);
        return parse;
    }


    /**广播消息*/
    @Override
    @Scheduled(cron = "0/3 * * * * ?")
    public void responseAllByScheduled() {
//        log.info("广播消息.....");
//        Map<String,String> message = new HashMap<>();
//        message.put("message", "服务端定时发送的广播消息!");
//        JSON parse = JSONUtil.parse(message);
        //simpMessageSendingOperations.convertAndSend("/topic/greetings",parse);
    }

    @Override
    public void responseAll(String message,String topic) {
        log.info("连接成功.....");
        Map<String,String> msg = new HashMap<>();
        msg.put("message", message);
        JSON parse = JSONUtil.parse(msg);
        simpMessageSendingOperations.convertAndSend(topic,parse);
    }


}

7、前端请求

<!DOCTYPE html>
<html lang="en">
<meta charset = "utf-8">
<head>
    <title>Hello WebSocket</title>
    <script src="https://cdn.bootcss.com/sockjs-client/1.0.0/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script type="text/javascript">

        var stompClient = null;

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
            document.getElementById('response').innerHTML = '';
        }

        //网关地址
        var gatewayUrl = "http://172.18.2.98:12002";

        //请求路由
        var serviceName = "/exampleSocket";

        //websocket连接端点
        var websocketEndPoint = "/socket";

        //连接
        function connect() {
            var socket = new SockJS("http://192.168.0.101:12003/socket");
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                setConnected(true);
                console.log('Connected: ' + frame);

                //订阅后端广播接口
                stompClient.subscribe('/topic/greetings',function(greeting){
                    var message = "订阅服务端【广播】接口/topic/greetings,接收到消息:" + JSON.parse(greeting.body).message;
                    console.log(message);
                    showGreeting(message);
                });

                /**
                 * 订阅后端点对点接口
                 *
                 * /user/queue/message : /user为前缀,可设为静态     /queue/message
                 *
                 */
                stompClient.subscribe('/user/queue/message',function(greeting){
                    var message = "订阅服务端【点对点】接口/user/queue/message,接收到消息:"+JSON.parse(greeting.body).message;
                    console.log(message);
                    showGreeting(message);
                });

            });
        }

        //向后端接口发送消息
        function sendName() {
            var name = document.getElementById('name').value;
            var message = { 'name': name };
            console.log("向点对点主题发送消息:" + message)
            //example 是后端配置的目标前缀
            stompClient.send("/example/message", {}, JSON.stringify(message));
        }

        //向所有订阅了该主题的连接发送消息
        function sendAll() {
            var name = document.getElementById('name').value;
            var message = { 'name': name };
            console.log("向广播主题发送消息:" + message)
            //example 是后端配置的目标前缀
            stompClient.send("/example/sendAllConnect", {}, JSON.stringify(message));
        }

        //关闭连接
        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");
        }

        function showGreeting(message) {
            var response = document.getElementById('response');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            response.appendChild(p);
        }
    </script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect();">连接</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">关闭连接</button>
    </div>
    <div id="conversationDiv">
        <label>向服务端发送消息:</label><input type="text" id="name" />
        <button id="sendName" onclick="sendName();">发送</button>
        <button id="sendAll" onclick="sendAll();">向所有人发送</button>
        <p id="response"></p>
    </div>
</div>
</body>
</html>

8、运行结果:

image.png

9、扩展:结合网关实现websocket通信