SpringCloud Gateway转发请求到同一个服务的不同端口
SpringCloud Gateway默认不支持将请求路由到一个服务的多个端口
本文将结合Gateway的处理流程,提供一些解决思路
需求背景
公司有一个IM项目,对外暴露了两个端口8081和8082,8081是springboot启动使用的端口,对外提供一些http接口,如获取聊天群组成员列表等,相关配置如下
spring: application: name: im
server:port: 8081servlet:context-path: /im
8082是内部启动了一个netty服务器,处理websocket请求,实现即时通讯
......
......
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
......
......
bootstrap.bind(8082).sync();
以本地环境为例,直连IM项目的请求地址如下:
http://localhost:8081/im/*** (请求http接口)
ws://localhost:8082/ws(进行websocket协议升级)
项目组希望客户端能统一走网关,实现对这两种接口的调用
问题描述
网关使用springcloud gateway,监听端口8080,注册中心使用nacos
正常情况下,gateway根据服务名转发,配置如下
- id: im_routeuri: lb://impredicates:- Path=/im/**
访问网关http://localhost:8080/im/**,即可将请求转发到IM服务
同理,我们希望访问网关ws://localhost:8080/ws,也能转发到IM的netty服务器
一般情况下,如果要通过gateway转发websocket请求,我们需要做如下配置
- id: im_ws_routeuri: lb:ws://impredicates:- Path=/ws/**
但实际上,lb这个schema告诉gateway要走负载,gateway转发的时候,会到nacos拉取im的服务清单,将其替换为 ip + 端口,而nacos上注册的是springboot项目的端口,也就是8081,所以想转发到IM的8082上,默认是没办法利用gateway的服务发现机制的,只能直接配置服务的ip地址,如下
- id: im_ws_routeuri: ws://localhost:8082predicates:- Path=/ws
不过这样也就失去使用网关的意义了
寻找思路
先观察下gateway转发的流程,找到DispatcherHandler的handle方法
@Overridepublic Mono<Void> handle(ServerWebExchange exchange) {if (this.handlerMappings == null) {return createNotFoundError();}if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {return handlePreFlight(exchange);}return Flux.fromIterable(this.handlerMappings).concatMap(mapping -> mapping.getHandler(exchange)).next().switchIfEmpty(createNotFoundError()).onErrorResume(ex -> handleDispatchError(exchange, ex)).flatMap(handler -> handleRequestWith(exchange, handler));}
首先遍历内部的HandlerMapping,依次调用其getHandler方法,寻找能处理当前请求的Handler
HandlerMapping共有四个,一般来说我们配置了上述的路由,会在第三个RoutePredicateHandlerMapping返回一个Handler

Handler类型为FilteringWebHandler,其中包含了一组Filter

