Ocelot简易教程(三)之主要特性及路由详解

做者:依乐祝
原文地址:http://www.javashuo.com/article/p-kejgcpxj-dv.htmlhtml

上篇《Ocelot简易教程(二)之快速开始2》教你们如何快速跑起来一个ocelot实例项目,也只是简单的对Ocelot进行了配置,这篇文章会给你们详细的介绍一下Ocelot的配置信息。但愿能对你们深刻使用Ocelot有所帮助。git

上篇中也提到了,最简单的Ocelot以下面所示,只有简单的两个节点,一个是ReRoutes,另外一个就是GlobalConfiguration关于这两个节点的做用,上篇也已经讲述了,这里再简单的讲下ReRoutes:告诉Ocelot如何处理上游的请求。GlobalConfiguration:顾名思义就是全局配置,此节点的配置容许覆盖ReRoutes里面的配置,你能够在这里进行通用的一些配置信息。github

{
    "ReRoutes": [],
    "GlobalConfiguration": {}
}

下面呢给出ReRoute 的全部的配置信息,固然在实际使用的时候你没有必要所有进行配置,只须要根据你项目的实际须要进行相关的配置就能够了。数据库

"ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",//下游路由模板
      "UpstreamPathTemplate": "/good/{everything}",//上游路由模板
      "UpstreamHttpMethod": [ "Get", "Post" ],//上游请求方法
      "AddHeadersToRequest": {},
      "UpstreamHeaderTransform": {},
      "DownstreamHeaderTransform": {},
      "AddClaimsToRequest": {},
      "RouteClaimsRequirement": {},
      "AddQueriesToRequest": {},
      "RequestIdKey": null,
      "FileCacheOptions": {
        "TtlSeconds": 0,
        "Region": null
      },
      "ReRouteIsCaseSensitive": false,
      "ServiceName": null,
      "DownstreamScheme": "http",
      "QoSOptions": {//Qos相关配置
        "ExceptionsAllowedBeforeBreaking": 0,
        "DurationOfBreak": 0,
        "TimeoutValue": 0
      },
      "LoadBalancerOptions": {//负载均衡相关选项
        "Type": "RoundRobin",
        "Key": null,
        "Expiry": 0
      },
      "RateLimitOptions": {//限流相关配置
        "ClientWhitelist": [],
        "EnableRateLimiting": false,
        "Period": null,
        "PeriodTimespan": 0.0,
        "Limit": 0
      },
      "AuthenticationOptions": {//认证相关选项
        "AuthenticationProviderKey": null,
        "AllowedScopes": []
      },
      "HttpHandlerOptions": {//HttpHandler相关的配置
        "AllowAutoRedirect": false,//是否对下游重定向进行响应
        "UseCookieContainer": false,//是否启动CookieContainer储存cookies
        "UseTracing": false,
        "UseProxy": true
      },
      "DownstreamHostAndPorts": [//下游端口及host
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamHost": null,//上游Host
      "Key": null,
      "DelegatingHandlers": [],
      "Priority": 1,
      "Timeout": 0,
      "DangerousAcceptAnyServerCertificateValidator": false
    }

固然上面的配置项我就不一一的进行介绍,由于不少配置相信你们根据意思都能知道个大概了。我只会对比较经常使用的配置作下介绍。并且在接下来的文章中对对每一个节点进行单独的详细的介绍。在介绍以前呢先看Ocelot的几个特性。json

Ocelot特性介绍

合并配置文件

这个特性容许用户建立多个配置文件来方便的对大型项目进行配置。试想一下,若是你的项目有几十个路由规则须要配置的话,那么在一个配置文件进行配置应该很痛苦吧,有了这个特性后,你就能够建立多个配置文件。Ocelot会自动合并他们。
在加载配置文件的时候 你能够经过下面的方式来调用AddOcelot()方法来替换直接加载某个配置的写法 如:AddJsonFile(“ocelot.json”)c#

.ConfigureAppConfiguration((hostingContext, config) =>
    {
        config
            .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
            .AddJsonFile("appsettings.json", true, true)
            .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true)
            .AddOcelot()
            .AddEnvironmentVariables();
    })

在这种状况下,Ocelot会寻找全部匹配了 (?i)ocelot.([a-zA-Z0-9]*).json 的文件,而后合并他们。如何你要设置GlobalConfiguration 属性,那么你须要创建一个ocelot.global.json 的文件来进行全局的配置。api

