gpt4 book ai didi

SpringCloud-微服务入门之Gateway+Nginx(4)

转载 作者:知者 更新时间:2024-03-13 11:38:03 32 4
gpt4 key购买 nike

确保你已经学完了SpringCloud-微服务入门之OpenFeign(3)

Gateway网关简介

Spring Cloud GateWay是Spring Cloud的⼀个全新项⽬,⽬标是取代Netflix Zuul,基于Spring5.0+SpringBoot2.0+WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发,性能⾼于Zuul,官⽅测试,GateWay是Zuul的1.6倍,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理⽅式
Gateway中文文档

为什么需要网关

API网关的出现的原因是微服务架构的出现,不同的微服务一般有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成完成一个业务需求,如果让客户端直接与各个微服务通信,会出现以下的问题。

  1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  2. 存在跨域请求,在一定场景下处理相对复制。
  3. 认证复杂,每个服务都需要独立的认证。
  4. 难以重构,随着项目的迭代。可能需要重新划分微服务。如果客户端与微服务直接通信,那么重构将会很复杂。
  5. 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难。

以上的问题可以借助API网关来解决。API网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过API网关这一层。也就是说,API网关可以完成安全、性能、监控等功能,而服务提供者可以专门的完成具体的业务逻辑。

使用API网关的优点

  1. 易于监控,可以在网关收集监控数据并将其推送到外部系统进行分析;
  2. 易于认证,可以在网关进行认证,然后再将请求转发到后端的微服务,而无需在每个微服务中进行认证;
  3. 减少客户端和各个微服务之间的交互次数。

核心三要素

项目结构图

上面这图config和controller,filter是可选的有需求的话在进行添加

搭建网关(基础)

添加依赖

<dependencies>
        <!--        添加公共模块-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
<!--        网关依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
<!--        注册中心依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

添加启动类

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

添加配置文件

server:
  port: 10010