找到Handler后,在DispatcherHandler # handle方法的最后一行,调了handleRequestWith方法
private Mono<Void> handleRequestWith(ServerWebExchange exchange, Object handler) {if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {return Mono.empty(); // CORS rejection}if (this.handlerAdapters != null) {for (HandlerAdapter adapter : this.handlerAdapters) {if (adapter.supports(handler)) {return adapter.handle(exchange, handler).flatMap(result -> handleResult(exchange, result));}}}return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));}
然后遍历了HandlerAdapter,一共有四个

这里起作用的是最后一个SimpleHandlerAdapter,进入其handle方法
@Overridepublic Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {WebHandler webHandler = (WebHandler) handler;Mono<Void> mono = webHandler.handle(exchange);return mono.then(Mono.empty());}
调用了上面找到Handler的handle方法
@Overridepublic Mono<Void> handle(ServerWebExchange exchange) {Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);List<GatewayFilter> gatewayFilters = route.getFilters();List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);combined.addAll(gatewayFilters);// TODO: needed or cached?AnnotationAwareOrderComparator.sort(combined);if (logger.isDebugEnabled()) {logger.debug("Sorted gatewayFilterFactories: " + combined);}return new DefaultGatewayFilterChain(combined).filter(exchange);}
最后filter方法依次执行了其中的Filter
@Overridepublic Mono<Void> filter(ServerWebExchange exchange) {return Mono.defer(() -> {if (this.index < filters.size()) {GatewayFilter filter = filters.get(this.index);DefaultGatewayFilterChain chain = new DefaultGatewayFilterChain(this, this.index + 1);return filter.filter(exchange, chain);}else {return Mono.empty(); // complete}});}}
在这些Filter中,有一个ReactiveLoadBalancerClientFilter,会完成从nacos拉取服务清单替换请求地址的任务

看下它的filter方法
@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {return chain.filter(exchange);}// preserve the original urladdOriginalRequestUrl(exchange, url);if (log.isTraceEnabled()) {log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);}URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);String serviceId = requestUri.getHost();Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator.getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),RequestDataContext.class, ResponseData.class, ServiceInstance.class);DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(new RequestDataContext(new RequestData(exchange.getRequest()), getHint(serviceId)));return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {if (!response.hasServer()) {supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response)));throw NotFoundException.create(properties.isUse404(), "Unable to find instance for " + url.getHost());}ServiceInstance retrievedInstance = response.getServer();URI uri = exchange.getRequest().getURI();// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,// if the loadbalancer doesn't provide one.String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";if (schemePrefix != null) {overrideScheme = url.getScheme();}DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,overrideScheme);URI requestUrl = reconstructURI(serviceInstance, uri);if (log.isTraceEnabled()) {log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);}exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response));}).then(chain.filter(exchange)).doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(CompletionContext.Status.FAILED, throwable, lbRequest,exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR))))).doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(CompletionContext.Status.SUCCESS, lbRequest,exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR),new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest()))))));}
首先判断uri配置了lb,需要根据服务名做负载转发
在这个filter前,已经根据请求路径/ws和router配置,将请求地址转换成了 服务名:请求路径的形式,也就是将ws://localhost:8080/ws转化成了ws://im/ws

然后根据服务名im到nacos获取到服务的实际地址
ServiceInstance retrievedInstance = response.getServer();
其中包含了ip和端口

最后替换出实际要转发的地址

所以我们只要在这一步,根据需要将端口8081改成8082,就能实现我们要的效果了
最终解决方案
最初是想自定义一个HandlerMapping完成转发的,但看下来后直接改源码更便捷一些,所以直接在项目中新建一个同路径的类,将源码copy进来,覆盖掉这个Filter

然后在获取到nacos实例后,修改掉端口号,就能改变最终的目标地址
在获取服务实例ServiceInstance retrievedInstance = response.getServer()这行后面加一段代码
if (url.toString().equals("ws://im/ws")) {try {Field field = retrievedInstance.getClass().getDeclaredField("port");field.setAccessible(true);field.set(retrievedInstance, 8082);} catch (Exception e) {e.printStackTrace();}
}
im相关的路由配置
- id: im_routeuri: lb://impredicates:- Path=/im/**- id: im_ws_routeuri: lb:ws://impredicates:- Path=/ws/**
相关文章:
SpringCloud Gateway转发请求到同一个服务的不同端口
SpringCloud Gateway默认不支持将请求路由到一个服务的多个端口 本文将结合Gateway的处理流程,提供一些解决思路 需求背景 公司有一个IM项目,对外暴露了两个端口8081和8082,8081是springboot启动使用的端口,对外提供一些http接口…...
【模块一】kubernetes容器编排进阶实战之pod的调度流程,pause容器及init容器
pod的调度流程及常见状态 pod的调度流 pod的常见状态 Unschedulable:#Pod不能被调度,kube-scheduler没有匹配到合适的node节点 PodScheduled:#pod正处于调度中,在kube-scheduler刚开始调度的时候,还没有将pod分配…...
PySpark3:pyspark.sql.functions常见的60个函数
目录 一、常见的60个函数 1、col 2、lit 3、sum 4、avg/mean 5、count 6、max 7、min 8、concat 9、substring 10、lower 11、upper 12、trim 13、ltrim 14、rtrim 15、split 16、explode 17、collect_list 18、collect_set 19、asc 20、desc 21、when 2…...
Python操作neo4j库py2neo使用之创建和查询(二)
Python操作neo4j库py2neo使用之创建和查询(二) py2neo 创建操作 1、连接数据库 from py2neo import Graph graph Graph("bolt://100.100.20.55:7687", auth(user, pwd), nameneo4j)2、创建Node from py2neo import Node, Subgraph # 创建…...
力扣11.23
1964. 找出到每个位置为止最长的有效障碍赛跑路线 你打算构建一些障碍赛跑路线。给你一个 下标从 0 开始 的整数数组 obstacles ,数组长度为 n ,其中 obstacles[i] 表示第 i 个障碍的高度。 对于每个介于 0 和 n - 1 之间(包含 0 和 n - 1&…...
golang实现TCP服务器与客户端的断线自动重连功能
1.服务端 2.客户端 生成服务端口程序: 生成客户端程序: 测试断线重连: 初始连接成功...
数据结构 (6)栈的应用举例
1. 递归调用 递归函数在执行时,会将每一层的函数调用信息(包括局部变量、参数和返回地址)存储在栈中。当递归函数返回时,这些信息会从栈中弹出,以便恢复之前的执行状态。栈的后进先出(LIFO)特性…...
谁的年龄最小(结构体专题)
题目描述 设计一个结构体类型,包含姓名、出生日期。其中出生日期又包含年、月、日三部分信息。输入n个好友的信息,输出年龄最小的好友的姓名和出生日期。 输入描述 首先输入一个整数n(1<n<10),表示好友人数,然后输入n行&…...
【论文笔记】LLaVA-KD: A Framework of Distilling Multimodal Large Language Models
Abstract 大语言模型(Large Language Models, LLM)的成功,使得研究者为了统一视觉和语言的理解去探索多模态大预言模型(Multimodal Large Language Models, MLLM)。 但是MLLM庞大的模型和复杂的计算使其很难应用在资源受限的环境,小型MLLM(s-MLLM)的表现…...
M|大脑越狱
rating: 7.0 豆瓣: 7.6 上映时间: “2015” 类型: M悬疑 导演: 约瑟夫怀特 Joseph White 主演: 亚历山大欧文 Alexander Owen爱德华富兰克林 Edward Franklin 国家/地区: 英国 片长/分钟: 20分钟 M|大脑越狱 想法不错,但是逻辑比较一般。属于…...
数据库编程(sqlite3)
一:数据库分类 常用的数据库 大型数据库 :Oracle商业、多平台、关系型数据库功能最强大、最复杂、市场占比最高的商业数据库 中型数据库 :Server是微软开发的数据库产品,主要支持windows平台 小型数据库 : mySQL是一个小型关系型…...
【C语言】关键字详解
【C语言】关键字详解 文章目录 [TOC](文章目录) 前言一、char1.定义字符串类型2.定义字符类型 二、short三、int四、long五、signed六、unsigned七、float八、double九、struct、union、enum十、void1.void用于函数声明,没有返回值的函数,其类型为 void。…...
什么是计算机网络
什么是计算机网络? 计算机网络的定义计算机网络的分类按覆盖范围分类按拓扑结构分类按通信传输介质分类按信号频带占用方式分类 计算机网络的功能信息交换资源共享分布式处理 计算机网络的组成计算机网络的定义计算机网络的分类按覆盖范围分类按拓扑结构分类按通信传…...
【大数据学习 | Spark-Core】Spark的分区器(HashPartitioner和RangePartitioner)
之前学过的kv类型上面的算子 groupby groupByKey reduceBykey sortBy sortByKey join[cogroup left inner right] shuffle的 mapValues keys values flatMapValues 普通算子,管道形式的算子 shuffle的过程是因为数据产生了打乱重分,分组、排序、join等…...
CSS3_BFC(十二)
BFC MDN对BFC的解释:块格式化上下文(Block Formating Context, BFC)是web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。 1、开启BFC flow-root对内容的影响是最低的&am…...
C0032.在Clion中使用MSVC编译器编译opencv的配置方法
使用MSVC编译器编译opencv的配置方法...
微信小程序中会议列表页面的前后端实现
题外话:想通过集成腾讯IM来解决即时聊天的问题,如果含语音视频,腾讯组件一年5万起步,贵了!后面我们改为自己实现这个功能,这里只是个总结而已。 图文会诊需求 首先是个图文列表界面 同个界面可以查看具体…...
WEB攻防-通用漏洞文件上传二次渲染.htaccess变异免杀
知识点: 1、文件上传-二次渲染 2、文件上传-简单免杀变异 3、文件上传-.htaccess妙用 4、文件上传-PHP语言特性 1、上传后门时,文件内容带.就不行 这时可以上传一个转换后的ip地址,ip地址对应网站包含后门代码 转换后的int会在访问的时候…...
vue实现列表滑动下拉加载数据
一、实现效果 二、实现思路 使用滚动事件监听器来检测用户是否滚动到底部,然后加载更多数据 监听滚动事件。检测用户是否滚动到底部。加载更多数据。 三、案例代码 <div class"drawer-content"><div ref"loadMoreTrigger" class&q…...
全面解析:HTML页面的加载全过程(四)--浏览器渲染之样式计算
主线程遍历得到的 DOM 树,依次为树中的每个节点计算出它最终的样式,称之为 Computed Style。 通过前面生成的DOM 树和 CSSOM 树,遍历 DOM 树,为每一个 DOM 节点,计算它的所有 CSS 属性,最后会得到一棵带有…...
多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
Objective-C常用命名规范总结
【OC】常用命名规范总结 文章目录 【OC】常用命名规范总结1.类名(Class Name)2.协议名(Protocol Name)3.方法名(Method Name)4.属性名(Property Name)5.局部变量/实例变量(Local / Instance Variables&…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...
Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...