这里上一个例子吧!能够方便你们的理解。数组

新建一个ocelot.good.json文件,并加入下面的配置:缓存

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamPathTemplate": "/good/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ]
}

而后再新建一个ocelot.order.json文件,并加入下面的配置:服务器

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamPathTemplate": "/order/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ]
}

最后新建一个ocelot.all.json文件,并把上篇文章中的路由拷贝到里面:

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamPathTemplate": "/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "LoadBalancerOptions": {
        "Type": "RoundRobin"
      }
    }
  ],
  "GlobalConfiguration": {

  }
}

而后修改下,Program.cs文件中的代码以下:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    config
                        .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath)
                        .AddJsonFile("appsettings.json", true, true)
                        .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true)
                        .AddOcelot()
                        .AddEnvironmentVariables();
                })
                .UseUrls("http://localhost:1000")
                .UseStartup<Startup>();

这里最重要的代码就是config.AddOcelot()了。这段代码就会按照上面的规则查找全部符合条件的文件并合并路由。合并后的代码以下:

{
  "ReRoutes": [
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "UpstreamPathTemplate": "/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AddHeadersToRequest": {},
      "UpstreamHeaderTransform": {},
      "DownstreamHeaderTransform": {},
      "AddClaimsToRequest": {},
      "RouteClaimsRequirement": {},
      "AddQueriesToRequest": {},
      "RequestIdKey": null,
      "FileCacheOptions": {
        "TtlSeconds": 0,
        "Region": null
      },
      "ReRouteIsCaseSensitive": false,
      "ServiceName": null,
      "DownstreamScheme": "http",
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 0,
        "DurationOfBreak": 0,
        "TimeoutValue": 0
      },
      "LoadBalancerOptions": {
        "Type": "RoundRobin",
        "Key": null,
        "Expiry": 0
      },
      "RateLimitOptions": {
        "ClientWhitelist": [],
        "EnableRateLimiting": false,
        "Period": null,
        "PeriodTimespan": 0.0,
        "Limit": 0
      },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": null,
        "AllowedScopes": []
      },
      "HttpHandlerOptions": {
        "AllowAutoRedirect": false,
        "UseCookieContainer": false,
        "UseTracing": false,
        "UseProxy": true
      },
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamHost": null,
      "Key": null,
      "DelegatingHandlers": [],
      "Priority": 1,
      "Timeout": 0,
      "DangerousAcceptAnyServerCertificateValidator": false
    },
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "UpstreamPathTemplate": "/good/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AddHeadersToRequest": {},
      "UpstreamHeaderTransform": {},
      "DownstreamHeaderTransform": {},
      "AddClaimsToRequest": {},
      "RouteClaimsRequirement": {},
      "AddQueriesToRequest": {},
      "RequestIdKey": null,
      "FileCacheOptions": {
        "TtlSeconds": 0,
        "Region": null
      },
      "ReRouteIsCaseSensitive": false,
      "ServiceName": null,
      "DownstreamScheme": "http",
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 0,
        "DurationOfBreak": 0,
        "TimeoutValue": 0
      },
      "LoadBalancerOptions": {
        "Type": "RoundRobin",
        "Key": null,
        "Expiry": 0
      },
      "RateLimitOptions": {
        "ClientWhitelist": [],
        "EnableRateLimiting": false,
        "Period": null,
        "PeriodTimespan": 0.0,
        "Limit": 0
      },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": null,
        "AllowedScopes": []
      },
      "HttpHandlerOptions": {
        "AllowAutoRedirect": false,
        "UseCookieContainer": false,
        "UseTracing": false,
        "UseProxy": true
      },
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamHost": null,
      "Key": null,
      "DelegatingHandlers": [],
      "Priority": 1,
      "Timeout": 0,
      "DangerousAcceptAnyServerCertificateValidator": false
    },
    {
      "DownstreamPathTemplate": "/api/{everything}",
      "UpstreamPathTemplate": "/order/{everything}",
      "UpstreamHttpMethod": [ "Get", "Post" ],
      "AddHeadersToRequest": {},
      "UpstreamHeaderTransform": {},
      "DownstreamHeaderTransform": {},
      "AddClaimsToRequest": {},
      "RouteClaimsRequirement": {},
      "AddQueriesToRequest": {},
      "RequestIdKey": null,
      "FileCacheOptions": {
        "TtlSeconds": 0,
        "Region": null
      },
      "ReRouteIsCaseSensitive": false,
      "ServiceName": null,
      "DownstreamScheme": "http",
      "QoSOptions": {
        "ExceptionsAllowedBeforeBreaking": 0,
        "DurationOfBreak": 0,
        "TimeoutValue": 0
      },
      "LoadBalancerOptions": {
        "Type": "RoundRobin",
        "Key": null,
        "Expiry": 0
      },
      "RateLimitOptions": {
        "ClientWhitelist": [],
        "EnableRateLimiting": false,
        "Period": null,
        "PeriodTimespan": 0.0,
        "Limit": 0
      },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": null,
        "AllowedScopes": []
      },
      "HttpHandlerOptions": {
        "AllowAutoRedirect": false,
        "UseCookieContainer": false,
        "UseTracing": false,
        "UseProxy": true
      },
      "DownstreamHostAndPorts": [
        {
          "Host": "localhost",
          "Port": 1001
        },
        {
          "Host": "localhost",
          "Port": 1002
        }
      ],
      "UpstreamHost": null,
      "Key": null,
      "DelegatingHandlers": [],
      "Priority": 1,
      "Timeout": 0,
      "DangerousAcceptAnyServerCertificateValidator": false
    }
  ],
  "DynamicReRoutes": [],
  "Aggregates": [],
  "GlobalConfiguration": {
    "RequestIdKey": null,
    "ServiceDiscoveryProvider": {
      "Host": null,
      "Port": 0,
      "Type": null,
      "Token": null,
      "ConfigurationKey": null,
      "PollingInterval": 0
    },
    "RateLimitOptions": {
      "ClientIdHeader": "ClientId",
      "QuotaExceededMessage": null,
      "RateLimitCounterPrefix": "ocelot",
      "DisableRateLimitHeaders": false,
      "HttpStatusCode": 429
    },
    "QoSOptions": {
      "ExceptionsAllowedBeforeBreaking": 0,
      "DurationOfBreak": 0,
      "TimeoutValue": 0
    },
    "BaseUrl": null,
    "LoadBalancerOptions": {
      "Type": null,
      "Key": null,
      "Expiry": 0
    },
    "DownstreamScheme": null,
    "HttpHandlerOptions": {
      "AllowAutoRedirect": false,
      "UseCookieContainer": false,
      "UseTracing": false,
      "UseProxy": true
    }
  }
}

