当前位置: 首页 > news >正文

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的处理流程&#xff0c;提供一些解决思路 需求背景 公司有一个IM项目&#xff0c;对外暴露了两个端口8081和8082&#xff0c;8081是springboot启动使用的端口&#xff0c;对外提供一些http接口…...

【模块一】kubernetes容器编排进阶实战之pod的调度流程,pause容器及init容器

pod的调度流程及常见状态 pod的调度流 pod的常见状态 Unschedulable&#xff1a;#Pod不能被调度&#xff0c;kube-scheduler没有匹配到合适的node节点 PodScheduled&#xff1a;#pod正处于调度中&#xff0c;在kube-scheduler刚开始调度的时候&#xff0c;还没有将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使用之创建和查询&#xff08;二&#xff09; 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 &#xff0c;数组长度为 n &#xff0c;其中 obstacles[i] 表示第 i 个障碍的高度。 对于每个介于 0 和 n - 1 之间&#xff08;包含 0 和 n - 1&…...

golang实现TCP服务器与客户端的断线自动重连功能

1.服务端 2.客户端 生成服务端口程序: 生成客户端程序: 测试断线重连: 初始连接成功...

数据结构 (6)栈的应用举例

1. 递归调用 递归函数在执行时&#xff0c;会将每一层的函数调用信息&#xff08;包括局部变量、参数和返回地址&#xff09;存储在栈中。当递归函数返回时&#xff0c;这些信息会从栈中弹出&#xff0c;以便恢复之前的执行状态。栈的后进先出&#xff08;LIFO&#xff09;特性…...

谁的年龄最小(结构体专题)

题目描述 设计一个结构体类型&#xff0c;包含姓名、出生日期。其中出生日期又包含年、月、日三部分信息。输入n个好友的信息&#xff0c;输出年龄最小的好友的姓名和出生日期。 输入描述 首先输入一个整数n(1<n<10)&#xff0c;表示好友人数&#xff0c;然后输入n行&…...

【论文笔记】LLaVA-KD: A Framework of Distilling Multimodal Large Language Models

Abstract 大语言模型(Large Language Models, LLM)的成功&#xff0c;使得研究者为了统一视觉和语言的理解去探索多模态大预言模型(Multimodal Large Language Models, MLLM)。 但是MLLM庞大的模型和复杂的计算使其很难应用在资源受限的环境&#xff0c;小型MLLM(s-MLLM)的表现…...

M|大脑越狱

rating: 7.0 豆瓣: 7.6 上映时间: “2015” 类型: M悬疑 导演: 约瑟夫怀特 Joseph White 主演: 亚历山大欧文 Alexander Owen爱德华富兰克林 Edward Franklin 国家/地区: 英国 片长/分钟: 20分钟 M&#xff5c;大脑越狱 想法不错&#xff0c;但是逻辑比较一般。属于…...

数据库编程(sqlite3)

一&#xff1a;数据库分类 常用的数据库 大型数据库 &#xff1a;Oracle商业、多平台、关系型数据库功能最强大、最复杂、市场占比最高的商业数据库 中型数据库 &#xff1a;Server是微软开发的数据库产品&#xff0c;主要支持windows平台 小型数据库 : mySQL是一个小型关系型…...

【C语言】关键字详解

【C语言】关键字详解 文章目录 [TOC](文章目录) 前言一、char1.定义字符串类型2.定义字符类型 二、short三、int四、long五、signed六、unsigned七、float八、double九、struct、union、enum十、void1.void用于函数声明&#xff0c;没有返回值的函数&#xff0c;其类型为 void。…...

什么是计算机网络

什么是计算机网络&#xff1f; 计算机网络的定义计算机网络的分类按覆盖范围分类按拓扑结构分类按通信传输介质分类按信号频带占用方式分类 计算机网络的功能信息交换资源共享分布式处理 计算机网络的组成计算机网络的定义计算机网络的分类按覆盖范围分类按拓扑结构分类按通信传…...

【大数据学习 | Spark-Core】Spark的分区器(HashPartitioner和RangePartitioner)

之前学过的kv类型上面的算子 groupby groupByKey reduceBykey sortBy sortByKey join[cogroup left inner right] shuffle的 mapValues keys values flatMapValues 普通算子&#xff0c;管道形式的算子 shuffle的过程是因为数据产生了打乱重分&#xff0c;分组、排序、join等…...

CSS3_BFC(十二)

BFC MDN对BFC的解释&#xff1a;块格式化上下文&#xff08;Block Formating Context, BFC&#xff09;是web页面的可视CSS渲染的一部分&#xff0c;是块盒子的布局过程发生的区域&#xff0c;也是浮动元素与其他元素交互的区域。 1、开启BFC flow-root对内容的影响是最低的&am…...

C0032.在Clion中使用MSVC编译器编译opencv的配置方法

使用MSVC编译器编译opencv的配置方法...

微信小程序中会议列表页面的前后端实现

题外话&#xff1a;想通过集成腾讯IM来解决即时聊天的问题&#xff0c;如果含语音视频&#xff0c;腾讯组件一年5万起步&#xff0c;贵了&#xff01;后面我们改为自己实现这个功能&#xff0c;这里只是个总结而已。 图文会诊需求 首先是个图文列表界面 同个界面可以查看具体…...

WEB攻防-通用漏洞文件上传二次渲染.htaccess变异免杀

