分析
我们知道,路由转发是网关处理完毕所有过滤逻辑之后的最后一个要执行的操作,它负责将我们的请求最终转发到某一个指定的后端服务实例上去,这里我们参考SpringCloudGateway的实现方式来模拟一个路由转发过滤器。
Spring Cloud中的路由转发过滤器是Spring Cloud Gateway(一种微服务网关)中的一种组件,用于对传入的HTTP请求进行过滤和转发操作。这些过滤器允许我们在请求到达目标服务之前对请求进行修改、验证、日志记录等操作。以下是路由转发过滤器的主要作用和为什么需要它们的原因:
-
请求修改和重定向:路由转发过滤器允许您修改请求的各个部分,包括请求头、请求体、请求参数等,以适应目标服务的要求。您可以添加、删除或修改请求信息,甚至可以重定向请求到不同的目标服务,实现请求路由的动态性。
-
安全性:通过路由转发过滤器,您可以添加安全性相关的功能,例如身份验证和授权,以确保只有授权用户可以访问某些服务。这有助于保护微服务架构中的各个服务免受未经授权的访问。
-
缓存:您可以使用过滤器来实现请求和响应的缓存,以减轻目标服务的负载,提高性能,并减少响应时间。这对于处理大量请求的服务非常有用。
-
日志和监控:路由转发过滤器还可以用于记录请求和响应的信息,以便进行监控和故障排除。您可以在过滤器中添加日志记录和指标收集的功能,以了解请求的性能和状态。
-
流量控制:通过路由转发过滤器,您可以实现流量控制和限流,以防止某个服务被过多的请求压垮。这有助于维护服务的可用性和性能。
-
请求转发和负载均衡:最常见的用途是将请求转发到后端的多个目标服务,并执行负载均衡策略,以确保请求均匀地分发到不同的服务实例。
代码实现
上面已经说了很多为什么要使用路由转发过滤器,那么我们现在来分析一下路由转发过滤器的实现方式。 从早期我们的架构设计图上,我们就已经分析出了一件事情,就是我们使用的是异步的方式去发送我们的http请求,这里我用的是Netty配合AsyncHttpClient的方式来实现的异步IO通信功能。如果对这一块有兴趣可以自己搜索:Netty异步IO通信模型的相关知识以及Netty配合AsyncHttpClient的使用。
c
package blossom.project.core.netty;
import blossom.project.core.Config;
import blossom.project.core.LifeCycle;
import blossom.project.core.helper.AsyncHttpHelper;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.EventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClient;
import org.asynchttpclient.DefaultAsyncHttpClientConfig;
import java.io.IOException;
/**
* @author: ZhangBlossom
* @date: 2023/10/23 13:57
* @contact: QQ:4602197553
* @contact: WX:qczjhczs0114
* @blog: https://blog.csdn.net/Zhangsama1
* @github: https://github.com/ZhangBlossom
*/
@Slf4j
public class NettyHttpClient implements LifeCycle {
private final Config config;
private final EventLoopGroup eventLoopGroupWoker;
private AsyncHttpClient asyncHttpClient;
public NettyHttpClient(Config config, EventLoopGroup eventLoopGroupWoker) {
this.config = config;
this.eventLoopGroupWoker = eventLoopGroupWoker;
init();
}
/**
* 使用netty框架执行异步http的时候可以使用到
* netty本身就支持bio、nio、
*/
@Override
public void init() {
DefaultAsyncHttpClientConfig.Builder builder = new DefaultAsyncHttpClientConfig.Builder()
.setEventLoopGroup(eventLoopGroupWoker)
.setConnectTimeout(config.getHttpConnectTimeout())
.setRequestTimeout(config.getHttpRequestTimeout())
.setMaxRedirects(config.getHttpMaxRequestRetry())
.setAllocator(PooledByteBufAllocator.DEFAULT) //池化的byteBuf分配器,提升性能
.setCompressionEnforced(true)
.setMaxConnections(config.getHttpMaxConnections())
.setMaxConnectionsPerHost(config.getHttpConnectionsPerHost())
.setPooledConnectionIdleTimeout(config.getHttpPooledConnectionIdleTimeout());
this.asyncHttpClient = new DefaultAsyncHttpClient(builder.build());
}
@Override
public void start() {
AsyncHttpHelper.getInstance().initialized(asyncHttpClient);
}
@Override
public void shutdown() {
if (asyncHttpClient != null) {
try {
this.asyncHttpClient.close();
} catch (IOException e) {
log.error("NettyHttpClient shutdown error", e);
}
}
}
}
直接看代码的话其实就是我通过整合这两个工具,实现了使用Netty完成异步通信的功能。 并初始化完毕了我们的AsyncHttpClient这个异步http请求发送工具。 而我们封装完毕这个工具之后,要实现对请求的响应就比较容易了,我们只需要将我们的响应内容写回到我们请求的响应体中即可。
c
package blossom.project.core.filter.router;
import blossom.project.common.enums.ResponseCode;
import blossom.project.common.exception.ConnectException;
import blossom.project.common.exception.ResponseException;
import blossom.project.core.ConfigLoader;
import blossom.project.core.context.GatewayContext;
import blossom.project.core.filter.Filter;
import blossom.project.core.filter.FilterAspect;
import blossom.project.core.helper.AsyncHttpHelper;
import blossom.project.core.helper.ResponseHelper;
import blossom.project.core.response.GatewayResponse;
import lombok.extern.slf4j.Slf4j;
import org.asynchttpclient.Request;
import org.asynchttpclient.Response;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import static blossom.project.common.constant.FilterConst.*;
/**
* @author: ZhangBlossom
* @date: 2023/11/1 12:51
* @contact: QQ:4602197553
* @contact: WX:qczjhczs0114
* @blog: https://blog.csdn.net/Zhangsama1
* @github: https://github.com/ZhangBlossom
* RouterFilter类
* 路由过滤器 执行路由转发操作 --- 网关核心过滤器
*/
@Slf4j
@FilterAspect(id=ROUTER_FILTER_ID,
name = ROUTER_FILTER_NAME,
order = ROUTER_FILTER_ORDER)
public class RouterFilter implements Filter {
@Override
public void doFilter(GatewayContext gatewayContext) throws Exception {
Request request = gatewayContext.getRequest().build();
CompletableFuture<Response> future = AsyncHttpHelper.getInstance().executeRequest(request);
boolean whenComplete = ConfigLoader.getConfig().isWhenComplete();
if (whenComplete) {
future.whenComplete((response, throwable) -> {
complete(request, response, throwable, gatewayContext);
});
} else {
future.whenCompleteAsync((response, throwable) -> {
complete(request, response, throwable, gatewayContext);
});
}
}
private void complete(Request request,
Response response,
Throwable throwable,
GatewayContext gatewayContext) {
gatewayContext.releaseRequest();
try {
if (Objects.nonNull(throwable)) {
String url = request.getUrl();
if (throwable instanceof TimeoutException) {
log.warn("complete time out {}", url);
gatewayContext.setThrowable(new ResponseException(ResponseCode.REQUEST_TIMEOUT));
gatewayContext.setResponse(GatewayResponse.buildGatewayResponse(ResponseCode.REQUEST_TIMEOUT));
} else {
gatewayContext.setThrowable(new ConnectException(throwable,
gatewayContext.getUniqueId(),
url, ResponseCode.HTTP_RESPONSE_ERROR));
gatewayContext.setResponse(GatewayResponse.buildGatewayResponse(ResponseCode.HTTP_RESPONSE_ERROR));
}
} else {
gatewayContext.setResponse(GatewayResponse.buildGatewayResponse(response));
}
} catch (Throwable t) {
gatewayContext.setThrowable(new ResponseException(ResponseCode.INTERNAL_ERROR));
gatewayContext.setResponse(GatewayResponse.buildGatewayResponse(ResponseCode.INTERNAL_ERROR));
log.error("complete error", t);
} finally {
gatewayContext.written();
ResponseHelper.writeResponse(gatewayContext);
}
}
}
上面的代码还是比较容易分析出来的。 我们先来分析doFilter方法,首先这个方法会构建好我们的请求,之后使用我们封装好的http工具异步的执行这个请求,这里我们用到了CompletableFuture这个工具来异步接收请求结果,当请求执行完毕之后会执行我们的回调方法。 这里我们默认使用的是单异步模式,当我们接收到请求响应之后,就会执行complete方法。 响应如果遇到异常,那么我们就捕获异常并报错,如果没有,我们就正常走finally写回响应数据到前端。 至此,我们就能成功的将我们的请求转发到后端服务上。
配合负载均衡过滤器的效果
在前文我已经实现了随机和轮询两种负载均衡过滤器,这里我们来演示一下效果。 首先我们启动两个后端服务实例,并确保他们已经成功注册到了注册中心。
同时配置我们的gateway网关的配置文件。 之后,我们配置我们的请求头,设定要请求的后端服务的名称以及版本 多次发送服务,这里我们选择的是轮询负载均衡,可以看到服务最后均匀的分发到两个后端服务了。