Ocelot的合并方式是先对知足格式的文件遍历查找,而后循环加载他们,并提取全部的ReRoutes以及AggregateReRoutes 的数据。若是发现ocelot.global.json ,则添加到GlobalConfiguration 中。而后Ocelto会将合并后的配置保存在ocelot.json的文件中,当Ocelot运行时会加载这个合并后的ocelot.json文件,从而加载了全部的配置。

注意:这里须要注意的是Ocelot在合并的过程当中不会对内容进行验证,只有在最终合并的配置进行校验,因此若是发现问题的话,那么你须要检查最终生成的ocelot.json 是否出错了!

在consul中存储配置

这里你首先要作的就是安装Ocelot中提供的Consul的NuGet包,Nuget安装方式:

Install-Package Ocelot.Provider.Consul

而后在注册服务时添加以下内容:Ocelot将会尝试在Consul KV存储并加载配置。

services
   .AddOcelot()
   .AddConsul()
   .AddConfigStoredInConsul();

固然你还得把下面的配置添加到你的ocelot.json文件中。这里定义Ocelot如何查找Consul根并从Consul中加载并存储配置.

"GlobalConfiguration": {
    "ServiceDiscoveryProvider": {
        "Host": "localhost",
        "Port": 9500
    }
}

变化时从新加载配置文件

Ocelot支持在配置文件发生改变的时候从新加载json配置文件。在加载ocelot.json文件的时候按照下面进行配置,那么当你手动更新ocelot.json文件时,Ocelot将从新加载ocelot.json配置文件。

config.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);

配置Key

若是你使用Consul进行配置,你可能须要配置Key以便区分多个配置,为了指定Key,你须要在json配置文件中的ServiceDiscoveryProvider部分设置ConfigurationKey属性:

"GlobalConfiguration": {
    "ServiceDiscoveryProvider": {
        "Host": "localhost",
        "Port": 9500,
        "ConfigurationKey": "Oceolot_A"
    }
}

在此实例中,Ocelot将会在Consul查找时使用Oceolot_A 做为配置的Key.若是没有设置ConfigurationKey 则Ocelot将使用字符串InternalConfiguration 做为此配置的Key

跟踪重定向和使用CookieContainer

在ReRoute配置中可使用HttpHandlerOptions来设置HttpHandler行为:

  1. AllowAutoRedirect是一个值,指示请求是否应遵循重定向响应。若是请求应自动遵循来自下游资源的重定向响应,则将其设置为true; 不然是假的。默认值为false。
  2. UseCookieContainer是一个值,指示处理程序是否使用CookieContainer属性存储服务器cookie并在发送请求时使用这些cookie。默认值为false。请注意,若是您使CookieContainer,则Ocelot会为每一个下游服务缓存HttpClient。这意味着对该DownstreamService的全部请求将共享相同的cookie。

SSL 错误处理

若是你想忽略SSL 警告/错误,你能够在你的ReRoute 配置中加上以下配置:

"DangerousAcceptAnyServerCertificateValidator": false

固然做者是不建议这样作的,最好的方式是建立你本地以及远程所信任的证书。

Ocelot路由详解

路由

Ocelot的最主要的功能是接收传入的http请求并将其转发到下游服务。

Ocelot使用ReRoute节点描述将一个请求路由到另外一个请求。为了让路由在Ocelot中起做用,您须要在配置中设置ReRoute:

{
    "ReRoutes": [
    ]
}

要配置ReRoute,您须要在ReRoutes json数组中至少添加一个:

{
    "DownstreamPathTemplate": "/api/good/{goodId}",//下游路由模板
    "DownstreamScheme": "http",//下游路由请求的方式
    "DownstreamHostAndPorts": [//下游路由的Host以及端口
            {
                "Host": "localhost",
                "Port": 1001,
            }
        ],
    "UpstreamPathTemplate": "/good/{goodId}",//上游路由请求的模板
    "UpstreamHttpMethod": [ "Put", "Delete" ]//上游路由请求的方式
}

DownstreamPathTemplate,DownstreamScheme和DownstreamHostAndPorts定义请求将转发到的URL。

DownstreamHostAndPorts是一个集合,用于定义您但愿将请求转发到的任何下游服务的主机和端口。一般这只包含一个条目,但有时你但愿对下游请求服务进行负载均衡,这个时候你就能够添加多个条目,并配合负载均衡选项进行相关的负载均衡设置。

UpstreamPathTemplate是Ocelot用于标识要用于给定请求的DownstreamPathTemplate对应的URL。使用UpstreamHttpMethod以便Ocelot能够区分具备不一样HTTP谓词的请求到相同的URL。您能够设置特定的HTTP方法列表,也能够设置一个空列表以容许全部的。

在Ocelot中,您能够以{something}的形式将变量的占位符添加到模板中。占位符变量须要同时出如今DownstreamPathTemplate和UpstreamPathTemplate属性中。请求时Ocelot将尝试请求时进行替换。

你也能够像下面这样配置,捕获全部的路由:

{
    "DownstreamPathTemplate": "/api/{everything}",
    "DownstreamScheme": "http",
    "DownstreamHostAndPorts": [
            {
                "Host": "localhost",
                "Port": 1001,
            },
            {
                "Host": "localhost",
                "Port": 1002,
            }
        ],
    "UpstreamPathTemplate": "/{everything}",
    "UpstreamHttpMethod": [ "Get", "Post" ]
}

这个配置将会把路径+查询字符串通通转发到下游路由.

注意:默认的ReRouting的配置是不区分大小写的,若是须要修改此配置,能够经过下面进行配置:

"ReRouteIsCaseSensitive": true

这意味着Ocelot将尝试将传入的上游URL与上游模板匹配时,区分大小写。

所有捕获

Ocelot的路由还支持捕获全部样式路由,用户能够指定他们想要匹配全部请求。

若是您设置以下所示的配置,则全部请求都将直接代理。占位符{url}名称不重要,任何名称均可以使用。

{
    "DownstreamPathTemplate": "/{url}",
    "DownstreamScheme": "http",
    "DownstreamHostAndPorts": [
            {
                "Host": "localhost",
                "Port": 1001,
            }
        ],
    "UpstreamPathTemplate": "/{url}",
    "UpstreamHttpMethod": [ "Get" ]
}

上面配置的所有捕获的优先级低于任何其余法人ReRoute。若是您的配置中还有下面的ReRoute,那么Ocelot会在所有捕获以前匹配它。

