Spring Cloud Gateway 结合 WebSocket 进行实时推送

22,176 阅读2分钟

已经有好长一段时间没有写文章。主要还是个人比较随性,也在学习别的东西,就顾不上了。今天主要讲一下如何通过使用SpringCloud Gateway + WebSocker整合和自己在实践当中遇到的问题讲解一下,希望与大家一起来学习。

1.创建Spring gateway工程

想要了解gateway,可以取SpringCloud官方网站下载个列子。网关的主要作用我在这里就不再讲解了,就问度娘吧。

  • 我的网关 pom.xml maven 依赖

网关的配置:

server:
  port: ${USER_PORT:8555}
  http2:
    enabled: true
  compression:
    enabled: true
  error:
    whitelabel:
      enabled: false
spring:
  cloud:
    gateway:
      routes:
      # =====================================
      # to run server
      # $ wscat --listen 9000
      # to run client
      # $ wscat --connect ws://localhost:8080/echo
#      - id: websocket_test
#        uri: ws://localhost:9000
#        order: 9000
#        predicates:
#        - Path=/echo
      # =====================================
      - id: grabservice-websocket
        uri: lb:ws://bilibili-grabservice
        order: 9000
        predicates:
        - Path=/api/grabservice/mq/**
        filters:
        - StripPrefix=2

2.再创建一个数据采集实时推送的服务名字可以自己定,贴上我的pom.xml maven 的主要依赖

  • 编写个Configuration
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketAutoConfig  implements WebSocketMessageBrokerConfigurer{
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/mq")
                    .setAllowedOrigins("*")
                    .withSockJS();
        }
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.enableSimpleBroker("/matches");
            registry.setPreservePublishOrder(true);
        }
    }
  • 编写matches Broker的端口业务
    @Slf4j
    @RestController
    public class LiveMatchesController {
    
    
        @Autowired
        private SimpMessagingTemplate messagingTemplate;//这个是重点
        
    
        @Scheduled(cron = "0/15 * * * * ?")
        @SendTo("/matches")
        public void matches() {
            
                    messagingTemplate.convertAndSend("/matches", "hell world");//这个也是重点(点对点)
        }
    }

这样一个简单的后端推送服务完成。

3.编写一个测试页面(注意我的页面是放在后端推送服务中),然后通过网关,再Broker 后端推送服务的matches 端口

  • 在static下创建index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Spring Boot WebSocket+广播式</title>
</head>
<body onload="disconnect()">
<noscript>
    <h2 style="color:#ff0000">貌似你的浏览器不支持websocket</h2>
</noscript>
<div>
    <div>
        <button id="connect" onclick="connect()">连接</button>
        <button id="disconnect"  onclick="disconnect();">断开连接</button>
    </div>
    <div id="conversationDiv">
        <label>输入你的名字</label> <input type="text" id="name" />
        <br>
        <label>输入消息</label> <input type="text" id="messgae" />
        <button id="send" onclick="send();">发送</button>
        <p id="response"></p>
    </div>
</div>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.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;
    var host="http://192.168.0.249:8555/api/grabservice";
    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
        $('#response').html();
    }
    function connect() {
        var socket = new SockJS(host+'/mq');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected:' + frame);
            stompClient.subscribe('/matches', function(response) {
                showResponse(response.body);
            });
        });
    }
    function disconnect() {
        if (stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }
    function send() {
        var name = $('#name').val();
        var message = $('#messgae').val();
        stompClient.send("/chat", {}, JSON.stringify({username:name,message:message}));
    }
    function showResponse(message) {
        var response = $('#response');
        response.html(message);
    }
</script>
</body>
</html>

http://192.168.0.249:8555/api/grabservice,是我的网关请求地址,api/grabservice这是我的请求路由,真正请求的地址看网关的配置,我的后端推送服务端口是8553。 这里会存在两个问题

  • 1.跨域的问题。解决办法(本机调试如何浏览器中报:The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:8553, http://localhost:8553', but only one is allowed,可以注释 headers.setAccessControlAllowOrigin((request.getHeaders().get(HttpHeaders.ORIGIN)).get(0));):
@Bean
    public WebFilter corsFilter() {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request)) {
                ServerHttpResponse response = ctx.getResponse();
                HttpHeaders headers = response.getHeaders();
                log.debug(" origin : {}", request.getHeaders().get(HttpHeaders.ORIGIN).get(0));
                headers.setAccessControlAllowOrigin((request.getHeaders().get(HttpHeaders.ORIGIN)).get(0));
                headers.setAccessControlAllowCredentials(true);
                headers.setAccessControlMaxAge(Integer.MAX_VALUE);
                headers.setAccessControlAllowHeaders(Arrays.asList("*"));
                headers.setAccessControlAllowMethods(Arrays.asList(HttpMethod.OPTIONS,
                        HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST,
                        HttpMethod.DELETE, HttpMethod.PUT));
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(ctx);
        };
    }
  • 2.默认SockJS请求会自动添加类似info?t=150xxxxx 的请求,来获取服务端的信息是否是websocket,然后才会发送websocket真正的请求。如果不处理info请求,会报websocket请求头相关错误。解决办法在网关添加个全局过滤器,把我的http://ws://bilibili-grabservice/mq/info类似的请求中的ws(如果是https://wss),修改为http(wss就修改为https),但必须在WebsocketRoutingFilter之前org.springframework.cloud.gateway.filter.WebsocketRoutingFilter:
@Component
public class WebsocketHandler implements GlobalFilter, Ordered {
    private final static String DEFAULT_FILTER_PATH = "/mq/info";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String upgrade = exchange.getRequest().getHeaders().getUpgrade();
        log.debug("Upgrade : {}", upgrade);
        URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
        log.debug("path: {}", requestUrl.getPath());
        String scheme = requestUrl.getScheme();
        if (!"ws".equals(scheme) && !"wss".equals(scheme)) {
            return chain.filter(exchange);
        } else if (DEFAULT_FILTER_PATH.equals(requestUrl.getPath())) {
            String wsScheme = convertWsToHttp(scheme);
            URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build().toUri();
            exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, wsRequestUrl);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 2;
    }

    static String convertWsToHttp(String scheme) {
        scheme = scheme.toLowerCase();
        return "ws".equals(scheme) ? "http" : "wss".equals(scheme) ? "https" : scheme;
    }

好了一个基本的wobsocket工程就完成了。

参考文章

Spring Cloud Gateway转发Spring WebSocket