# 设置  服务名 为gateway-service
spring:
  application:
    name: gateway
  profiles:
    active: common
  cloud:
    gateway:
      default-filters:
        # 去掉全部前缀api
        - RewritePath=/api(?<segment>/?.*),/$\{segment}
      routes:
        - id: consumer-route # 路由id,可以随意写不影响  一般都是以要代理的服务器名称-route
          uri: lb://consumer # 要访问的服务名称 (集群的时候可以防止宕机)
          #我们请求http://consumer/xxxx 会被拦截
          predicates: #允许的路由地址,要算上过滤器(filters)
            - Path=/api/user/**

之前我们访问消费者地址是: http://localhost:9001/user/getUserAll,那么现在我们有了网关之后的访问地址是http://localhost:10010/api/user/getUserAll

网关的其他配置

基本配置介绍

路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:

id:路由标识符,区别于其他 Route,必须唯一。
uri:路由指向的目的地 uri,即客户端请求最终被转发到的微服务。,如果是服务名称那么需要使用lb://服务名 如果是url那么直接写就行
order:用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
predicate:断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
filter:过滤器用于修改请求和响应信息。

执行顺序: uri>order>predicate>filter 只要filter后才会去请求实际的路径多个filter是从上到下依次执行的,而多个predicate必须同时都满足才行

Predicate 断言条件

在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。

说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。
转发规则(predicates),假设 转发uri都设定为http://localhost:9023

规则 实例 说明
Path - Path=/gate/*,/rule/* 当请求的路径为gate或rule开头的时,转发到http://localhost:9023服务器上
Before - Before=2017-01-20T17:42:47.789-07:00[Asia/Shanghai] 在某个时间之前的请求才会被转发到 http://localhost:9023服务器上
After - After=2017-01-20T17:42:47.789-07:00[Asia/Shanghai] 在某个时间之后的请求才会被转发
Between - Between=2017-01-20T17:42:47.789-07:00[Asia/Shanghai],2017-01-21T17:42:47.789-07:00[Asia/Shanghai] 在某个时间段之间的才会被转发
Cookie - Cookie=sessionId, test 可以接收两个参数,一个是 Cookie name ,一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。<br>1. 名为sessionId的而且满足正则test的才会被匹配到进行请求转发
Header - Header=X-Request-Id, \d+ 接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。<br>1. 携带参数X-Request-Id而且满足\d+的请求头才会匹配
Host - Host=www.hd123.com 1.当域名为www.hd123.com的时候直接转发到http://localhost:9023服务器上
Method - Method=GET 可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。<br>1.只有GET方法才会匹配转发请求,还可以限定
Query - Query=smile <br>- Query=keep, pu. 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。<br>1.只要请求中包含 smile 属性的参数即可匹配路由。<br>2.请求中包含 keep 属性并且参数值是以 pu 开头的长度为三位的字符串才会进行匹配和路由。
RemoteAddr - RemoteAddr=192.168.1.1/24 设置某个 ip 区间号段的请求才会路由,<br>1. 如果请求的远程地址是 192.168.1.10,则此路由将匹配。

组合案例:

routes:
        - id: gateway-service
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Host=**.foo.org
            - Path=/headers
            - Method=GET
            - Header=X-Request-Id, \d+
            - Query=foo, ba.
            - Query=baz
            - Cookie=chocolate, ch.p

各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。

多路由

spring:
  application:
    gateway:
      routes:
        - id: product-route
          uri: lb://product
          predicates:
              #  多个路由可以使用逗号分隔
            - Path=/product/**,/product1/**
          filters:
            - StripPrefix=1
        - id: order-route
          uri: lb://order
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1

一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发

过滤器规则

过滤规则 实例 说明
PrefixPath - PrefixPath=/app 在请求路径前加上app
StripPrefix - StripPrefix=1 (1,2,3,4) 在请求路径前前去掉第一个路径
RewritePath - RewritePath=/test, /app/test 访问localhost:9022/test,请求会转发到localhost:8001/app/test
SetPath - SetPath=/app/{path} 通过模板设置路径,转发的规则时会在路径前增加app,{path}表示原请求路径
RedirectTo - RedirectTo=302, https://acme.org 重定向到https://acme.org
RemoveRequestHeader 去掉某个请求头信息
RemoveResponseHeader 去掉某个响应头信息
RemoveRequestParameter 去掉某个请求参数信息

注:当配置多个filter时,优先定义的会被调用,剩余的filter将不会生效

PrefixPath

对所有的请求路径添加前缀:

routes:
      - id: prefixpath_route
        uri: https://example.org
        filters:
        - PrefixPath=/mypath

访问/hello的请求被发送到https://example.org/mypath/hello

StripPrefix

跳过指定路径。

routes:
        - id: consumer-route 
          uri: lb://consumer 
          predicates: 
            - Path=/api/consumer /**
          filters: # 添加请求路径的前缀
            - StripPrefix=1

访问的时候http://consumer/api/xxx/xxx
但是实际上会将你前面的/api给去掉http://consumer/xxx/xxx
1就是跳过一个路径,2就是跳过第二个路径…以此类推

RedirectTo

配置包含重定向的返回码和地址:

routes:
      - id: prefixpath_route
        uri: https://example.org
        filters:
        - RedirectTo=302, https://acme.org

当你请https://example.org那么会重定向跳转到https://acme.org

RemoveRequestHeader

去掉某个请求头信息:

routes:
      - id: removerequestheader_route
        uri: https://example.org
        filters:
        - RemoveRequestHeader=X-Request-Foo

去掉请求头信息 X-Request-Foo

RemoveResponseHeader

去掉某个回执头信息:

routes:
      - id: removerequestheader_route
        uri: https://example.org
        filters:
        - RemoveResponseHeader=X-Request-Foo
RemoveRequestParameter

去掉某个请求参数信息:

routes:
      - id: removerequestparameter_route
        uri: https://example.org
        filters:
        - RemoveRequestParameter=red
RewritePath

改写路径:

routes:
        - id: consumer-route 
          uri: lb://consumer 
          predicates: #允许的路由地址,要算上过滤器(filters)
            - Path=/api/user/**
          filters: # 添加请求路径的前缀
            - RewritePath=/api/user(?<segment>/?.*),/user$\{segment}

对于请求路径/api/user,当前的配置在请求到到达前会被重写为/user,由于YAML的语法问题,$符号后⾯应该加上\在解释正则表达式前,
(?<segment>/?.*)表达式将获取后面的路径和参数并且放入到segment里,那么我们在使用的地方通过$\{segment}就能获取到了

SetPath

设置请求路径,与RewritePath类似。

routes:
      - id: setpath_route
        uri: https://example.org
        predicates:
        - Path=/red/{segment}
        filters:
        - SetPath=/{segment}

获取/red/*请求时候的后面参数,保存到segment中,然后我们实际请求的是/{segment}
假设:我们请求/red/user/get那么实际请求/user/get

SetRequestHeader

设置请求头信息。key=value

routes:
      - id: setrequestheader_route
        uri: https://example.org
        filters:
        - SetRequestHeader=X-Request-Red, Blue
SetStatus

设置回执状态码。,也就是响应成功后返回的状态码

routes:
      - id: setstatusint_route
        uri: https://example.org
        filters:
        - SetStatus=401
RequestSize

请求大小。

routes:
      - id: request_size_route
        uri: http://localhost:8080/upload
        predicates:
        - Path=/upload
        filters:
        - name: RequestSize
          args:
            maxSize: 5000000

超过5M的请求会返回413错误。

RequestRateLimiter

请求限流:

routes:
      - id: requestratelimiter_route
        uri: http://example.org
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10 # 每秒执行的请求数
            redis-rate-limiter.burstCapacity: 20 # 每秒最大请求数

replenishRate超过了那么其他请求会被缓存起来,如果超过了burstCapacity那么多余的请求直接就请求失败HTTP 429-请求太多

需要依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
Default-filters

对所有请求添加过滤器。支持以上所有过滤器的写法

spring:
  cloud:
    gateway:
      default-filters:
      - AddResponseHeader=X-Response-Default-Red, Default-Blue
      - PrefixPath=/httpbin

通过代码进行配置路由

此方式是不受yml方式影响的,相互之间是隔离的,也就是yml里的配置不会影响到Bean的配置,但是都会起作用,谁满足条件那么就执行谁的

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {

        return builder.routes()
                .route("consumer-route",
                         r->r.path("/api/user/**")
                                 .filters(f->f.rewritePath("/api(?<segment>/?.*)","/${segment}"))
                                 .uri("lb://consumer"))
                .build();
    }
}
@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
			.route("path_route", r -> r.path("/get")
				.uri("http://httpbin.org"))
				
			.route("host_route", r -> r.host("*.myhost.org")
				.uri("http://httpbin.org"))
				
			.route("rewrite_route", r -> r.host("*.rewrite.org")
				.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
				.uri("http://httpbin.org"))
				
			.route("hystrix_route", r -> r.host("*.hystrix.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
				.uri("http://httpbin.org"))
				
			.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
				.uri("http://httpbin.org"))
				
			.route("limit_route", r -> r
				.host("*.limited.org").and().path("/anything/**")
				.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
				.uri("http://httpbin.org"))
			.build();
	}

局部自定义过滤器

自定义过滤器的命名后缀应该为:***GatewayFilterFactory 这是必须的 否则就会找不到你定义的类,然在配置文件中filters:下面添加 - 前缀=参数

routes:
        # 路由id,可以随意写不影响  一般都是以要代理的服务器名称-route
        - id: product-service-route
          uri: lb://product-service # 要代理的服务器名称 (集群的时候可以防止宕机)
          #我们请求http://127.0.0.1:10010/xxxx 会被拦截 如果路径包含指定的路径 那么就执行对应的路由地址
          predicates: #允许的路由地址
            - Path=/product/user1/**
          filters:
            - # 添加自定义局部过滤器,MyParam就是前缀,name,age就是参数
            - MyParam=name,age

创建局部过滤器

import lombok.Data;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

//MyParam就是我们在配置文件中的前缀
@Component
public class MyParamGatewayFilterFactory extends AbstractGatewayFilterFactory<MyParamGatewayFilterFactory.Config> {

    @Data
    public static class Config {
        private String param1;
        private String param2;
    }

    public MyParamGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        //需要添加的参数成员变量名称
        return Arrays.asList("param1","param2");
    }
    @Override
    public GatewayFilter apply(Config config) {

        GatewayFilter gatewayFilter = new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                /**
                 * 拦截所有请求,如果参数中包含name,把参数打印到控制台
                 */
                //获取请求对象
                ServerHttpRequest request = exchange.getRequest();
                //判断路径中key是否包含 ,如果包含那么就拦截
                if (request.getQueryParams().containsKey(config.param1)&&request.getQueryParams().containsKey(config.param2)) {
                    //返回一个405状态 请求的方法不允许
                    exchange.getResponse().setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
                    return exchange.getResponse().setComplete();
                }
                return chain.filter(exchange);//放行所有请求
            }
        };
        return gatewayFilter;
    }

}

