websocket二实战

SockJS

SockJS是一个浏览器JavaScript库。

一些浏览器中缺少对WebSocket的支持,因此,回退选项是必要的,而Spring框架提供了基于SockJS协议的透明的回退选项。参考spring整合websocket官方文档

SockJS的一大好处在于提供了浏览器兼容性。优先使用原生WebSocket,如果在不支持websocket的浏览器中,会自动降为轮询的方式。

// 前端代码
<script src="/js/sockjs.min.js"></script>
websocket = new SockJS("端点http地址")
websocket.onOpen(); // 等方法均可使用。不一定准确。大概是这么个意思


// 后端基于spring的sockJs实现
// withSockJS() 方法 划重点
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").withSockJS();
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

stomp

stomp协议定义

stomp协议中文版

spring整合stomp

stomp是基于帧的协议,它为这些客户机和服务器之间传递的消息定义了一种基于文本的消息格式。stomp由命令 + header + payload组成。

stomp消息协议

stomp协议组成分析

ws中传递的stomp帧

客户端COMMAND

SEND

SUBSCRIBE

UNSUBSCRIBE

BEGIN

COMMIT

ABORT

ACK

NACK

DISCONNECT

服务端COMMAND

MESSAGE

RECEIPT

ERROR

HEADER

在发送特定的消息时,会携带特定的header。同时前端也可以携带header。然后后端接收。

前端传递:

stompClient.send("/app/sendToServer", {"accountId": 1}, msg);

其中accoontId:1 即为携带的header键值对

后端接收:

方法中添加MessageHeaders 参数即可

BODY

Body^@ -- 消息体

spring with stomp demo

// 前端
stompClient.connect(function (frame) {
//		writeToScreen("connected: " + frame);

        /** 订阅广播 */
//		stompClient.subscribe('/topic/reply', function (response) {
//			writeToScreen("/topic/:" + response.body);
//		});
//
//        /** 订阅点对点 */
//		stompClient.subscribe("/topic/"+accountId, function (response) {
//			writeToScreen("/topic/"+accountId+":" + response.body);
//		});

		}, function (error) {
			wsCreateHandler && clearTimeout(wsCreateHandler);
			wsCreateHandler = setTimeout(function () {
//				console.log("重连...");
//				connect();
//				console.log("重连完成");
			}, 1000);
		}

// 后端配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 此时基于http的url.
        registry.addEndpoint("/stomp").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // STOMP messages whose destination header begins with /app are routed to @MessageMapping methods in @Controller classes.
        // 相当于在所有的方法访问前添加 /app 才可以访问
        config.setApplicationDestinationPrefixes("/app");
      	// 相当于直接发给相关的broker通道,订阅的客户端都可以收到
      	// The simple broker is great for getting started but supports only 
				//		a subset of STOMP commands (it does not support acks, receipts,
				//	and some other features), relies on a simple message-sending loop,
				//	and is not suitable for clustering. As an alternative, you can upgrade
				//	your applications to use a full-featured message broker.
        config.enableSimpleBroker("/topic", "/queue");
    }
}

// 后端回复和发送消息
/**
 * MessageMapping 注解会结合setApplicationDestinationPrefixes方法形成完整路径
 * SendTo 如果不指定,则返回给MessageMapping的/topic/sendToServer和/queue/sendToServer。指定,则返回给指定的topic
 * 也可以使用SimpMessagingTemplate 指定目的地进行发送
 */
@RestController
public class StompController {

    @Autowired
    SimpMessagingTemplate template;

  	
    @MessageMapping("/sendToServer")
    @SendTo("/topic/reply")
		//MessageHeaders 会携带header信息,其入参中可构造的参数参考spring文档
    public String sendToServer(String msg, MessageHeaders headers){
        System.out.println("msg = " + msg);
        // template.convertAndSend("/templateSend", msg);
				System.out.println("headers = " + JSON.toJSONString(headers));
        return "服务端已经收到了" + msg;
    }

  	// 可以通过MessageExceptionHandler 进行全局的异常处理
		@MessageExceptionHandler
    public ApplicationError handleException(MyException exception) {
        // ...
        return appError;
    }
}

