解决SpringCloud集成websocket报错的问题Invalid handshake response getStatus: 401

本文解决WebSocket握手异常问题,当出现401错误时,通常是因为安全框架拦截。通过在Nacos中配置忽略特定路径,可以避免此问题。适用于使用Security的安全框架。

如果报错io.netty.handler.codec.http.websocketx.WebSocketHandshakeException: Invalid handshake response getStatus: 401。说明是安全框架进行了拦截。我用的是security。只需要在Nacos中忽略掉websocket路径就可以了。

oauth2-ignore:
    urls:
      - /actuator/**
      - /websocket/**
<think>我们遇到了一个Netty相关的错误:`UnsupportedMessageTypeException`,提示期望的是`ByteBuf`类型,但实际收到了`CloseWebSocketFrame`。 这个错误通常发生在使用Netty作为WebSocket客户端时,尝试写入一个关闭帧(CloseWebSocketFrame)到SSL/TLS处理器(SslHandler)时,因为SslHandler只支持ByteBuf类型。 问题分析: 在Netty的管道(pipeline)中,当使用SSL时,SslHandler期望处理的数据类型是ByteBuf。然而,WebSocket帧(如CloseWebSocketFrame)是对象类型,因此需要经过编码器转换为ByteBuf。 解决方案: 1. 确保在Netty的管道中,在SslHandler之后添加了WebSocket帧的编码器(WebSocketFrameEncoder)。 2. 检查管道的顺序:应该先进行SSL处理(将对象转为加密的ByteBuf),然后WebSocket编码器将WebSocket帧转为ByteBuf,但这里注意,WebSocket编码器应该在SslHandler之前?实际上,对于出站数据,处理顺序是反向的。所以我们需要确保在出站方向上,WebSocket帧编码器在SslHandler之前,这样WebSocket帧先被编码成ByteBuf,然后SslHandler才能处理。 正确的管道配置顺序(出站方向): WebSocketFrameEncoder (将WebSocketFrame编码为ByteBuf) SslHandler (将ByteBuf加密,输出为加密的ByteBuf) 因此,在初始化管道时,应该先添加SslHandler,然后添加WebSocketFrameEncoder?不对,注意入站和出站处理器的顺序是独立的,但通常我们按处理顺序添加。对于出站数据,是从后往前(tail到head)处理的,所以我们应该先添加的处理器后被调用(即后添加的处理器先处理出站数据)。 所以,在Netty管道中,添加处理器顺序(从head到tail): 1. SslHandler(处理入站解密和出站加密) 2. 其他入站处理器(如HttpClientCodec, WebSocketFrameDecoder等) 3. 出站处理器:WebSocketFrameEncoder(将WebSocketFrame转为ByteBuf)应该放在SslHandler之后?不对,因为出站数据是从后往前,所以如果WebSocketFrameEncoder放在SslHandler后面,那么出站数据会先经过SslHandler(它要求ByteBuf),但此时数据还是WebSocketFrame,所以会出错。 正确的顺序应该是: 在出站方向上,数据从我们的业务处理器发出,先经过WebSocketFrameEncoder(将WebSocketFrame转为ByteBuf),然后再经过SslHandler(对ByteBuf进行加密)。因此,在管道中,WebSocketFrameEncoder应该在SslHandler之后被添加(因为管道是从head到tail,而出站处理是从tail到head,所以后添加的处理器在出站时先执行)。 因此,添加顺序: pipeline.addLast(sslHandler); // 最先添加到管道(靠近head) ... // 添加其他处理器,如编解码器 pipeline.addLast(new WebSocketFrameEncoder()); // 最后添加到管道(靠近tail) 这样,当出站数据(WebSocketFrame)从尾部开始传递时,先遇到WebSocketFrameEncoder(在tail附近),将其编码为ByteBuf,然后往前传递到SslHandler(在head附近),SslHandler处理ByteBuf。 但是,错误信息中显示的是在SslHandler的write方法中出现了异常,因为收到了CloseWebSocketFrame,而不是ByteBuf。这明确说明在SslHandler之前没有将WebSocketFrame编码为ByteBuf。 因此,我们需要检查管道的配置,确保添加了WebSocketFrameEncoder,并且其位置在SslHandler之后(即更靠近tail)。 以下是修正的Netty WebSocket客户端初始化代码片段: ```java public class WebSocketClientInitializer extends ChannelInitializer<SocketChannel> { private final SSLContext sslContext; private final String host; private final int port; private final String path; public WebSocketClientInitializer(SSLContext sslContext, String host, int port, String path) { this.sslContext = sslContext; this.host = host; this.port = port; this.path = path; } @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // 添加SSL处理器(如果需要) SSLEngine engine = sslContext.createSSLEngine(host, port); engine.setUseClientMode(true); pipeline.addLast("ssl", new SslHandler(engine)); // 添加HTTP编解码器 pipeline.addLast(new HttpClientCodec()); // 聚合HTTP分块消息 pipeline.addLast(new HttpObjectAggregator(65536)); // WebSocket协议处理器:负责握手、关闭等 pipeline.addLast(new WebSocketClientProtocolHandler(URI.create("wss://" + host + ":" + port + path))); // 添加WebSocket帧编码器(将WebSocketFrame转为ByteBuf,用于出站) pipeline.addLast(new WebSocketFrameEncoder()); // 添加WebSocket帧解码器(将ByteBuf转为WebSocketFrame,用于入站) pipeline.addLast(new WebSocketFrameDecoder()); // 添加自定义业务处理器 pipeline.addLast(new WebSocketClientHandler()); } } ``` 注意:`WebSocketClientProtocolHandler` 内部已经包含了处理握手和关闭的逻辑,并且它会在握手成功后升级管道(移除HTTP编解码器等)。但是,我们仍然需要显式添加WebSocketFrameEncoder和WebSocketFrameDecoder吗? 实际上,`WebSocketClientProtocolHandler` 在握手成功后会添加必要的WebSocket帧编解码器。所以,我们不应该在初始化时就添加WebSocketFrameEncoder和WebSocketFrameDecoder,而是由协议处理器在握手完成后自动添加。 那么为什么会出现上述错误呢?可能是因为在握手阶段就发生了关闭操作?或者是在握手之前就发送了关闭帧?我们看到错误发生在发送关闭帧时。 另一种可能:在管道中,我们在SslHandler之后没有正确添加将WebSocketFrame转为ByteBuf的编码器。而WebSocketClientProtocolHandler只在握手成功后才添加这些编码器。所以,在握手阶段(还未升级为WebSocket协议)发送WebSocket帧是不允许的。 因此,我们需要确保在握手完成之前不要发送任何WebSocket帧(包括关闭帧)。但是,这个关闭帧可能是握手失败后协议处理器尝试关闭连接时发送的。 我们来看错误信息:`UnsupportedMessageTypeException: io.netty.handler.codec.http.websocketx.CloseWebSocketFrame`。这发生在SslHandler.write阶段,说明我们在握手过程中或者握手失败后,尝试写入了一个CloseWebSocketFrame,而此时管道中还没有WebSocketFrame编码器(因为握手未成功,协议处理器还没有添加编解码器)。 所以,解决方案: 1. 确保在握手成功之前不要发送任何WebSocket帧。 2. 检查握手失败的情况,确保在这种情况下我们不会尝试发送WebSocket帧。 但是,这个错误是Netty内部在尝试处理关闭时发生的,所以可能是Netty协议处理器在握手失败后尝试发送关闭帧,而这时管道还是HTTP的,没有升级为WebSocket,因此无法处理WebSocket帧。 因此,我们需要检查握手失败的原因,并确保在握手失败时,我们使用适当的HTTP响应来关闭,而不是WebSocket关闭帧。 然而,`WebSocketClientProtocolHandler` 应该处理这种情况。所以,可能是我们的管道配置有问题,导致在握手失败时,协议处理器尝试使用WebSocket关闭帧而不是HTTP响应。 实际上,`WebSocketClientProtocolHandler` 内部会处理握手过程,如果握手失败,它会发送一个HTTP请求来关闭连接吗?还是发送WebSocket关闭帧?注意,在握手阶段,连接还是HTTP的,所以应该用HTTP响应来关闭。 所以,这个错误可能是Netty的一个bug,或者是我们错误地配置了管道。 考虑到我们使用的是WebSocket over SSL,可能是SSL握手失败导致连接关闭,然后WebSocketClientProtocolHandler尝试发送一个WebSocket关闭帧,而此时管道还是SSL+HTTP,没有升级,所以发送WebSocket帧就会失败。 因此,我们需要在SSL握手完成后才进行WebSocket握手。而Netty的SslHandler是异步的,所以我们需要等待SSL握手成功后再发送WebSocket握手请求。实际上,WebSocketClientProtocolHandler内部应该已经处理了这种顺序。 但是,错误信息表明在发送CloseWebSocketFrame时,管道中还没有将WebSocket帧转为ByteBuf的编码器,并且SslHandler不能处理WebSocket帧。 所以,我们可能需要自定义处理器来处理握手失败的情况,避免发送WebSocket帧。 然而,更常见的做法是确保在管道初始化时,我们添加了在SSL握手阶段能够处理HTTP对象的编码器(即HttpClientCodec和HttpObjectAggregator),这样在握手失败时,我们可以发送HTTP请求来关闭。 所以,我们按照以下顺序添加处理器: - SslHandler - HttpClientCodec - HttpObjectAggregator - WebSocketClientProtocolHandler 然后,在握手失败时,WebSocketClientProtocolHandler会发送一个HTTP请求来关闭连接,而不是WebSocket关闭帧。 但是,错误信息中提到的CloseWebSocketFrame,说明协议处理器还是尝试发送了WebSocket关闭帧。这可能是因为WebSocketClientProtocolHandler在握手失败后错误地构造了WebSocket关闭帧。 因此,我们需要升级Netty版本,因为这个问题可能是旧版本的一个bug。 如果升级版本不可行,我们可以尝试自定义协议处理器,但这样比较复杂。 另一种解决方案:在管道中,在SslHandler之后添加一个处理器,将WebSocketFrame转换为ByteBuf(即WebSocketFrameEncoder),但是注意,在握手完成之前,我们不能处理WebSocket帧。所以,这个编码器在握手完成之前不应该被使用。 但是,WebSocketClientProtocolHandler在握手成功后会修改管道,添加WebSocket帧编解码器。所以,我们在初始化管道时不应该添加WebSocket帧编解码器。 所以,问题又回到了原点:为什么在握手失败时,协议处理器会发送WebSocket关闭帧? 鉴于时间,我们可以先尝试升级Netty到最新版本(如4.1.68.Final),看看问题是否解决。 如果不能升级,我们需要在管道中在SslHandler之后添加一个通用的编码器,将Object转换为ByteBuf?这并不现实。 因此,我们可能需要捕获这个异常,并忽略它,因为连接已经失败了。 我们可以通过添加一个异常处理器来捕获这个异常并记录为调试信息,因为连接已经关闭,这个错误不影响程序逻辑。 在Netty中,我们可以添加一个异常处理器: pipeline.addLast(new ExceptionHandler()); 其中,ExceptionHandler是一个ChannelInboundHandlerAdapter,重写exceptionCaught方法。 但是,错误发生在出站方向,所以我们需要在出站方向处理异常?实际上,Netty的异常传播机制是双向的,我们可以在任意位置添加异常处理器。 所以,在管道最后添加一个异常处理器: ```java pipeline.addLast(new ChannelDuplexHandler() { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof UnsupportedMessageTypeException) { // 记录日志,忽略 System.out.println("捕获到预期外的消息类型,连接可能已失败: " + cause.getMessage()); } else { // 其他异常传播 ctx.fireExceptionCaught(cause); } } }); ``` 但是,这并不能解决根本问题,只是避免日志中的异常堆栈。 综上所述,根本原因是握手失败后,Netty协议处理器尝试发送WebSocket关闭帧,但此时管道并没有升级,导致SslHandler无法处理WebSocket帧。因此,我们需要先解决握手失败的原因(可能是SSL证书问题、网络问题、服务器拒绝等),而不是处理这个异常。 然而,用户的问题描述中,之前是因为SSL证书问题导致连接被拒绝,现在可能还是同样的问题?所以,我们首先要确保SSL握手成功。 因此,我们回到SSL证书的配置上。在之前的代码中,我们使用了信任所有证书的方式,但可能服务器要求客户端证书?或者证书域名不匹配?等等。 所以,我们需要先确保SSL握手成功,避免握手失败导致后续问题。 总结:优先解决SSL握手失败的问题,确保WebSocket握手能够成功。如果SSL握手成功,那么WebSocket握手就会正常进行,管道会升级,就不会有这个问题了。 如果确实需要在握手失败时避免这个异常,我们可以考虑在WebSocketClientProtocolHandler之前添加一个处理器,在握手失败时直接关闭连接,而不发送任何WebSocket帧。 不过,这需要深入理解Netty的WebSocket协议处理机制,比较复杂。 因此,我们给出两种建议: 1. 优先解决SSL握手问题,确保连接能够成功建立。 2. 如果确定是握手失败后引发的次要异常,可以通过异常处理器忽略该错误。 由于问题描述中服务器说“连接不是ssl/TLS记录”,所以我们的重点应该放在SSL配置上,确保SSL握手成功。 所以,我们回到之前的SSL配置代码,检查是否正确地设置了SSL引擎,以及是否正确禁用了主机名验证(仅测试环境)等。 在之前的代码中,我们使用了一个信任所有证书的SSL上下文,这应该可以避免证书验证问题。但是,服务器可能要求客户端证书?或者服务器使用的协议版本与客户端不匹配? 因此,我们需要检查服务器的SSL配置,确保客户端与服务器兼容。 由于问题复杂,且用户要求解决Netty的异常,我们提供一种在Netty中忽略该异常的方法: ```java // 自定义异常处理器 public class SuppressCloseFrameExceptionHandler extends ChannelDuplexHandler { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof UnsupportedMessageTypeException) { if (cause.getMessage().contains("CloseWebSocketFrame")) { // 忽略此异常 System.out.println("连接失败,忽略后续关闭帧异常"); ctx.close(); return; } } ctx.fireExceptionCaught(cause); } } ``` 在管道中添加这个处理器: pipeline.addLast(new SuppressCloseFrameExceptionHandler()); 将此处理器添加在管道的最后(即最靠近tail的位置)。 但是,这只是一个临时解决方案,根本解决还是要确保SSL握手成功。 由于我们无法得知服务器的具体配置,这里只能提供这些建议。 最后,我们修改之前的管道初始化代码,添加这个异常处理器: ```java @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // 添加SSL处理器 SSLEngine engine = sslContext.createSSLEngine(host, port); engine.setUseClientMode(true); pipeline.addLast("ssl", new SslHandler(engine)); pipeline.addLast(new HttpClientCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new WebSocketClientProtocolHandler(URI.create("wss://" + host + ":" + port + path)); // 添加异常处理器 pipeline.addLast(new SuppressCloseFrameExceptionHandler()); // 注意:WebSocketClientProtocolHandler在握手成功后会添加WebSocket编解码器,所以我们不需要手动添加 // 自定义业务处理器(在握手成功后处理WebSocket帧) pipeline.addLast(new WebSocketClientHandler()); } ``` 希望这能帮助解决问题。 §§相关问题§§ 1. 如何确保 Netty 管道的处理器添加顺序以正确处理 WebSocket over SSL? 2. 怎样在 Netty 中捕获并处理特定异常以避免连接失败时的冗余错误日志? 3. 如何诊断和修复 SSL 握手失败的问题? 4. 在 WebSocket 客户端中,如何处理握手失败后的清理工作? 5. 怎样升级 Netty 版本以修复潜在的协议处理器 bug? </think>### Netty WebSocket 客户端 SSL 连接问题解决方案 以下是修复 `UnsupportedMessageTypeException` 错误的完整 Netty 客户端实现,包含 SSL 配置和管道处理器优化: ```java import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.websocketx.*; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import javax.net.ssl.SSLException; import java.net.URI; import java.net.URISyntaxException; import java.util.Scanner; public class NettyWebSocketClient { private static final String DEVICE_ID = "your_device_id_here"; private static Channel channel; public static void main(String[] args) throws Exception { String url = "wss://127.0.0.1:10556/relayservice?deviceid=" + DEVICE_ID; URI uri = new URI(url); String scheme = uri.getScheme() == null ? "ws" : uri.getScheme(); String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost(); int port = uri.getPort(); if (port == -1) { port = "wss".equalsIgnoreCase(scheme) ? 443 : 80; } // 配置SSL上下文 (信任所有证书 - 仅测试环境) final SslContext sslCtx = "wss".equalsIgnoreCase(scheme) ? SslContextBuilder.forClient() .trustManager(InsecureTrustManagerFactory.INSTANCE) .build() : null; EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); // 关键修复:正确处理SSL和WebSocket帧 if (sslCtx != null) { pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port)); } // HTTP编解码器 pipeline.addLast(new HttpClientCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); // WebSocket处理器 - 修复消息类型问题 WebSocketClientProtocolConfig config = WebSocketClientProtocolConfig.newBuilder() .webSocketUri(uri) .subprotocol(null) .headers(new DefaultHttpHeaders() .add("Device-ID", DEVICE_ID) .add("Connection", "Upgrade") .add("Upgrade", "websocket") .add("Sec-WebSocket-Version", "13")) .handleCloseFrames(true) .build(); pipeline.addLast(new WebSocketClientProtocolHandler(config)); // 自定义帧处理器 pipeline.addLast(new WebSocketFrameHandler()); } }); // 连接到服务器 ChannelFuture future = bootstrap.connect(host, port).sync(); channel = future.channel(); System.out.println("成功连接到服务器: " + uri); // 交互式消息发送 try (Scanner scanner = new Scanner(System.in)) { while (true) { System.out.print("输入消息 (exit退出): "); String input = scanner.nextLine(); if ("exit".equalsIgnoreCase(input)) break; // 发送文本帧 WebSocketFrame frame = new TextWebSocketFrame(input); channel.writeAndFlush(frame); System.out.println("已发送 >> " + input); } } // 优雅关闭 channel.writeAndFlush(new CloseWebSocketFrame()); channel.closeFuture().sync(); } finally { group.shutdownGracefully(); System.out.println("连接已关闭"); } } // 自定义WebSocket帧处理器 public static class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> { @Override protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) { // 处理文本帧 if (frame instanceof TextWebSocketFrame) { String text = ((TextWebSocketFrame) frame).text(); System.out.println("收到消息 << " + text); } // 处理关闭帧 else if (frame instanceof CloseWebSocketFrame) { System.out.println("收到关闭帧"); ctx.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { System.err.println("发生异常: " + cause.getMessage()); ctx.close(); } } } ``` ### 关键修复说明: 1. **正确处理 CloseWebSocketFrame**: ```java // 在协议处理器中启用关闭帧处理 .handleCloseFrames(true) ``` 2. **优化管道处理器顺序**: ```java // 正确顺序: // 1. SSL处理器 // 2. HTTP编解码器 // 3. HTTP聚合器 // 4. WebSocket协议处理器 // 5. 自定义帧处理器 ``` 3. **使用 Netty 内置 WebSocket 类型**: ```java // 发送时使用正确的帧类型 WebSocketFrame frame = new TextWebSocketFrame(input); channel.writeAndFlush(frame); ``` 4. **Maven 依赖**: ```xml <dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.86.Final</version> </dependency> </dependencies> ``` ### 修复原理: 错误 `UnsupportedMessageTypeException` 发生在 Netty 管道中处理器接收到不兼容的数据类型: 1. `SslHandler` 要求处理 `ByteBuf` 类型 2. 但直接传递了 `CloseWebSocketFrame` 对象帧 3. 解决方案是通过 `WebSocketClientProtocolHandler` 正确转换帧类型
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值