知识点&#xff1a; 1、文件上传-二次渲染 2、文件上传-简单免杀变异 3、文件上传-.htaccess妙用 4、文件上传-PHP语言特性 1、上传后门时&#xff0c;文件内容带.就不行 这时可以上传一个转换后的ip地址&#xff0c;ip地址对应网站包含后门代码 转换后的int会在访问的时候…...

vue实现列表滑动下拉加载数据

一、实现效果 二、实现思路 使用滚动事件监听器来检测用户是否滚动到底部&#xff0c;然后加载更多数据 监听滚动事件。检测用户是否滚动到底部。加载更多数据。 三、案例代码 <div class"drawer-content"><div ref"loadMoreTrigger" class&q…...

全面解析:HTML页面的加载全过程(四)--浏览器渲染之样式计算

主线程遍历得到的 DOM 树&#xff0c;依次为树中的每个节点计算出它最终的样式&#xff0c;称之为 Computed Style。 通过前面生成的DOM 树和 CSSOM 树&#xff0c;遍历 DOM 树&#xff0c;为每一个 DOM 节点&#xff0c;计算它的所有 CSS 属性&#xff0c;最后会得到一棵带有…...

EmuELEC 3.9 vs 4.0+:不同版本写入EMMC的详细操作指南(附常见问题解决)

EmuELEC 3.9与4.0版本EMMC写入全流程实战解析 1. 版本差异与核心机制解析 EmuELEC作为开源游戏系统&#xff0c;其3.9与4.0版本在EMMC写入机制上存在根本性架构差异。理解这些差异是避免操作失误的前提。 3.9版本的技术特点&#xff1a; 采用传统的installtointernal.sh脚本…...

Qt, C++数据类型扩展问题

Qt项目中ObjectDic类的类型扩展与代码优化 前言 在Qt项目开发中&#xff0c;我们经常会遇到需要处理不同类型数据的情况&#xff0c;尤其是当涉及到负数时&#xff0c;类型的选择就显得尤为重要。本文将详细介绍如何在Qt项目中扩展ObjectDic类的类型支持&#xff0c;从无符号整…...

AgentCPM-Report研报系统实操:Pixel Epic贤者响应延迟优化教程

AgentCPM-Report研报系统实操&#xff1a;Pixel Epic贤者响应延迟优化教程 1. 认识Pixel Epic智识终端 Pixel Epic是一款基于AgentCPM-Report大模型构建的创新研究报告辅助系统。与传统AI工具不同&#xff0c;它将枯燥的科研过程转化为一场像素风格的RPG冒险。在这个系统中&a…...

光伏板缺陷检测实战:从数据集构建到YOLO模型训练全流程解析

1. 光伏板缺陷检测的现实意义 光伏发电作为清洁能源的重要组成部分&#xff0c;其运维效率直接影响发电量收益。我在实地考察中发现&#xff0c;一块被鸟粪覆盖的光伏板&#xff0c;发电效率可能下降30%以上&#xff1b;而热斑效应更会导致组件永久性损伤。传统人工巡检每天最多…...

宠物店主的福音:用LongCat一键生成宠物服装电商主图,省时省力

宠物店主的福音&#xff1a;用LongCat一键生成宠物服装电商主图&#xff0c;省时省力 1. 为什么宠物店主需要AI图片编辑工具 开宠物店的朋友们都知道&#xff0c;商品主图的质量直接影响销量。一件宠物小衣服&#xff0c;如果只是平铺拍摄或者随便套在模特身上&#xff0c;很…...

Fish-Speech-1.5在短视频生产的应用:批量生成多语种配音方案

Fish-Speech-1.5在短视频生产的应用&#xff1a;批量生成多语种配音方案 1. 引言 短视频内容创作正面临着一个普遍痛点&#xff1a;多语言配音成本高、周期长。传统方式下&#xff0c;一个MCN机构要为一条短视频制作中文、英文、日文三种语言的配音&#xff0c;需要分别联系不…...

从零到一:深度解析BertTokenizer.from_pretrained的加载机制与实战技巧

1. 初识BertTokenizer.from_pretrained&#xff1a;你的NLP敲门砖 第一次接触Hugging Face的Transformers库时&#xff0c;我被BertTokenizer.from_pretrained()这个方法深深吸引了。它就像是一把万能钥匙&#xff0c;能快速打开各种预训练语言模型的大门。记得当时我尝试用传统…...

从‘腐蚀液’到‘设计美学’:PCB布线‘禁止直角’这条规则是怎么流行起来的?

从工艺限制到设计美学&#xff1a;PCB布线"禁止直角"规则的历史演变 在电子工程领域&#xff0c;PCB布线中"禁止直角"的规则几乎成为了一种行业圣经。从大学实验室到商业设计部门&#xff0c;新手工程师们总是被反复告诫要避免在布线中使用90度转角。但有趣…...

投入式水位监测站 地下水位监测设备

地下水位自动监测设备&#xff0c;核心亮点在于“本安防爆设计”&#xff0c;严格遵循本安型防爆标准&#xff0c;从电路设计、材质选用、结构防护三方面杜绝点火源&#xff0c;确保在井下易燃易爆气体环境中安全运行&#xff0c;彻底消除设备运行带来的安全隐患&#xff0c;真…...

Flutter项目卡在‘assembleDebug’?Gradle配置优化全攻略

1. 为什么Flutter项目会卡在assembleDebug阶段&#xff1f; 这个问题困扰过无数Flutter开发者&#xff0c;尤其是刚入门的新手。当你满怀期待地运行flutter run命令&#xff0c;结果控制台卡在Running Gradle task assembleDebug...一动不动&#xff0c;那种感觉就像等一辆永远…...