springcloud 入门 之网关 springcloud gateway
文章目录
项目版本
1、jdk:1.8
2、springboot 2.1.6.RELEASE ,springcloud Greenwich.SR6
前言
网关提供 API 全托管服务,丰富的 API 管理功能,辅助企业管理大规模的 API ,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略( WAF )、防刷、流量、监控日志等功能。一般来说,网关对外暴露的 URL 或者接口信息,我们统称为路由信息。网关的核心是Filter 和 Filter Chain(过滤器链)
Spring Cloud Gateway 是什么
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,旨在取代Zuul网关。关于zuul可查看springcloud 入门(6) 网关 zuul
Spring Cloud Gateway是Spring Cloud官方基于Spring 5、Spring Boot 2 和 Project Reactor等技术开发的网关,旨在提供一种简单而有效的方式来路由到 API 并为它们提供交叉关注点,例如:安全性、监控/指标和弹性。
术语
路由(Route):网关的基本构建块。 它由 ID、目标 URI、断言集合和过滤器集合定义。 如果断言为真,则路由匹配成功。
断言(Predicate):Java 8 断言函数。 输入类型是 Spring 框架的ServerWebExchange。 可以匹配来自 HTTP 请求的任何内容,例如请求头或参数。
过滤器(Filter):使用特定工厂构建的 GatewayFilter 实例。 通过Filter可以在发送下游请求之前或之后修改请求和响应。
Spring Cloud Gateway 工作流程
流程图,来自官网
客户端向 Spring Cloud Gateway 发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关 Web Handler处理程序。此处理程序通过特定于请求的过滤器链运行请求。过滤器被虚线分隔的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有“pre”类型过滤器逻辑,然后进行代理请求,发出代理请求后,将运行“post”类型过滤器逻辑。
入门示例
入门小样
**注意: 在没有端口的路由中 ,HTTP 和 HTTPS 默认的端口分别是80 和 443。 Spring Cloud Gateway 启动容器目前只支持netty
**
1、创建cloud-gateway springboot项目,引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2、修改配置文件
添加日志和监控
server.port=7201
spring.application.name=CLOUD-GATEWAY
# 日志配置
logging.level.org.springframework.cloud.gateway=trace
logging.level.org.springframework.http.server.reactive=debug
logging.level.org.springframework.web.reactive=debug
logging.level.reactor.netty=debug
# actuator 监控 http://localhost:7201/actuator/gateway/routes
management.endpoints.web.exposure.include=*
3、配置路由信息
配置路由信息有两种方式:
3.1 通过配置类
@Configuration
public class RouteLocatorConfig {
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder locatorBuilder){
final RouteLocatorBuilder.Builder routes = locatorBuilder.routes();
routes.route(baidu-> baidu.path("/qq_39654841").uri("http://blog.csdn.net/").id("my_csdn_route"));
return routes.build();
}
}
3.2 在配置文件中配置
# 路由配置
spring.cloud.gateway.routes[0].id=my_csdn_route
spring.cloud.gateway.routes[0].uri=http://blog.csdn.net/
spring.cloud.gateway.routes[0].predicates[0]=Path=/qq_39654841
4、测试
不管选择哪种方式,访问http://localhost:7201/qq_39654841,会跳到https://blog.csdn.net/qq_39654841页面
前面引入了actuator,通过访问http://localhost:7201/actuator/gateway/routes可以看到所有路由信息
路由发现
Spring Cloud Gateway 路由发现规则在不同的注册中心也有所不同:
- 如果把Gateway 注册到Eureka 上,通过网关转发服务调用,访问网关的URL是http://Gateway_HOST:Gateway_port/大写serviceld /*,其中服务名默认必须是大写,否则会抛404错误,如果服务名要用小写访问,可以在属性配置文件里面加spring.cloud .gateway.discovery .locator.lowerCaseServiceld =true配置解决。
- 如果把Gateway 注册到Zookeeper 上,通过网关转发服务调用,服务名默认小写,因此不需要做任何处理。
- 如果把Gateway 注册到Consul 上,通过网关转发服务调用,服务名默认小写,也不需要做人为修改。
路由发现示例
示例使用eureka作为注册中心,有关eureka可以查看 springcloud 入门(1) eureka注册中心
1、在cloud-gateway引入eureka依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、修改cloud-gateway配置文件
添加eureka、日志配置
# 此客户端是否应该从eureka server 获取eureka注册信息
eureka.client.register-with-eureka=true
eureka.instance.instance-id=cloud-gateway
# 和eureka服务器通讯的URL
eureka.client.service-url.defaultZone=http://eurekaUser:eurekaUserPassword@localhost:8001/eureka
# 在地址栏上使用 IP 地址进行显示
eureka.instance.prefer-ip-address=true
# 设置心跳的时间间隔(默认是30秒)
eureka.instance.lease-renewal-interval-in-seconds=15
# eureka server 最后一次收到心跳时等待的时间,超时将会移除client(默认是90秒)
eureka.instance.lease-expiration-duration-in-seconds=90
# 日志配置
logging.level.org.springframework.cloud.gateway=debug
logging.level.org.springframework.http.server.reactive=debug
logging.level.org.springframework.web.reactive=debug
logging.level.reactor.netty=debug
# 是否与服务发现组件相结合,通过serviceId 转发到具体的实例
spring.cloud.gateway.discovery.locator.enabled=true
# 注册中心为eureka时,设置为true表示开启用小写的serviceId 进行服务路由的转发
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
eureka-server做了安全处理,可参考springcloud 入门(10) Spring Security 安全与权限 ,没有做处理的可以不用账号密码。
3、eureka-consumer在之前的基础上添加测试方法
@RequestMapping("/consumer/feign")
@RestController
public class FeignController {
@GetMapping("/gateway/request")
public String getRequest(HttpServletRequest request){
final String header = request.getHeader("request-arg");
System.out.println(header);
return "request";
}
}
4、测试
分别启动eureka-server、eureka-consumer、cloud-gateway
访问 http://localhost:7201/eureka-consumers/consumer/feign/gateway/request,出现下面页面成功调用eureka-consumer。
当Spring Cloud Gateway 注册到eureka时,会自动发现服务并生成默认的路由规则
可以通过cloud-gateway控制台发现
或者访问http://localhost:7201/actuator/gateway/routes
{
"route_id": "CompositeDiscoveryClient_EUREKA-CONSUMERS",
"route_definition": {
"id": "CompositeDiscoveryClient_EUREKA-CONSUMERS",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/eureka-consumers/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/eureka-consumers/(?<remaining>.*)",
"replacement": "/${remaining}"
}
}
],
"uri": "lb://EUREKA-CONSUMERS",
"order": 0
},
"order": 0
},
如果把spring.cloud.gateway.discovery.locator.lower-case-service-id改成false
# 注册中心为eureka时,设置为true表示开启用小写的serviceId 进行服务路由的转发
spring.cloud.gateway.discovery.locator.lower-case-service-id=false
路由变成了大写的
{
"route_id": "CompositeDiscoveryClient_EUREKA-CONSUMERS",
"route_definition": {
"id": "CompositeDiscoveryClient_EUREKA-CONSUMERS",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/EUREKA-CONSUMERS/**"
}
}
],
"filters": [
{
"name": "RewritePath",
"args": {
"regexp": "/EUREKA-CONSUMERS/(?<remaining>.*)",
"replacement": "/${remaining}"
}
}
],
"uri": "lb://EUREKA-CONSUMERS",
"order": 0
},
"order": 0
},
再访问 http://localhost:7201/eureka-consumers/consumer/feign/gateway/request,就会出现404页面
断言工厂
Spring Cloud Gateway 的路由功能是在 Spring WebFlux HandlerMapping 基础上实现的,Spring Cloud Gateway由许多个路由断言工厂组成。当请求进入Spring Cloud Gateway时,网关中的路由断言工厂会根据规则进行路由匹配,匹配成功则进行下一步工作,否则返回错误信息。
After断言工厂
AfterRoutePredicateFactory会取一个UTC格式的时间参数,请求进来的当前时间是在配置的UTC时间之后则请求成功,否则失败。
通过以下代码生成UTC格式的时间:
final String utcFormat = ZonedDateTime.now().plusHours(1).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
System.out.println(utcFormat);
1、在之前的基础上修改配置文件:
# 路由配置
spring.cloud.gateway.routes[0].id=my_csdn_route
spring.cloud.gateway.routes[0].uri=http://blog.csdn.net/
spring.cloud.gateway.routes[0].predicates[0]=Path=/qq_39654841
#after 路由
spring.cloud.gateway.routes[1].id=after_route
spring.cloud.gateway.routes[1].uri=http://blog.csdn.net/qq_39654841/category_9123089.html
spring.cloud.gateway.routes[1].predicates[0]=After=2021-12-27T13:50:12.191+08:00[Asia/Shanghai]
或者在配置类中:
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder locatorBuilder){
final RouteLocatorBuilder.Builder routes = locatorBuilder.routes();
//比当前时间早一个小时
ZonedDateTime localDateTimeAfter = LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault());
System.out.println(localDateTimeAfter);
//配置文件UTC时间
String format = ZonedDateTime.now().minusHours(1).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
routes
.route(baidu-> baidu.path("/qq_39654841").uri("http://blog.csdn.net/").id("my_csdn_route"))
.route(afterRoute ->afterRoute.after(localDateTimeAfter).uri("http://blog.csdn.net/qq_39654841/category_9123089.html").id("after_route"))
;
return routes.build();
}
2、测试
访问http://localhost:7201/,进入CSDN页面。
如果生成一个晚于当前的时间,访问http://localhost:7201/,会出现下面路由匹配失败页面
Before 断言工厂
BeforeRoutePredicateFactory会取一个UTC格式的时间参数,请求进来的当前时间是在配置的UTC时间之前则请求成功,否则失败。
配置文件:
spring.cloud.gateway.routes[1].predicates[0]=Before=2021-12-27T23:50:12.191+08:00[Asia/Shanghai]
Between断言工厂
BetweenRoutePredicateFactory会取一个UTC格式的时间参数,请求进来的当前时间是在配置的UTC时间之间则请求成功,否则失败。
配置文件:
spring.cloud.gateway.routes[1].predicates[0]=Between=2021-12-27T20:50:12.191+08:00[Asia/Shanghai],2021-12-27T23:50:12.191+08:00[Asia/Shanghai]
Cookie 断言工厂
CookieRoutePredicateFactory 会取两个参数,cookie的键和值,当请求中携带的key和value,其中value可以为正则表达式,跟CookieRoutePredicateFactory配置的相同时,则匹配成功。
配置文件:
#Cookie
spring.cloud.gateway.routes[1].predicates[0]=Cookie=cloud,gateway
测试:
在postman中添加请求,
点击send,会看到访问的页面,输入一个错误的cookie会返回404信息。
Header断言工厂
Header Route Predicate Factory 会根据请求头进行匹配,匹配成功转发到下一步,否则不转发。
Header Route Predicate Factory 需要两个参数,header的key和value,value可以为正则表达式。
spring.cloud.gateway.routes[1].predicates[0]=Header=X-Requested-Id,\\d+
当请求的Header中有X-Request-Id的header名,且header值为数字时,请求会被路由到配置的 uri
否则,出现404
Host 断言工厂
HostRoutePredicateFactory 需要一个参数即hostname,它可以使用. * 等去匹配host。这个参数会匹配请求头中的host的值,一致,则请求正确转发
#Host
spring.cloud.gateway.routes[1].predicates[0]=Host=**.sunlong.site
Method 断言工厂
Method Route Predicate Factory 需要一个参数,即请求的类型。比如GET类型的请求都转发到此路由。
#Method
spring.cloud.gateway.routes[1].predicates[0]=Method=GET
Path 断言工厂
Path Route Predicate Factory 需要一个参数: 一个spel表达式,应用匹配路径。
上面的示例用的就是Path 断言工厂。
spring.cloud.gateway.routes[0].predicates[0]=Path=/qq_39654841/{segment}
请求路径匹配到/qq_39654841/{segment}就能转发成功。
Query 断言工厂
Query Route Predicate Factory 需要2个参数:一个参数名和一个参数值的正则表达式。
当请求参数中有配置的参数名并且对应的value能匹配配置的参数值路由转发成功,
#Query
spring.cloud.gateway.routes[1].predicates[0]=Query=cloud,gateway
当请求参数中有cloud字段,并且值是gateway时匹配成功。
Query 断言工厂也可以只填一个参数,当只填一个参数是,只要请求中有这个参数就能匹配成功。
RemoteAddr 断言工厂
RemoteAddrRoutePredicateFactory 会配置一个IPv4或IPv6网段的字符串或ip。当请求的IP地址在网段内或者和配置的ip地址相同则匹配成功。例如 192.168.0.1/16(其中 192.168.0.1 是 IP 地址,16 是子网掩码)。
#RemoteAddr
spring.cloud.gateway.routes[1].predicates[0]=RemoteAddr=127.0.0.1
Weight 断言工厂
Weight Route Predicate Factory需要两个参数:group 和 weight(int类型)。 权重是按组计算的。
spring.cloud.gateway.routes[0].id=weight_high
spring.cloud.gateway.routes[0].uri=https://weighthigh.org
spring.cloud.gateway.routes[0].predicates[0]=Weight=group1, 8
spring.cloud.gateway.routes[1].id=weight_low
spring.cloud.gateway.routes[1].uri=https://weightlow.org
spring.cloud.gateway.routes[1].predicates[0]=Weight=group1, 2
该路由会将约 80% 的流量转发到 weighthigh.org,将约 20% 的流量转发到 weightlow.org
Spring Cloud Gateway 的内置 Filter
Spring Cloud Gatewat 中内置很多的路由过滤工厂,当然可以自己根据实际应用场景需要定制的自己的路由过滤器工厂。路由过滤器允许以某种方式修改请求进来的 http 请求或返回的 http 响应。路由过滤器主要作用于需要处理的特定路由。 Spring Cloud Gateway 提供了很多种类的过滤器工厂,过滤器的实现类将近二十多个。总得来说,可以分为七类: Header 、 Parameter 、 Path 、 Status 、 Redirect 跳转、 Hytrix 熔断和 RateLimiter 。
AddRequestHeaderGatewayFilterFactory
AddRequestHeaderGatewayFilterFactory作用是给匹配上的请求添加header。
测试:
1、给cloud-gateway添加配置
# eureka_consumers
spring.cloud.gateway.routes[1].id=eureka_consumers_route
spring.cloud.gateway.routes[1].uri=http://localhost:7001
spring.cloud.gateway.routes[1].predicates[0]=Path=/consumer/feign/gateway/request
spring.cloud.gateway.routes[1].filters[0]=AddRequestHeader=request-arg,Header
2、eureka-consumer消费端
@RequestMapping("/consumer/feign")
@RestController
public class FeignController {
@GetMapping("/gateway/request")
public String getRequest(HttpServletRequest request){
final String header = request.getHeader("request-arg");
System.out.println(header);
return "request";
}
}
3、访问http://localhost:7201/consumer/feign/gateway/request
查看eureka-consumer控制台
Header
AddRequestParameterGatewayFilterFactory
AddRequestParameterGatewayFilterFactory作用是给匹配上的请求路由添加参数。
1、给cloud-gateway添加配置
spring.cloud.gateway.routes[1].filters[1]=AddRequestParameter=request-parameter,addRequestParameter
2、eureka-consumer消费端
@GetMapping("/gateway/request")
public String getRequest(HttpServletRequest request){
final String header = request.getHeader("request-arg");
System.out.println("AddRequestHeader="+header);
final String requestParameter = request.getParameter("request-parameter");
System.out.println("AddRequestParameter="+requestParameter);
return "request";
}
3、访问http://localhost:7201/consumer/feign/gateway/request
查看eureka-consumer控制台
AddRequestHeader=Header
AddRequestParameter=addRequestParameter
AddResponseHeaderGatewayFilterFactory
AddResponseHeaderGatewayFilterFactory作用是从网关返回的数据添加header。
1、给cloud-gateway添加配置
spring.cloud.gateway.routes[1].filters[2]=AddResponseHeader=add-response-header,addResponseHeader
2、访问http://localhost:7201/consumer/feign/gateway/request
通过浏览器network查看
StripPrefixGatewayFilterFactory
StripPrefixGatewayFilterFactory 是针对请求url进行处理的,作用是去除url前缀。
PrefixPathGatewayFilterFactory 和 StripPrefixGatewayFilterFactory 相反,作用是增加url前缀。
1、给cloud-gateway添加配置
spring.cloud.gateway.routes[1].filters[3]=StripPrefix=2
2、访问http://localhost:7201/consumer/feign/gateway/request,会发现出现404
再看控制台
Handler is being applied: {uri=http://localhost:7001/gateway/request?request-parameter=addRequestParameter, method=GET}
StripPrefixGatewayFilterFactory 是把端口号后面的前两个前缀给去掉了,所以访问就出现404了,
把路径修改为
spring.cloud.gateway.routes[1].predicates[0]=Path=/a/b/consumer/feign/gateway/request
访问 http://localhost:7201/a/b/consumer/feign/gateway/request就能成功了。
RewritePathGatewayFilterFactory
RewritePathGatewayFilterFactory 作用是重写路径。
1、给cloud-gateway添加配置
spring.cloud.gateway.routes[1].predicates[0]=Path=/a/b/consumer/feign/gateway/rewritePath/request/
spring.cloud.gateway.routes[1].filters[4]=RewritePath=/rewritePath/?(?<segment>.*), /$\\{segment}
2、访问http://localhost:7201/a/b/consumer/feign/gateway/rewritePath/request/,访问成功
查看控制台
Handler is being applied: {uri=http://localhost:7001/consumer/feign/gateway/request/?request-parameter=addRequestParameter, method=GET}
上面的配置是把http://localhost:7201/a/b/consumer/feign/gateway/rewritePath/request/中的rewritePath重写为{segment},然后再进行转发,这里相当于把/rewritePath前缀去掉
RetryGatewayFilterFactory
Retry GatewayFilter工厂支持以下参数:
- retries:尝试的重试次数。
- statuses: 重试的 HTTP 状态码,可选值用 org.springframework.http.HttpStatus 。
- methods: 重试的 HTTP 方法,可选值用org.springframework.http.HttpMethod.
- series:要重试的一系列状态码,可选值用org.springframework.http.HttpStatus.Series。
- exceptions: 重试的抛出异常列表。
- backoff:为重试配置的回退指数。 在 firstBackoff * (factor ^ n) 的回退间隔后执行重试,其中 n 是迭代。 如果配置了 maxBackoff,则应用的最大退避限制为 maxBackoff。 如果 basedOnPreviousValue 为真,则使用 prevBackoff * factor计算回退。
Retry如果启用,则为过滤器配置以下默认值:
- retries: 3次
- series: 5XX 系列状态码
- methods: GET方法
- exceptions:IOException和TimeoutException
- backoff: disabled
在配置文件中有两种书写方式:
1、每一个参数都列出来
# Retry 过滤器
spring.cloud.gateway.routes[1].filters[5].name=Retry
spring.cloud.gateway.routes[1].filters[5].args.retries=3
spring.cloud.gateway.routes[1].filters[5].args.statuses=BAD_GATEWAY
spring.cloud.gateway.routes[1].filters[5].args.methods=GET,POST
spring.cloud.gateway.routes[1].filters[5].args.backoff.firstBackoff=10ms
spring.cloud.gateway.routes[1].filters[5].args.backoff.maxBackoff=50ms
spring.cloud.gateway.routes[1].filters[5].args.backoff.factor=2
spring.cloud.gateway.routes[1].filters[5].args.backoff.basedOnPreviousValue=false
2、快捷方式把参数用逗号隔开
spring.cloud.gateway.routes[1].filters[5]=Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false
修改eureka-consumer#FeignController的getRequest方法
@RequestMapping("/consumer/feign")
@RestController
public class FeignController {
private final static Map<String, AtomicInteger> countMap = new ConcurrentHashMap<>();
@GetMapping("/gateway/request")
public String getRequest(HttpServletRequest request){
final String header = request.getHeader("request-arg");
System.out.println("AddRequestHeader="+header);
final String requestParameter = request.getParameter("request-parameter");
System.out.println("AddRequestParameter="+requestParameter);
// 重试
final AtomicInteger atomicInteger = countMap.computeIfAbsent(requestParameter, v -> new AtomicInteger());
final int i = atomicInteger.incrementAndGet();
System.out.println("重试次数:"+i);
if (i < 3){
throw new RuntimeException("try again.");
}
return "request";
}
}
ConcurrentHashMap作为计数器
使用AddRequestParameterGatewayFilterFactory修改的参数作为ConcurrentHashMap的key,AtomicInteger作为value。
3、测试
访问http://localhost:7201/a/b/consumer/feign/gateway/rewritePath/request
查看控制台
AddRequestHeader=Header
AddRequestParameter=addRequestParameter
重试次数:3
三次之内每次访问都会报错
java.lang.RuntimeException: try again.
at site.sunlong.eurekaConsumer.controller.FeignController.getRequest(FeignController.java:77) ~[classes/:na]
at site.sunlong.eurekaConsumer.controller.FeignController$$FastClassBySpringCGLIB$$1.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.8.RELEASE.jar:5.1.8.RELEASE]
HystrixGatewayFilterFactory
当通过Spring Cloud Gateway调用后端服务时,也可能会出现服务异常、服务不可用的情况,就需要对服务进行降级处理,返回友好提示,提高用户体验。Spring Cloud Gateway内置了Hystrix过滤器。
想了解Hystrix的可移步到 springcloud 入门(4) Hystrix
1、cloud-gateway中新建一个FallbackController
@RestController
public class FallbackController {
@GetMapping("eureka_consumer/fallback")
public String fallback(){
return "Spring Cloud Gateway fallback.";
}
}
2、cloud-gateway配置文件
# Hystrix 过滤器
spring.cloud.gateway.routes[1].filters[6].name=Hystrix
# HystrixCommand 名字
spring.cloud.gateway.routes[1].filters[6].args.name=fallbackcmd
spring.cloud.gateway.routes[1].filters[6].args.fallbackUri=forward:/eureka_consumer/fallback
#fackball 时间
hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds=5000
3、修改eureka-consumer#FeignController的getRequest方法
让线程休眠20s
@GetMapping("/gateway/request")
public String getRequest(HttpServletRequest request) throws InterruptedException {
final String header = request.getHeader("request-arg");
System.out.println("AddRequestHeader="+header);
final String requestParameter = request.getParameter("request-parameter");
System.out.println("AddRequestParameter="+requestParameter);
//休眠20s
TimeUnit.SECONDS.sleep(20);
// 重试
final AtomicInteger atomicInteger = countMap.computeIfAbsent(requestParameter, v -> new AtomicInteger());
final int i = atomicInteger.incrementAndGet();
System.out.println("重试次数:"+i);
if (i < 3){
throw new RuntimeException("try again.");
}
return "request";
}
4、测试
访问http://localhost:7201/a/b/consumer/feign/gateway/rewritePath/request,出现以下页面HystrixGatewayFilter使用成功。
Spring Cloud Gateway 入门到此就结束了。
学习更多关于springcloud的可以关注我的 springcloud 专栏
GitHub地址:
https://github.com/ArronSun/micro-services-practice.git
参考:
《重新定义springcloud 实战》
Spring Cloud Gateway 官网
能力一般,水平有限,如有错误,请多指出。
如果对你有用 点个关注给个赞呗