在微服务架构中,网关作为所有服务的统一入口,承担着请求路由、负载均衡、安全认证等重要职责。WebSocket作为一种全双工通信协议,在实时消息推送、在线聊天、协同编辑等场景中广泛应用。但很多开发者第一次在Spring Cloud Gateway中配置WebSocket转发时,往往会遇到连接立即断开、跨域报错等"坑"。
我去年在开发一个在线教育平台时,就遇到过这样的问题:前端通过WebSocket连接网关,但握手成功后连接瞬间断开,控制台抛出UnsupportedOperationException异常。经过排查发现,这其实是由于网关的跨域处理方式与WebSocket协议不兼容导致的。本文将带你从零开始,一步步解决这些典型问题。
Spring Cloud Gateway的核心配置围绕三个关键概念展开:
对于WebSocket转发,最基础的配置如下:
yaml复制spring:
cloud:
gateway:
routes:
- id: websocket_route
uri: ws://localhost:8080
predicates:
- Path=/ws/**
这个配置表示所有以/ws开头的请求都会被转发到本地的8080端口WebSocket服务。
在实际生产环境中,WebSocket服务通常会部署多个实例。这时可以通过服务发现和负载均衡来实现高可用:
yaml复制spring:
cloud:
gateway:
routes:
- id: websocket_lb
uri: lb:ws://websocket-service
predicates:
- Path=/ws/**
这里的lb:ws://前缀表示通过负载均衡器访问名为websocket-service的服务。Gateway会从注册中心(如Nacos、Eureka)获取服务实例列表,并采用轮询等策略进行负载均衡。
在某些场景下,我们可能需要将流量按比例分配到不同版本的服务。比如灰度发布时,可以这样配置:
yaml复制spring:
cloud:
gateway:
routes:
- id: websocket_v1
uri: ws://localhost:8081
predicates:
- Path=/ws/**
- Weight=group1,80
- id: websocket_v2
uri: ws://localhost:8082
predicates:
- Path=/ws/**
- Weight=group1,20
这个配置会将80%的WebSocket请求转发到v1版本,20%转发到v2版本。我在实际项目中用这种方式实现了无感知的版本切换,用户体验非常好。
很多开发者配置完WebSocket路由后,会遇到这样的问题:客户端能成功建立连接,但立即断开,控制台报错:
code复制java.lang.UnsupportedOperationException: null
at org.springframework.http.ReadOnlyHttpHeaders.put(ReadOnlyHttpHeaders.java:126)
这个错误通常发生在跨域请求时,根本原因是Gateway默认的跨域处理方式与WebSocket协议不兼容。
WebSocket握手过程实际上是HTTP升级请求。当浏览器发起WebSocket连接时,会先发送一个HTTP请求,包含Upgrade: websocket等Header。如果服务端同意升级,就会返回101状态码,协议切换为WebSocket。
在这个过程中,浏览器会先发送一个OPTIONS请求(预检请求)来检查跨域权限。而Spring Cloud Gateway默认的跨域过滤器在处理这些请求时,会尝试修改响应头,但WebSocket的响应头是只读的,导致UnsupportedOperationException。
最简单的解决方案是在application.yml中配置全局跨域:
yaml复制spring:
cloud:
gateway:
globalcors:
add-to-simple-url-handler-mapping: true
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: "*"
allowedHeaders: "*"
allowCredentials: true
maxAge: 3600
关键参数说明:
allowedOriginPatterns: 允许的源,*表示所有allowedMethods: 允许的HTTP方法allowCredentials: 是否允许携带凭证(如Cookie)maxAge: 预检请求缓存时间这种配置方式简单直接,适合大多数场景。但缺点是灵活性较差,无法根据请求动态调整跨域策略。
对于更复杂的场景,可以通过编码方式实现更灵活的跨域控制:
java复制@Configuration
public class CorsConfig {
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(ctx);
}
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
request.getHeaders().getOrigin());
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
request.getMethod().name());
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "18000");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(ctx);
};
}
}
这种方式的优势在于:
*根据我的项目经验,给出以下建议:
WebSocket连接可能会因为网络问题而断开。为了保持连接稳定,可以配置心跳检测:
yaml复制spring:
cloud:
gateway:
httpclient:
websocket:
ping-interval: 30000
ping-timeout: 10000
这个配置会让Gateway每30秒发送一次ping帧,如果10秒内没有收到pong响应,就会断开连接。
为了防止单个客户端创建过多连接,可以限制每个路由的最大连接数:
yaml复制spring:
cloud:
gateway:
routes:
- id: websocket_route
uri: ws://localhost:8080
predicates:
- Path=/ws/**
metadata:
max-connections: 1000
我们可以通过自定义过滤器来实现一些高级功能,比如认证、限流等。下面是一个简单的认证过滤器示例:
java复制public class AuthFilter implements GatewayFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest()
.getHeaders()
.getFirst("Authorization");
if (!validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private boolean validateToken(String token) {
// 实现你的认证逻辑
return true;
}
}
然后在路由配置中引用这个过滤器:
yaml复制spring:
cloud:
gateway:
routes:
- id: websocket_auth
uri: ws://localhost:8080
predicates:
- Path=/ws/**
filters:
- name: AuthFilter
如果WebSocket连接根本无法建立,可以按以下步骤排查:
连接随机断开通常有以下原因:
对于高并发场景,我总结了以下优化经验:
server.tomcat.threads.max=200server.http2.enabled=truereactor.netty.ioWorkerCount=16