{
    "DownstreamPathTemplate": "/",
    "DownstreamScheme": "http",
    "DownstreamHostAndPorts": [
            {
                "Host": "localhost",
                "Port": 1001,
            }
        ],
    "UpstreamPathTemplate": "/",
    "UpstreamHttpMethod": [ "Get" ]
}

上游主机

此功能容许您根据上游主机得到ReRoutes。这经过查看客户端使用的主机头,而后将其用做咱们用于识别ReRoute的信息的一部分来工做。

要使用此功能,请在配置中添加如下内容。

{
    "DownstreamPathTemplate": "/",
    "DownstreamScheme": "http",
    "DownstreamHostAndPorts": [
            {
                "Host": "localhost",
                "Port": 1001,
            }
        ],
    "UpstreamPathTemplate": "/",
    "UpstreamHttpMethod": [ "Get" ],
    "UpstreamHost": "yilezhu.cn"
}

仅当主机标头值为yilezhu.cn时,才会匹配上面的ReRoute。

若是您没有在ReRoute上设置UpstreamHost,那么任何主机头都将与之匹配。这意味着若是你有两个相同的ReRoutes,除了UpstreamHost,其中一个为null而另外一个不为null 那么Ocelot将支持已设置的那个。

优先级

你能够经过ocelot.json文件的ReRoutes节点中的Priorty属性来设置匹配上游HttpRequest的优先级顺序
好比,下面两个路由:

{
    "UpstreamPathTemplate": "/goods/{catchAll}"
    "Priority": 0
}

以及

{
    "UpstreamPathTemplate": "/goods/delete"
    "Priority": 1
}

上面两个路由中,若是向Ocelot发出的请求时/goods/delete格式的话,则Ocelot会优先匹配/goods /delete 的路由。

动态路由

做者的想法是在使用服务发现提供程序时启用动态路由,这样您就没必要提供ReRoute的配置。咱们会在服务发现那一章进行详细的介绍。

查询字符串

Ocelot容许您指定一个查询字符串做为DownstreamPathTemplate的一部分,以下例所示。

{
    "ReRoutes": [
        {
            "DownstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}",
            "UpstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates",
            "UpstreamHttpMethod": [
                "Get"
            ],
            "DownstreamScheme": "http",
            "DownstreamHostAndPorts": [
                {
                    "Host": "localhost",
                    "Port": 50110
                }
            ]
        }
    ],
    "GlobalConfiguration": {
    }
}

在此示例中,Ocelot将使用上游路径模板中{unitId}的值,并将其做为名为unitId的查询字符串参数添加到下游请求中!

Ocelot还容许您将查询字符串参数放在UpstreamPathTemplate中,以便您能够将某些查询与某些服务匹配。

{
    "ReRoutes": [
        {
            "DownstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates",
            "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}",
            "UpstreamHttpMethod": [
                "Get"
            ],
            "DownstreamScheme": "http",
            "DownstreamHostAndPorts": [
                {
                    "Host": "localhost",
                    "Port": 50110
                }
            ]
        }
    ],
    "GlobalConfiguration": {
    }
}

在此示例中,Ocelot将仅匹配具备匹配的url路径的请求,而且查询字符串以unitId = something开头。您能够在此以后进行其余查询,但必须以匹配参数开头。此外,Ocelot将交换查询字符串中的{unitId}参数,并在下游请求路径中使用它。

源码地址

固然是放上实例中的源码地址了:https://github.com/yilezhu/OcelotDemo

Ocelot简易教程目录

  1. Ocelot简易教程(一)之Ocelot是什么
  2. Ocelot简易教程(二)之快速开始1
  3. Ocelot简易教程(二)之快速开始2
  4. Ocelot简易教程(三)之主要特性及路由详解
  5. Ocelot简易教程(四)之请求聚合以及服务发现
  6. Ocelot简易教程(五)之集成IdentityServer认证以及受权
  7. Ocelot简易教程(六)之重写配置文件存储方式并优化响应数据
  8. Ocelot简易教程(七)之配置文件数据库存储插件源码解析

    总结

    本文主要是对Ocelot的新特性以及路由进行详细的介绍,这些介绍对你使用ocelot会有很大的帮助。下篇文章呢,我会对请求聚合以及服务发现以及动态路由进行记录,敬请期待!同时须要说明一点是,本文大部份内容是翻译自官方文档,固然中间穿插着本身在使用过程当中一些理解,但愿你们可以喜欢!

相关文章
相关标签/搜索