我使用的gateway版本是2020.0.1,在通信時使用到了websocket。本來用得好好的,結果在某天出現了異常:Max frame length of 65536 has been exceeded。
看報錯信息就知道是因為websocket的幀超過了默認的65536限制,這個限制可以在源碼中的這個類 reactor.netty.http.websocket.WebsocketSpec 中看得到。
本來我也沒想着自己去解決,因為這種問題一般來說大家都可能會遇到,網上應該有很多的解決辦法。然而事實上,我在網上搜了半天,搜索到的結果幾乎都是同一篇文章,而且過程相當麻煩,又是繼承重寫,又是引包改配置,直接把我勸退了。
於是,我開始了自己的探索。
在我的debug大法下,從升級握手開始追蹤。找到了我們的適配器,這個一般是寫在配置中
@Bean public WebSocketHandlerAdapter handlerAdapter() { return new WebSocketHandlerAdapter(); }
里面有個handle方法,用來提交數據
public WebSocketHandlerAdapter() { this(new HandshakeWebSocketService()); } public WebSocketHandlerAdapter(WebSocketService webSocketService) { Assert.notNull(webSocketService, "'webSocketService' is required"); this.webSocketService = webSocketService; } @Override public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) { WebSocketHandler webSocketHandler = (WebSocketHandler) handler; return getWebSocketService().handleRequest(exchange, webSocketHandler).then(Mono.empty()); }
注意到,還有兩個構造方法,其中一個可以傳入一個WebSocketService實例,這里舉例的實例是HandshakeWebSocketService實例。
我們按照代碼追蹤,進入到handleRequest方法內部
@Override public Mono<Void> handleRequest(ServerWebExchange exchange, WebSocketHandler handler) { ServerHttpRequest request = exchange.getRequest(); HttpMethod method = request.getMethod(); HttpHeaders headers = request.getHeaders(); if (HttpMethod.GET != method) { return Mono.error(new MethodNotAllowedException( request.getMethodValue(), Collections.singleton(HttpMethod.GET))); } if (!"WebSocket".equalsIgnoreCase(headers.getUpgrade())) { return handleBadRequest(exchange, "Invalid 'Upgrade' header: " + headers); } List<String> connectionValue = headers.getConnection(); if (!connectionValue.contains("Upgrade") && !connectionValue.contains("upgrade")) { return handleBadRequest(exchange, "Invalid 'Connection' header: " + headers); } String key = headers.getFirst(SEC_WEBSOCKET_KEY); if (key == null) { return handleBadRequest(exchange, "Missing \"Sec-WebSocket-Key\" header"); } String protocol = selectProtocol(headers, handler); return initAttributes(exchange).flatMap(attributes -> this.upgradeStrategy.upgrade(exchange, handler, protocol, () -> createHandshakeInfo(exchange, request, protocol, attributes)) ); }
注意到,最后調用了upgradeStrategy的upgrade的方法,而這個upgradeStrategy是RequestUpgradeStrategy的一個實現類,也是是可以在創建WebSocketService實例的時候傳入的。
public HandshakeWebSocketService(RequestUpgradeStrategy upgradeStrategy) { Assert.notNull(upgradeStrategy, "RequestUpgradeStrategy is required"); this.upgradeStrategy = upgradeStrategy; }
那么,我們就進入這個upgrade方法看看,我這里舉例的實例是RequestUpgradeStrategy的一個實現類ReactorNettyRequestUpgradeStrategy。
@Override public Mono<Void> upgrade(ServerWebExchange exchange, WebSocketHandler handler, @Nullable String subProtocol, Supplier<HandshakeInfo> handshakeInfoFactory) { ServerHttpResponse response = exchange.getResponse(); HttpServerResponse reactorResponse = ServerHttpResponseDecorator.getNativeResponse(response); HandshakeInfo handshakeInfo = handshakeInfoFactory.get(); NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory(); URI uri = exchange.getRequest().getURI(); // Trigger WebFlux preCommit actions and upgrade return response.setComplete() .then(Mono.defer(() -> { WebsocketServerSpec spec = buildSpec(subProtocol); return reactorResponse.sendWebsocket((in, out) -> { ReactorNettyWebSocketSession session = new ReactorNettyWebSocketSession( in, out, handshakeInfo, bufferFactory, spec.maxFramePayloadLength()); return handler.handle(session).checkpoint(uri + " [ReactorNettyRequestUpgradeStrategy]"); }, spec); })); }
到了這里,並沒有發現什么異樣。但是注意到這樣一句代碼
WebsocketServerSpec spec = buildSpec(subProtocol);
而這個叫buildSpec的實現就就在類里面
WebsocketServerSpec buildSpec(@Nullable String subProtocol) { WebsocketServerSpec.Builder builder = this.specBuilderSupplier.get(); if (subProtocol != null) { builder.protocols(subProtocol); } if (this.maxFramePayloadLength != null) { builder.maxFramePayloadLength(this.maxFramePayloadLength); } if (this.handlePing != null) { builder.handlePing(this.handlePing); } return builder.build(); }
可以看到,WebsocketServerSpec是由一個specBuilderSupplier構建出來的,那么specBuilderSupplier它是怎么來的呢?答案就在代碼上面的構造方法中。是的,這個specBuilderSupplier他除了默認的構建之外,也能從外部傳進來
public ReactorNettyRequestUpgradeStrategy() { this(WebsocketServerSpec::builder); } public ReactorNettyRequestUpgradeStrategy(Supplier<WebsocketServerSpec.Builder> builderSupplier) { Assert.notNull(builderSupplier, "WebsocketServerSpec.Builder is required"); this.specBuilderSupplier = builderSupplier; }
點進這個WebsocketServerSpec看一下
public interface WebsocketServerSpec extends WebsocketSpec { static WebsocketServerSpec.Builder builder() { return new WebsocketServerSpec.Builder(); } public static final class Builder extends reactor.netty.http.websocket.WebsocketSpec.Builder<WebsocketServerSpec.Builder> { private Builder() { } public final WebsocketServerSpec build() { return new WebsocketServerSpecImpl(this); } } }
進入其父類WebsocketSpec看一下,是不是發現了我們想要的東西
public interface WebsocketSpec { ...... public static class Builder<SPEC extends WebsocketSpec.Builder<SPEC>> implements Supplier<SPEC> { int maxFramePayloadLength = 65536; ...... } ...... }
那么,這一長串的路徑就理通暢了,如果沒看明白的話,請自己去追蹤一下代碼。
- WebSocketHandlerAdapter內部有一個WebSocketService實例,調用handleRequest方法提交數據
- WebSocketService實例內部有一個RequestUpgradeStrategy實例,調用RequestUpgradeStrategy的upgrade方法
- RequestUpgradeStrategy內部有一個WebsocketServerSpec.Builder的實例,用來構建參數
- 並且,也是最重要的,每一環,都可以將需要的實例作為構造參數直接傳進去,而不使用默認的。
一通分析之后,最終的解決辦法也很簡單,在你的配置類中修改配置成如下狀態
@Bean public WebSocketHandlerAdapter handlerAdapter() {
// 這里可以根據自己的實際情況決定使用哪種實現類和實現方式,數字可以改成可配置的 注意,這個websocketProperties是我自己的寫的配置屬性類,不要照搬過去啊 int frameSizeLimit = webSocketProperties().getFrameSizeLimit() <= 0 ? 65536 : webSocketProperties().getFrameSizeLimit(); WebsocketServerSpec.Builder builder = WebsocketServerSpec.builder().maxFramePayloadLength(frameSizeLimit); RequestUpgradeStrategy upgradeStrategy = new ReactorNettyRequestUpgradeStrategy(builder); return new WebSocketHandlerAdapter(new HandshakeWebSocketService(upgradeStrategy)); }
有沒有更加更加簡單的辦法呢?當然有了!springboot嘛,springcloud嘛,配置文件走起啊!
spring:
cloud:
gateway:
httpclient:
websocket:
max-frame-payload-length: xxxxxx # 看你的需要咯
上述兩種方式,看你的喜好咯!
到這里,你的項目應該不會再報出Max frame length of 65536 has been exceeded這種問題了,希望我的解決辦法能夠幫到大家!