自定义全局过滤器

全局过滤是在局部过滤之前执行的,作用全部的路由,一般用作权限效验等操作,无须其他配置直接继承GlobalFilter接口实现后就自动生效

import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("-----------------全局过滤器MyGlobalFilter------------------- --");
        // 获取路径参数中指定的key对应的value值
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        //判断token是否为空的
        if (StringUtils.isBlank(token)) {
             //返回一个401状态 没有权限
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        //放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        //值越小越先执行
        return 1;
    }
}

网关跨域

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allow-credentials: true #允许携带认证信息Cookie
            allowed-origins: "*"   # 允许所有请求
            allowed-headers: "*"   #允许所有请求头
            allowed-methods:  #允许指定的请求方式
              - OPTIONS
              - GET
              - POST
            max-age: 86400  # 86400 秒,也就是 24 小时 在有效时间内,浏览器无须为同一请求再次发起预检请求,可以减少发送请求的次数,减少系统部分压力。

还可以使用代码方式进行跨域

// 解决网关跨域问题
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

实现负载均衡器

在gateway中已经默认集成了负载均衡相关的依赖,相关配置我们在之前common模块中已经配置完毕了

那么我们在路由的时候需要指定服务名称才行

routes:
      - id: consumer-route
        uri: lb://consumer  #想要负载必须指定lb://服务名称

