解決SpringCloud Gateway配置自定義路由404的坑

問題背景

將原有項目中的websocket模塊遷移到基於SpringCloud Alibaba的微服務系統中,其中網關部分使用的是gateway。

問題現象

遷移後,我們在使用客戶端連接websocket時報錯:

io.netty.handler.codec.http.websocketx.WebSocketHandshakeException: Invalid subprotocol. Actual: null. Expected one of: protocol

同時,我們還有一個用py寫的程序,用來模擬客戶端連接,但是程序的websocket連接就是正常的。

解決過程

1 檢查網關配置

先開始,我們以為是gateway的配置有問題。

但是在檢查gateway的route配置後,發現並沒有問題。很常見的那種。

...
	gateway:
      routes:
        #表示websocket的轉發
        - id: user-service-websocket
          uri: lb:ws://user-service
          predicates:
          	- Path=/user-service/mq/**
          filters:
            - StripPrefix=1

其中,lb指負載均衡,ws指定websocket協議。

ps,如果,這裡還有其他協議的相同路徑的請求,也可以直接寫成:

...
gateway:
      routes:
        #表示websocket的轉發
        - id: user-service-websocket
          uri: lb://user-service
          predicates:
          	- Path=/user-service/mq/**
          filters:
            - StripPrefix=1

這樣,其他協議的請求也可以通過這個規則進行轉發瞭。

2 跟源碼,查找可能的原因

既然gate的配置沒有問題,那我們就嘗試從源碼的角度,看看gateway是如何處理ws協議請求的。

首先,我們要對“gateway是如何工作的”有個大概的認識:

在這裡插入圖片描述

可見,在收到請求後,要先經過多個Filter才會到達Proxied Service。其中,要有自定義的Filter也有全局的Filter,全局的filter可以通過GET請求/actuator/gateway/globalfilters來查看

{
  "org.springframework.cloud.gateway.filter.LoadBalancerClientFilter@77856cc5": 10100,
  "org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4f6fd101": 10000,
  "org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@32d22650": -1,
  "org.springframework.cloud.gateway.filter.ForwardRoutingFilter@106459d9": 2147483647,
  "org.springframework.cloud.gateway.filter.NettyRoutingFilter@1fbd5e0": 2147483647,
  "org.springframework.cloud.gateway.filter.ForwardPathFilter@33a71d23": 0,
  "org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@135064ea": 2147483637,
  "org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@23c05889": 2147483646
}

可以看到,其中的WebSocketRoutingFilter似乎與我們這裡有關

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        this.changeSchemeIfIsWebSocketUpgrade(exchange);
        URI requestUrl = (URI)exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
        String scheme = requestUrl.getScheme();
        if (!ServerWebExchangeUtils.isAlreadyRouted(exchange) && ("ws".equals(scheme) || "wss".equals(scheme))) {
            ServerWebExchangeUtils.setAlreadyRouted(exchange);
            HttpHeaders headers = exchange.getRequest().getHeaders();
            HttpHeaders filtered = HttpHeadersFilter.filterRequest(this.getHeadersFilters(), exchange);
            List<String> protocols = headers.get("Sec-WebSocket-Protocol");
            if (protocols != null) {
                protocols = (List)headers.get("Sec-WebSocket-Protocol").stream().flatMap((header) -> {
                    return Arrays.stream(StringUtils.commaDelimitedListToStringArray(header));
                }).map(String::trim).collect(Collectors.toList());
            }
            return this.webSocketService.handleRequest(exchange, new WebsocketRoutingFilter.ProxyWebSocketHandler(requestUrl, this.webSocketClient, filtered, protocols));
        } else {
            return chain.filter(exchange);
        }
    }

以debug模式跟蹤到這裡後,可以看到,客戶端請求中,子協議指定為“protocol”。

netty相關:

  • WebSocketClientHandshaker
  • WebSocketClientHandshakerFactory

這段爛尾瞭。。。

直接看結論吧

最後發現出錯的原因是在netty的WebSocketClientHandshanker.finishHandshake

public final void finishHandshake(Channel channel, FullHttpResponse response) {
        this.verify(response);
        String receivedProtocol = response.headers().get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL);
        receivedProtocol = receivedProtocol != null ? receivedProtocol.trim() : null;
        String expectedProtocol = this.expectedSubprotocol != null ? this.expectedSubprotocol : "";
        boolean protocolValid = false;
        if (expectedProtocol.isEmpty() && receivedProtocol == null) {
            protocolValid = true;
            this.setActualSubprotocol(this.expectedSubprotocol);
        } else if (!expectedProtocol.isEmpty() && receivedProtocol != null && !receivedProtocol.isEmpty()) {
            String[] var6 = expectedProtocol.split(",");
            int var7 = var6.length;
            for(int var8 = 0; var8 < var7; ++var8) {
                String protocol = var6[var8];
                if (protocol.trim().equals(receivedProtocol)) {
                    protocolValid = true;
                    this.setActualSubprotocol(receivedProtocol);
                    break;
                }
            }
        }
        if (!protocolValid) {
            throw new WebSocketHandshakeException(String.format("Invalid subprotocol. Actual: %s. Expected one of: %s", receivedProtocol, this.expectedSubprotocol));
        } else {
           ......
        }
    }

這裡,當期望的子協議類型非空,而實際子協議不屬於期望的子協議時,會拋出異常。也就是文章最初提到的那個。

3 異常原因分析

客戶端在請求時,要求子協議為“protocol”,而我們的後臺websocket組件中,並沒有指定使用這個子協議,也就無法選出使用的哪個子協議。因此,在走到finishHandShaker時,netty在檢查子協議是否匹配時拋出異常WebSocketHandshakeException。 py的模擬程序中,並沒有指定子協議,也就不會出錯。

而在springboot的版本中,由於我們是客戶端直連(通過nginx轉發)到websocket服務端的,因此也沒有出錯(猜測是客戶端沒有檢查子協議是否合法)。。。

解決方法

在WebSocketServer類的註解@ServerEndpoint中,增加subprotocols={“protocol”}

@ServerEndpoint(value = "/ws/asset",subprotocols = {"protocol"})

隨後由客戶端發起websocket請求,請求連接成功,未拋出異常。

與客戶端的開發人員交流後,其指出,他們的代碼中,確實指定瞭子協議為“protocol”,當時隨手寫的…

心得

  • 定位問題較慢,中間走瞭不少彎路。有優化的空間;
  • 對gateway的模型有瞭更深刻的理解;
  • idea 可以用雙擊Shift鍵來查找所有類,包括依賴包中的。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: