springboot+webSocket的多服务器下的点对点推送实现

 

 

webSocket的概念在这里就不多阐述了,网上有很多大家可以自行搜索。本篇博客主要是实现webSocket在多服务器下的点对点推送实现

直接进入代码实现

import com.cjh.websocket.socket.vo.User;
import com.cjh.websocket.util.WebsocketMapUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.web.socket.WebSocketHandler;
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.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;

import java.security.Principal;
import java.util.List;
import java.util.Map;

@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {

        config.enableSimpleBroker("/topic", "/user", "/message");
        config.setApplicationDestinationPrefixes("/app");

    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket")
                .addInterceptors(myHandshakeInterceptor())  //添加 websocket握手拦截器
                .setHandshakeHandler(myDefaultHandshakeHandler())   //添加 websocket握手处理器
                .setAllowedOrigins("*")
                .withSockJS();
    }

    /**
     * WebSocket 握手拦截器
     * 可做一些用户认证拦截处理
     */
    private HandshakeInterceptor myHandshakeInterceptor() {
        return new HandshakeInterceptor() {
            /**
             * websocket握手连接
             * @return 返回是否同意握手
             */
            @Override
            public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
                return true;
            }

            @Override
            public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

            }
        };
    }

    //WebSocket 握手处理器
    private DefaultHandshakeHandler myDefaultHandshakeHandler() {
        return new DefaultHandshakeHandler() {
            @Override
            protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
                //设置认证通过的用户到当前会话中
                return (Principal) attributes.get("user");
            }
        };
    }

    /**
     * 输入通道参数设置
     */
    @Override
    public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
        log.info("configureClientInboundChannel-start");
        channelRegistration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                log.info("configureClientInboundChannel-accessor.getCommand():" + accessor.getCommand());
                //判断前端socket发送过来操作
                switch (accessor.getCommand()) {
                    case CONNECT: {
                        String authorization = accessor.getNativeHeader("Authorization").get(0);
                        User user = (User) JSONObject.toBean(JSONObject.fromObject(authorization), User.class);
                        log.info("configureClientInboundChannel-CONNECT:" + "user:" + user);
                        if (user == null) {
                            return null;
                        }
                        WebsocketMapUtil.putServerChannel(user);
                        //注册当前用户
                        accessor.setUser(new MyPrincipal(user));
                        return message;
                    }
                    case ABORT:
                    case DISCONNECT: {
                        MyPrincipal myPrincipal = (MyPrincipal) accessor.getHeader("simpUser");
                        log.info("configureClientInboundChannel-DISCONNECT:myPrincipal:" + myPrincipal.toString());
                        if (myPrincipal == null) {
                            return null;
                        }
                        WebsocketMapUtil.removeServerChannel(myPrincipal.user);
                        return message;
                    }
                    case SUBSCRIBE: {
                        return message;
                    }
                    default:
                        return null;
                }
            }
        });
    }
    //重新实现Principal接口,主要重写getName方法。点对点推送就是采用这个getName方法获取用户名
    class MyPrincipal implements Principal {

        private User user;

        public MyPrincipal(User user) {
            this.user = user;
        }

        @Override
        public String getName() {
            return user.getId();
        }
    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> list) {
        return false;
    }
}

有几个方法需要进行解释:

