解決SpringCloud Gateway配置自定義路由404的坑
問題背景
將原有項(xiàng)目中的websocket模塊遷移到基于SpringCloud Alibaba的微服務(wù)系統(tǒng)中,其中網(wǎng)關(guān)部分使用的是gateway。
問題現(xiàn)象
遷移后,我們?cè)谑褂每蛻舳诉B接websocket時(shí)報(bào)錯(cuò):
io.netty.handler.codec.http.websocketx.WebSocketHandshakeException: Invalid subprotocol. Actual: null. Expected one of: protocol
...
同時(shí),我們還有一個(gè)用py寫的程序,用來模擬客戶端連接,但是程序的websocket連接就是正常的。
解決過程
1 檢查網(wǎng)關(guān)配置
先開始,我們以為是gateway的配置有問題。
但是在檢查gateway的route配置后,發(fā)現(xiàn)并沒有問題。很常見的那種。
... gateway: routes: #表示websocket的轉(zhuǎn)發(fā) - id: user-service-websocket uri: lb:ws://user-service predicates: - Path=/user-service/mq/** filters: - StripPrefix=1
其中,lb指負(fù)載均衡,ws指定websocket協(xié)議。
ps,如果,這里還有其他協(xié)議的相同路徑的請(qǐng)求,也可以直接寫成:
... gateway: routes: #表示websocket的轉(zhuǎn)發(fā) - id: user-service-websocket uri: lb://user-service predicates: - Path=/user-service/mq/** filters: - StripPrefix=1
這樣,其他協(xié)議的請(qǐng)求也可以通過這個(gè)規(guī)則進(jìn)行轉(zhuǎn)發(fā)了。
2 跟源碼,查找可能的原因
既然gate的配置沒有問題,那我們就嘗試從源碼的角度,看看gateway是如何處理ws協(xié)議請(qǐng)求的。
首先,我們要對(duì)“gateway是如何工作的”有個(gè)大概的認(rèn)識(shí):
可見,在收到請(qǐng)求后,要先經(jīng)過多個(gè)Filter才會(huì)到達(dá)Proxied Service。其中,要有自定義的Filter也有全局的Filter,全局的filter可以通過GET請(qǐng)求/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似乎與我們這里有關(guān)
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模式跟蹤到這里后,可以看到,客戶端請(qǐng)求中,子協(xié)議指定為“protocol”。
netty相關(guān):
WebSocketClientHandshaker
WebSocketClientHandshakerFactory
這段爛尾了。。。
直接看結(jié)論吧
最后發(fā)現(xiàn)出錯(cuò)的原因是在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 { ...... } }
這里,當(dāng)期望的子協(xié)議類型非空,而實(shí)際子協(xié)議不屬于期望的子協(xié)議時(shí),會(huì)拋出異常。也就是文章最初提到的那個(gè)。
3 異常原因分析
客戶端在請(qǐng)求時(shí),要求子協(xié)議為“protocol”,而我們的后臺(tái)websocket組件中,并沒有指定使用這個(gè)子協(xié)議,也就無法選出使用的哪個(gè)子協(xié)議。因此,在走到finishHandShaker時(shí),netty在檢查子協(xié)議是否匹配時(shí)拋出異常WebSocketHandshakeException。 py的模擬程序中,并沒有指定子協(xié)議,也就不會(huì)出錯(cuò)。
而在springboot的版本中,由于我們是客戶端直連(通過nginx轉(zhuǎn)發(fā))到websocket服務(wù)端的,因此也沒有出錯(cuò)(猜測(cè)是客戶端沒有檢查子協(xié)議是否合法)。。。
解決方法
在WebSocketServer類的注解@ServerEndpoint中,增加subprotocols={“protocol”}
@ServerEndpoint(value = "/ws/asset",subprotocols = {"protocol"})
隨后由客戶端發(fā)起websocket請(qǐng)求,請(qǐng)求連接成功,未拋出異常。
與客戶端的開發(fā)人員交流后,其指出,他們的代碼中,確實(shí)指定了子協(xié)議為“protocol”,當(dāng)時(shí)隨手寫的…
心得
- 定位問題較慢,中間走了不少彎路。有優(yōu)化的空間;
- 對(duì)gateway的模型有了更深刻的理解;
- idea 可以用雙擊Shift鍵來查找所有類,包括依賴包中的。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- springcloud?gateway無法路由問題的解決
- SpringCloud Gateway動(dòng)態(tài)路由配置詳解
- SpringCloud gateway+zookeeper實(shí)現(xiàn)網(wǎng)關(guān)路由的詳細(xì)搭建
- springcloud gateway如何實(shí)現(xiàn)路由和負(fù)載均衡
- SpringCloud Gateway 利用 Mysql 實(shí)現(xiàn)動(dòng)態(tài)路由的方法
- SpringCloud Gateway使用redis實(shí)現(xiàn)動(dòng)態(tài)路由的方法
- SpringCloud Gateway路由核心原理解析
相關(guān)文章
Java實(shí)現(xiàn)PDF轉(zhuǎn)HTML/Word/Excel/PPT/PNG的示例代碼
這篇文章主要為大家介紹了如何利用Java語言是PDF轉(zhuǎn)HTML、Word、Excel、PPT和PNG功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-05-05Java老手該當(dāng)心的13個(gè)錯(cuò)誤
這篇文章主要介紹了Java老手該當(dāng)心的13個(gè)錯(cuò)誤,需要的朋友可以參考下2015-04-04Java源碼解析阻塞隊(duì)列ArrayBlockingQueue常用方法
今天小編就為大家分享一篇關(guān)于Java源碼解析阻塞隊(duì)列ArrayBlockingQueue常用方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01Spring框架中Bean的三種配置和實(shí)例化方法總結(jié)
在Spring框架中,Bean的配置和實(shí)例化是很重要的基礎(chǔ)內(nèi)容,掌握各種配置方式,才能靈活管理Bean對(duì)象,本文將全面介紹Bean的別名配置、作用范圍配置,以及構(gòu)造器實(shí)例化、工廠實(shí)例化等方式2023-10-10Java使用設(shè)計(jì)模式中的工廠方法模式實(shí)例解析
當(dāng)系統(tǒng)準(zhǔn)備為用戶提供某個(gè)類的子類的實(shí)例,又不想讓用戶代碼和該子類形成耦合時(shí),就可以使用工廠方法模式來設(shè)計(jì)系統(tǒng).工廠方法模式的關(guān)鍵是在一個(gè)接口或抽象類中定義一個(gè)抽象方法,下面我們會(huì)具體介紹Java使用設(shè)計(jì)模式中的工廠方法模式實(shí)例解析.2016-05-05詳解Spring Cache使用Redisson分布式鎖解決緩存擊穿問題
本文主要介紹了詳解Spring Cache使用Redisson分布式鎖解決緩存擊穿問題,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04Java使用ffmpeg和mencoder實(shí)現(xiàn)視頻轉(zhuǎn)碼
這篇文章主要為大家詳細(xì)介紹了Java使用ffmpeg和mencoder實(shí)現(xiàn)視頻轉(zhuǎn)碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12SpringBoot實(shí)現(xiàn)文件上傳與下載功能的示例代碼
文件上傳與下載是Web應(yīng)用開發(fā)中常用的功能之一。接下來我們將討論如何在Spring?Boot的Web應(yīng)用開發(fā)中,如何實(shí)現(xiàn)文件的上傳與下載,感興趣的可以了解一下2022-06-06