SpringCloud Gateway转发请求到同一个服务的不同端口

SpringCloud Gateway默认不支持将请求路由到一个服务的多个端口
本文将结合Gateway的处理流程,提供一些解决思路

需求背景

公司有一个IM项目,对外暴露了两个端口8081和8082,8081是springboot启动使用的端口,对外提供一些http接口,如获取聊天群组成员列表等,相关配置如下

spring: 
  application: 
    name: im
server:
  port: 8081
  servlet:
    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_route
      uri: lb://im
      predicates:
        - Path=/im/**

访问网关http://localhost:8080/im/**,即可将请求转发到IM服务
同理,我们希望访问网关ws://localhost:8080/ws,也能转发到IM的netty服务器

一般情况下,如果要通过gateway转发websocket请求,我们需要做如下配置

    - id: im_ws_route
      uri: lb:ws://im
      predicates:
        - Path=/ws/**

但实际上,lb这个schema告诉gateway要走负载,gateway转发的时候,会到nacos拉取im的服务清单,将其替换为 ip + 端口,而nacos上注册的是springboot项目的端口,也就是8081,所以想转发到IM的8082上,默认是没办法利用gateway的服务发现机制的,只能直接配置服务的ip地址,如下

    - id: im_ws_route
      uri: ws://localhost:8082
      predicates:
        - Path=/ws

不过这样也就失去使用网关的意义了

寻找思路

先观察下gateway转发的流程,找到DispatcherHandler的handle方法

	@Override
	public 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方法

	@Override
	public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
		WebHandler webHandler = (WebHandler) handler;
		Mono<Void> mono = webHandler.handle(exchange);
		return mono.then(Mono.empty());
	}

调用了上面找到Handler的handle方法

	@Override
	public 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

		@Override
		public 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方法

    @Override
    public 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 url
        addOriginalRequestUrl(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_route
      uri: lb://im
      predicates:
        - Path=/im/**
      
    - id: im_ws_route
      uri: lb:ws://im
      predicates:
        - Path=/ws/**

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921634.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

全面监测Exchange邮件服务器的关键指标

在当今高度信息化的社会&#xff0c;Exchange邮件服务器已成为企业日常通信的重要组成部分。为了确保邮件服务器的稳定运行&#xff0c;及时发现潜在问题并采取相应的解决措施显得尤为重要。监控易作为一款专业的监控工具&#xff0c;为Exchange邮件服务器提供了全方位的监测功…

实用功能,觊觎(Edge)浏览器的内置截(长)图功能

Edge浏览器内置截图功能 近年来&#xff0c;Edge浏览器不断更新和完善&#xff0c;也提供了长截图功能。在Edge中&#xff0c;只需点击右上角的“...”&#xff0c;然后选择“网页捕获”->“捕获整页”&#xff0c;即可实现长截图。这一功能的简单易用&#xff0c;使其成为…

IDEA2023版本配置项目全局编码

IDEA默认的项目编码是UTF-8&#xff0c;有时候拿到别人的代码使用的编码是GBK&#xff0c;虽然可以在idea右下角进行修改&#xff0c;但是一个一个的修改太慢了。所以需要去进行该项目的编码全局配置。接下来直接讲步骤&#xff0c;以IDEA2023版本为例。 第一步 File>Sett…

【Spiffo】环境配置:VScode+Windows开发环境

摘要&#xff1a; 在Linux下直接开发有时候不习惯快捷键和操作逻辑&#xff0c;用Windows的话其插件和工具都更齐全、方便&#xff0c;所以配置一个Windows的开发环境能一定程度提升效率。 思路&#xff1a; 自己本地网络内远程连接自己的虚拟机&#xff08;假定用的是虚拟机…

计算机网络 实验六 组网实验

一、实验目的 通过构造不同的网络拓扑结构图并进行验证&#xff0c;理解分组转发、网络通信及路由选择的原理&#xff0c;理解交换机和路由器在子网划分中的不同作用。 二、实验原理 组网实验是指将多个计算机通过网络连接起来&#xff0c;实现数据的共享和通信。 组网需要考虑…

springboot vue工资管理系统源码和答辩PPT论文

人类现已迈入二十一世纪&#xff0c;科学技术日新月异&#xff0c;经济、资讯等各方面都有了非常大的进步&#xff0c;尤其是资讯与网络技术的飞速发展&#xff0c;对政治、经济、军事、文化等各方面都有了极大的影响。 利用电脑网络的这些便利&#xff0c;发展一套工资管理系统…

【PPTist】添加PPT模版

前言&#xff1a;这篇文章来探索一下如何应用其他的PPT模版&#xff0c;给一个下拉菜单&#xff0c;列出几个项目中内置的模版 PPT模版数据 &#xff08;一&#xff09;增加菜单项 首先在下面这个菜单中增加一个“切换模版”的菜单项&#xff0c;点击之后在弹出框中显示所有的…

输入/输出管理 III(磁盘和固态硬盘)

一、磁盘 【总结】&#xff1a; 磁盘&#xff08;Disk&#xff09;是由表面涂有磁性物质的物理盘片&#xff0c;通过一个称为磁头的导体线圈从磁盘存取数据。在读&#xff0f;写操作期间&#xff0c;磁头固定&#xff0c;磁盘在下面高速旋转。如下图所示&#xff1a; 磁盘盘面…

链表算法速成计划

链表算法速成计划 1.准备工作 1.1创建链表节点结构体 struct ListNode {int val;ListNode* next;ListNode() : val(0), next(NULL) {}ListNode(int x) : val(x), next(NULL) {}ListNode(int x, ListNode* next) : val(x), next(next) {} };1.2 在IDE中创建链表代码 ListNod…

iPhone或iPad接收的文件怎么找?怎样删除?

因为iOS系统和iPadOS系统的特殊性&#xff0c;在使用AirDroid传输文件之后&#xff0c;往往存在“找文件”的难题。本篇文章一次性解释清楚。 文件传输到iPhone/iPad之后&#xff0c;怎样才能找到&#xff1f; iPhone/iPad接收到的全部文件都可以在AirDroid右上角的【时钟】按钮…

猎板 HDI 多阶工艺:高密度互连的核心技术

一、猎板HDI引言 随着电子设备不断向小型化、高性能化和多功能化发展&#xff0c;PCB&#xff08;印刷电路板&#xff09;的设计与制造面临着前所未有的挑战。HDI&#xff08;高密度互连&#xff09;技术应运而生&#xff0c;而其中的多阶工艺更是满足了对更高布线密度、更小尺…

VUE 的前置知识

一、JavaScript----导图导出 1. JS 提供的导入导出机制&#xff0c;可以实现按需导入 1.1 在html页面中可以把JS文件通过 <script src"showMessage.js"></script> 全部导入 1.2 通过在JS文件中写export关键字导出通过 <script src"showMessage…

---Arrays类

一 java 1.Arrays类 1.1 toString&#xff08;&#xff09; 1.2 arrays.sort( )-----sort排序 1&#xff09;直接调用sort&#xff08;&#xff09; Arrays.sort() 方法的默认排序顺序是 从小到大&#xff08;升序&#xff09;。 2&#xff09;定制排序【具体使用时 调整正负…

【H2O2|全栈】JS进阶知识(六)ES6(2)

目录 前言 开篇语 准备工作 Set和Map 基本概念 Set 相互转化 常见属性和API 数组去重 并集、交集和差集 Map 转化 常见的属性和API Set和Map的区别 This的指向 function函数 箭头函数 修改this 使用方式 三种方式的异同 案例 更改this指向为obj 求数组数…

Node基本使用

1. 创建自定义全局指令 1.1 新建一个空的文件夹, 创建一个cli.js文件 1.2 在cli.js写入内容 /usr/bin/env就是让系统使用node来执行你的脚本文件。 #! /usr/bin/env node1.3 执行终端指令 // 在文件夹 node-project 的终端下执行指令npm init执行完后package.json结构如下,…

Python编程技巧:多变量赋值的优雅艺术

在Python编程的世界里&#xff0c;有许多令人惊叹的语法特性&#xff0c;而多变量赋值就像是一颗闪耀的明珠&#xff0c;它不仅让代码更优雅&#xff0c;还能提升程序的执行效率。今天我们就深入探讨这个看似简单却蕴含深意的编程技巧。 基础认识 传统的变量赋值方式&#xff…

Transformer架构笔记

Attention is All You Need. 3.Model Architecture 3.1 整体架构如图 3.2 Encoder与Decoder Encoder&#xff1a;由 N 6 N6 N6个相同的Block/Layer堆叠而成。每个Block有两个子层sub-layer&#xff1a;多头注意力和MLP&#xff08;FFN&#xff0c;前馈神经网络&#xff09;&…

网络安全简单入门与扫描

网络安全简单入门 内容大纲 策略制定安全工具其他 1、安全策略 1.1、安全三要素 要全面地认识一个安全问题,我们有很多种办法,但首先要理解安全问题的组成属性。前人通过无数实践,最后将安全的属性总结为安全三要素,简称CIA。 安全三要素是安全的基本组成元素,分别是机密性…

在WPF程序中实现PropertyGrid功能

使用C#开发过Windows Forms的都知道&#xff0c;在Windows Forms程序中&#xff0c;有一个PropertyGrid控件&#xff0c;可以用于显示对象的属性&#xff0c;在WPF中并没有默认提供此功能的控件&#xff0c;今天以一个简单的小例子&#xff0c;简述在WPF中借助WinForm的Propert…

大模型时代的具身智能系列专题(十四)

冯晨团队 冯晨是纽约大学的副教授。他对通过多学科使用启发研究实现机器人主动和协作感知和学习感兴趣&#xff0c;这些研究源自建筑、制造和运输领域。在纽约大学之前&#xff0c;冯晨是马萨诸塞州剑桥市三菱电机研究实验室 (MERL) 计算机视觉小组的研究科学家&#xff0c;专…