1.@EnableWebSocketMessageBroker注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思。 
2.registerStompEndpoints方法表示注册STOMP协议的节点,并指定映射的URL。 
3.stompEndpointRegistry.addEndpoint("/webSocket").withSockJS();这一行代码用来注册STOMP协议节点,同时指定使用SockJS协议。 中间增加了两个方法用于webSocket握手拦截器和处理器的认证处理。
4.configureMessageBroker方法用来配置消息代理,由于我们是实现推送功能,这里的消息代理是/app。config.enableSimpleBroker("/topic", "/user/*");表示以/topic和/user/*开头的url地址进行交互通信。

@Slf4j
@Component
public class SubThread extends Thread {

    @Resource
    protected JedisPool jedisPool;

    @Autowired
    Subscriber subscriber;

    final public static String serverChannel = "server_channel";

    @Override
    public void run() {
        log.info(String.format("subscribe redis, channel %s, thread will be blocked", serverChannel));
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.subscribe(subscriber, serverChannel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}
@Component
public class CommandLine implements CommandLineRunner{

    @Autowired
    SubThread subThread;

    @Override
    public void run(String... args) {
        subThread.start();
    }
}

启动程序时就启动一条线程用于监听redis消息。

@Component
@AllArgsConstructor
public class Subscriber extends JedisPubSub {
    private final SimpMessageSendingOperations simpMessageSendingOperations;

    private static final String DESTINATION = "/message";

    @Override
    //监听到消息进行处理
    public void onMessage(String channel, String messageJson) {
        BaseSubPubBean baseSubPubBean = null;
        try {
            baseSubPubBean = (BaseSubPubBean) SerializationUtil.deserializeToObject(messageJson);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (baseSubPubBean == null) {
            return;
        }
        switch (baseSubPubBean.getType()) {
            case LOGIN:
                String message = (String) baseSubPubBean.getData();
                if (WebsocketMapUtil.inServerChannel(baseSubPubBean.getId())) {
                    simpMessageSendingOperations.convertAndSendToUser(
                            baseSubPubBean.getId(),
                            DESTINATION,
                            message);
                }
                break;
            default:
                break;
        }
        System.out.println("信息发送成功");
    }

    @Override
    public void onSubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
                channel, subscribedChannels));
    }

    @Override
    public void onUnsubscribe(String channel, int subscribedChannels) {
        System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
                channel, subscribedChannels));

    }
}

获取到监听的消息进行判断,webSocket建立连接的是否本服务器。如果是则进行单点数据返回,不是则跳过。

点对点数据发送,convertAndSendToUser方法就是用于发送给指定用户数据。

<!DOCTYPE html>
<html>
<head>
    <title>websocket</title>
    <script src="//cdn.bootcss.com/angular.js/1.5.6/angular.min.js"></script>
    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script type="text/javascript">
        /*<![CDATA[*/

        var stompClient = null;

        var app = angular.module('app', []);
        // 定义客户端的认证信息,按需求配置

        app.controller('MainController', function($rootScope, $scope, $http) {

            $scope.data = {
                //用户id
                id : '',
                //用户名
                name : '',
                //连接状态
                connected : false,
                //消息
                message : '',
                rows : []
            };



            //连接
            $scope.connect = function() {
                var socket = new SockJS('/websocket');
                var headers = {
                    Authorization:'{"id":"' + $scope.data.id + '"}'
                }
                stompClient = Stomp.over(socket);
                stompClient.connect(headers, function(frame) {
                    // 注册推送时间回调
                    stompClient.subscribe('/user/message', function(r) {
                        $scope.data.time = '当前服务器时间:' + r.body;
                        $scope.data.connected = true;
                        $scope.$apply();
                    });

                    $scope.data.connected = true;
                    $scope.$apply();
                });
            };

            $scope.disconnect = function() {
                if (stompClient != null) {
                    stompClient.disconnect();
                }
                $scope.data.connected = false;
            }


        });
    </script>
</head>
<body ng-app="app" ng-controller="MainController">

<h2>websocket</h2>
id:<input type="text" ng-model="data.id" placeholder="只能为纯数字..." />
<br/>
<label>WebSocket连接状态:</label>
<button type="button" ng-disabled="data.connected" ng-click="connect()">连接</button>
<button type="button" ng-click="disconnect()" ng-disabled="!data.connected">断开</button>
<br />
<br />
<div ng-show="data.connected">
    <label>{{data.time}}</label> <br /> <br />
</div>
</body>
</html>

简单的前端实现,这里需要注意的是convertAndSendToUser发送的地址是 /message,而前端需要进行通讯的地址是 /user/message。指定用户发送需要在地址前面增加 /user 路径。

代码在git上可以下载运行访问 http://localhost:8099/地址输入自己指定的 id 连接后,调用http://localhost:8099/send?id=?进行发送redis消息测试。其中redis消息服务可以替换成现在流程的mq消息服务,这里主要给大家提供一个多服务器下webSocket单点发送数据的思路,再在此基础上进行改造。 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值