创建2个相同的consumer(消费者),都启动起来
consumer: 9001 , consumer1:9002 , consumer2:9003

然后进行访问,就会发现是走负载的

实现熔断降级

为什么要实现熔断降级?

在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。

为什么在网关上请求失败需要快速返回给客户端?

因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应,所以需要网关上请求失败需要快速返回给客户端。

添加网关的依赖

<!--        熔断器-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>

全局路径默认熔断器配置方式

cloud:
    gateway:
      default-filters:
        # 去掉全部前缀api
        - RewritePath=/api(?<segment>/?.*),/$\{segment}
        - name: Hystrix  # 网关名称(随意只要不重复就行)
          args:
            name: fallbackHystrix  # 降级名称和方法名称一样就行了
            fallbackUri: forward:/fallbackHystrix  # 当发生降级后,降级回调的controller
@RestController
public class FallbackHystrixController {

    @GetMapping("/fallbackHystrix")
    public ResponseEntity fallbackHystrix() {

        System.out.println("发送邮件给管理员,进行处理");

        //503
        ResponseEntity response = new ResponseEntity(HttpStatus.SERVICE_UNAVAILABLE);
        return response;
    }
}

指定路由配置熔断器方式
这样的好处就是能知道是哪里出现了问题

routes:
        - id: consumer-route # 路由id,可以随意写不影响  一般都是以要代理的服务器名称-route
          uri: lb://consumer # 要访问的服务名称 (集群的时候可以防止宕机)
          #我们请求http://consumer/xxxx 会被拦截
          predicates: #允许的路由地址,要算上过滤器(filters)
            - Path=/api/user/**
          filters:
            - name: Hystrix  # 网关(固定)
              args:
                name: consumerFallbackHystrix  # 降级名称
                fallbackUri: forward:/consumerFallbackHystrix  # 当发生降级后,降级回调的controller
@RestController
public class FallbackController {

    @GetMapping("/consumerFallbackHystrix")
    public ResponseEntity consumerFallbackHystrix() {
        System.out.println(" consumer服务出问题了,发送邮件给管理员,进行处理");
        //503
        ResponseEntity response = new ResponseEntity(HttpStatus.SERVICE_UNAVAILABLE);
        return response;
    }
}

Gateway+Nginx进行高可用

基本上微服务到Gateway这一层,就无法在通过Ribbon进行负载了,因为这里已经是用户访问的最开始的入口了那么如果大量的请求都去访问一台Gateway肯定是扛不住的,比如双十一这种大量并发请求的场景世界上是没有这么高性能的单机服务的,解决办法通过负载均衡器Nginx进行代理,他是安装在服务器上的,来看下面一张部署图:

我么这里就不详细的介绍nginx具体的使用了,因为篇幅有点长,我们会专门拿一篇文章来讲解nginx和高可用部署的,下面就简单的使用Nginx进行负载多台Gateway,我们在创建2台Gateway

gateway:10010 ,gateway:10011 ,gateway:10012

我们可以自行安装Windows的Nginx然后在nginx.conf中进行配置

upstream gateway-server {
      server 127.0.0.1:10010;
      server 127.0.0.1:10011;
      server 127.0.0.1:10012;
    }
	 server {
		listen       20202;
		server_name  localhost;
		  location / {
			proxy_pass http://gateway-server/;
		  }
	 }

然后重启nginx,进行访问http://localhost:20202/api/user/getUserAll

注意事项:

  1. 配置前需要检查listen的端口是否被占了,不然是访问不通的
  2. 从新加载配置文件nginx -s reload

小提示: 以上架构就没问题了吗? 当然不是,可以看到Nginx现在是瓶颈了,而Nginx本身并没有能力支持集群,最多只能做主从,那么如何解决呢?
我所知道的就3种:

  1. 网络层进行负载(DNS)
  2. 硬件(F5等)来进行负载 ,一些大厂用F5的还是比较多的而小厂一般都用不起太贵了
  3. 第三方平台进行流量代理转发(收费不高,小企业是能接受的)

我推荐几个大厂的负载均衡器价格还是能接受的

阿里云SLB

腾讯云CLB

华为云ELB

点赞 -收藏-关注-便于以后复习和收到最新内容有其他问题在评论区讨论-或者私信我-收到会在第一时间回复如有侵权,请私信联系我感谢,配合,希望我的努力对你有帮助^_^

32 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com