spring stomp 鉴权

  	@Override
    public void configureClientInboundChannel(ChannelRegistration registration){
        registration.interceptors(MyAuthInterceptor);
    }
package com.example.websocketdemo.spring.stomp;

import com.sun.security.auth.UserPrincipal;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;

@Component
public class MyAuthInterceptor implements ChannelInterceptor {

    private static final String TOKEN = "token";

    /**
     * 实际消息发送到频道之前调用
     * @param message
     * @param channel
     * @return
     */
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        StompCommand command = accessor.getCommand();

        if (StompCommand.CONNECT.equals(command)) {
            Object token = accessor.getNativeHeader(TOKEN);
            if(token == null){
                throw new RuntimeException();
            }
            // 也可以自定义Principal类,用于信息存储和校验。
            // 还有一种常用的方式是接入spring security
            UserPrincipal userPrincipal = new UserPrincipal("my name from token");
            accessor.setUser(userPrincipal);
        }else if (StompCommand.SUBSCRIBE.equals(command)) {
            // 此处可以通过一些存储的信息进行权限校验
            UserPrincipal userPrincipal = (UserPrincipal) accessor.getUser();
            userPrincipal.getName();
        }else if(StompCommand.SEND.equals(command)){
            // 此处可以通过一些存储的信息进行权限校验
            UserPrincipal userPrincipal = (UserPrincipal) accessor.getUser();
            userPrincipal.getName();
        }
        return message;
    }
}

注:其与http鉴权的不同之处在于,ws基于长链接的,会一直保存。因此token信息只需要在connect时一次存储即可,其他时候如果要进行权限校验,则直接通过Accessor中保存的信息即可。

另外websocket本质上建立的链接是本地和服务器直接建立。因此不支持网关统一鉴权。暂时未考证。

spring stomp client

参考spring官网文档4.4.18 stomp client章节

多节点问题处理

使用rabbitmq broker

安装rabbitmq

mac电脑 brew install rabbitmq

开启stomp服务

rabbitmq-plugins enable rabbitmq_web_stomp rabbitmq_web_stomp_examples

启动rabbitmq

rabbitmq-server

登录控制台查看rabbitmq开发的stomp地址

控制台地址: http://localhost:15672/#/

账密:guest guest

默认端口:61613

符合rabbitmq的前缀

/exchange/<exchangeName>/<pattern>

subcribe frame

手动创建exchange,自动创建一个和exchangeName一致的queue,并且指定了routingkey就是pattern

send frame

发送到定义的exchange中,并且指定了其routingkey

/queue/<queueName>

使用默认的exchange,自动创建持久化的queue

无法做到群发,因为使用的是同一个queue,只会有一个队列进行消费。但是其队列是持久化的,包括消息,所以如果在消费的过程中,客户端没有消费到。会在下次连接时,从没有消费的地方进行ws的传输

/amq/queue/<queueName>

需要手动创建queue,更严谨一些。但是一般都使用自动创建。

/topic/<topicName>

subcribe frame 会自动创建AD 非持久的queue,并且根据routingkey为topicName 绑定到queue上,同时实现对该queue的订阅。使用默认的amq.topic exchange

send frame 消息会被发送到amp.topic exchange中,routingkey 是topicName。

智慧物流项目选择使用的方式。此方式使用起来比较便捷

demo

  	@Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
                registry.enableStompBrokerRelay("/topic/test").setRelayHost(brokerRelayHost)
                    .setRelayPort(brokerRelayPort).setClientLogin("guest").setClientPasscode("guest");
    }

原理简述

猜想:每次在客户端进行订阅的时候会根据标识产生当前客户端对应的临时队列。当客户端进行send的时候会产生消息,然后让队列消费,写入到对应的通道中。

nginx接入

构建稳定的连接

断连情况

浏览器页面隐藏导致的断连

tcp断连

stomp客户端自实现ping、pong,不会发生

服务器重启断连

单节点

多节点滚动更新

解决方式

前端尝试重连,但是不能一直重连。建议重连三次。

重连可以解决 浏览器页面隐藏以及多节点滚动更新的问题。单节点问题无法解决。所以重连三次之后不能一直尝试,避免把网关打满,影响其他服务。

 

参考文档

stomp协议定义

stomp协议中文版

spring整合stomp

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值