• ASP.NET Core框架学习


    参考:

    官网文档:ASP.NET Core

    深入探究ASP.NET Core Startup初始化

    蒋金楠:

    梁桐铭:书籍《深入浅出 ASP.NET Core》、 博客园:从零开始学 ASP.NET Core 与 EntityFramework Core 

    ASP.NET Core跨平台 技术内幕

    ASP.NET MVC从请求到响应发生了什么

    ASP.NET/MVC/Core的HTTP请求流程

    System.Configuration 命名空间 :包含提供用于处理配置数据的编程模型的类型

    • Configuration 类 :表示适用于特定计算机、应用程序或资源的配置文件。 此类不能被继承

     

    ASP.NET Core框架的本质

    参考:200行代码,7个对象——让你了解ASP.NET Core框架的本质 --重点

    Asp.net Core 入门实战 2.请求流程

    第一个对象:HttpContext (Http上下文类)

    HttpContext:封装有关单个HTTP请求的所有特定于HTTP的信息。

    第二个对象:RequestDelegate(请求委托)

    RequestDelegate:可以处理HTTP请求的功能。

    第三个对象:Middleware(中间件)

    中间件中间件是一种装配到应用管道以处理请求和响应的软件。 每个组件:

    • 选择是否将请求传递到管道中的下一个组件。
    • 可在管道中的下一个组件前后执行工作。

    中间件在ASP.NET Core被表示成一个Func<RequestDelegate, RequestDelegate>对象,也就是说它的输入和输出都是一个RequestDelegate

    第四个对象:ApplicationBuilder(应用程序建造者)

    ApplicationBuilder:应用程序建造者

    第五个对象:Server(服务)

    IServer:代表服务器接口

    第六个对象:WebHost(Web主机)

    WebHost:提供方便的方法来创建具有预配置默认值的IWebHostIWebHostBuilder实例

    真实框架中是在Program类中调用ConfigureWebHostDefaults方法,此方法是用IHostBuilder接口声明的,此接口内有个 IHost 接口 

    第七个对象:WebHostBuilder(Web主机建造者)

    WebHostBuilder Web主机建造者

    真实框架中是在Program类中调用ConfigureWebHostDefaults方法,此方法是用IHostBuilder接口声明的

    asp.net core框架中的Http

    Http基础

    参考:

    HTTP   

    HTTP 2.0

    HTTP协议详解

    HTTP请求报文和响应报文

    ASP.NET/MVC/Core的HTTP请求流程

    Asp.net Core 入门实战 2.请求流程

    c# netcore 发送http请求并接收返回数据

    .NET Core中使用HttpClient的正确姿势(三种使用方式)

    http是什么:

    http是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当。

    常用方法:GET 查询 、POST 修改、PUT 增加、DELETE 删除

    http报文结构:

    Request请求报文

    • Request line:请求行,由三部分组成:请求方法,请求URL(不包括域名)
    • Request header:请求头部,由关键字/值对组成,每行一对,例如:
    • blank line空行
    • request-body请求数据:GET没有请求数据,POST有

    具体如下

    POST /user HTTP/1.1      //请求行
    Host: www.user.com
    Content-Type: application/x-www-form-urlencoded
    Connection: Keep-Alive
    User-agent: Mozilla/5.0.      //以上是首部行
    (此处必须有一空行)  //空行分割header和请求内容 
    name=world   请求体

    相应报文

    • 状态行:由三部分组成:服务器HTTP协议版本,响应状态码,状态码的文本描述
    • 状态码:由3位数字组成,第一个数字定义了响应的类别
    • 空行
    • 响应体

    MVC和WebApi的Http

    他们两个的控制器都会继承

    Razor的Http

    类中继承

    Blazor Server的Http

    启动流程简单说明

    参考:

    基础知识

    ASP.NET Core管道详解[5]: ASP.NET Core应用是如何启动的?[上篇]

    ASP.NET Core管道详解[6]: ASP.NET Core应用是如何启动的?[下篇]

    流程简单说明如下:

    • 创建asp.net core项目后默认是一个应用台控制程序,入口是Program类的Main方法
    • 在Main方法内调用了一个创建主机的方法CreateHostBuilder,此方法做了下面两件事
      • CreateDefaultBuilder:创建通用主机,       (该方法属于Microsoft.Extensions.Hosting下的 Host 类)
      • ConfigureWebHostDefaults,                      (该方法属于Microsoft.Extensions.Hosting下的 GenericHostBuilderExtensions 类)
        • 使用默认值配置IHostBuilder来托管Web应用程序,指定Startup类
        • 创建web主机后,就有监控、请求、响应等功能,然后交给对应的中间件来处理

    Program类代码如下:

        public class Program
        {
            public static void Main(string[] args)
            {
                CreateHostBuilder(args).Build().Run();
            }
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    });
        }

    以上是流程简单说明,更详细看下面的主机、托管、启动类、配置等

    主机

    ASP.NET Core 应用在启动时构建主机, 主机是封装了应用的所有资源,例如:

    • HTTP 服务器实现
    • 中间件组件
    • 日志
    • 依赖关系注入 (DI) 服务
    • 配置

    有两种主机,推荐使用通用主机

    创建主机 

    Host.CreateDefaultBuilder:创建通用主机,使用预配置默认值初始化 HostBuilder 类的新实例。

    默认生成主机的设置 :

    • 内容根目录设置为由 GetCurrentDirectory 返回的路径。
    • 通过以下项加载主机配置:
      • 前缀为 DOTNET_ 的环境变量。
      • 命令行参数。
    • 通过以下对象加载应用配置:
      • appsettings.json.
      • appsettings.{Environment}.json。
      • 应用在 Development 环境中运行时的用户机密
      • 环境变量。
      • 命令行参数。
    • 添加以下日志记录提供程序:
      • 控制台
      • 调试
      • EventSource
      • EventLog(仅当在 Windows 上运行时)
    • 当环境为“开发”时,启用范围验证依赖关系验证

    属于Microsoft.Extensions.Hosting下的Host类,重点看CreateDefaultBuilder方法,如下:

    // Microsoft.Extensions.Hosting.Host
    using System.IO;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Logging.EventLog;
    
    public static class Host
    {
        public static IHostBuilder CreateDefaultBuilder()
        {
            return CreateDefaultBuilder(null);
        }
    
        public static IHostBuilder CreateDefaultBuilder(string[] args)
        {
            HostBuilder hostBuilder = new HostBuilder();
            hostBuilder.UseContentRoot(Directory.GetCurrentDirectory());
            hostBuilder.ConfigureHostConfiguration(delegate(IConfigurationBuilder config)
            {
                config.AddEnvironmentVariables("DOTNET_");
                if (args != null)
                {
                    config.AddCommandLine(args);
                }
            });
            hostBuilder.ConfigureAppConfiguration(delegate(HostBuilderContext hostingContext, IConfigurationBuilder config)
            {
                IHostEnvironment hostingEnvironment = hostingContext.HostingEnvironment;
                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true).AddJsonFile("appsettings." + hostingEnvironment.EnvironmentName + ".json", optional: true, reloadOnChange: true);
                if (hostingEnvironment.IsDevelopment() && !string.IsNullOrEmpty(hostingEnvironment.ApplicationName))
                {
                    Assembly assembly = Assembly.Load(new AssemblyName(hostingEnvironment.ApplicationName));
                    if (assembly != null)
                    {
                        config.AddUserSecrets(assembly, optional: true);
                    }
                }
                config.AddEnvironmentVariables();
                if (args != null)
                {
                    config.AddCommandLine(args);
                }
            }).ConfigureLogging(delegate(HostBuilderContext hostingContext, ILoggingBuilder logging)
            {
                bool num = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
                if (num)
                {
                    logging.AddFilter<EventLogLoggerProvider>((LogLevel level) => level >= LogLevel.Warning);
                }
                logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                logging.AddConsole();
                logging.AddDebug();
                logging.AddEventSourceLogger();
                if (num)
                {
                    logging.AddEventLog();
                }
            }).UseDefaultServiceProvider(delegate(HostBuilderContext context, ServiceProviderOptions options)
            {
                bool validateOnBuild = (options.ValidateScopes = context.HostingEnvironment.IsDevelopment());
                options.ValidateOnBuild = validateOnBuild;
            });
            return hostBuilder;
        }
    }
    View Code

    托管Web应用程序

    属于Microsoft.Extensions.Hosting下的GenericHostBuilderExtensions类的方法,如下:

    ConfigureWebHostDefaults (IHostBuilder, Action<IWebHostBuilder>):使用默认值配置IHostBuilder来托管Web应用程序,一般通过方法参数IWebHostBuilder.UseStartup指定启动类UseStartup

    以下默认值应用于IHostBuilder

    • 从前缀为 ASPNETCORE_ 的环境变量加载主机配置。
    • 使用应用的托管配置提供程序将 Kestrel 服务器设置为 web 服务器并对其进行配置。
    • 添加主机筛选中间件
    • 如果 ASPNETCORE_FORWARDEDHEADERS_ENABLED 等于 true,则添加转接头中间件
    • 支持 IIS 集成。

    查看ConfigureWebHostDefaults方法的源码发现:方法内调用了Microsoft.AspNetCore下的WebHost.ConfigureWebDefaults方法

    // Microsoft.Extensions.Hosting.GenericHostBuilderExtensions
    using System;
    using Microsoft.AspNetCore;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Hosting;
    
    public static class GenericHostBuilderExtensions
    {
        public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
        {
            return builder.ConfigureWebHost(delegate(IWebHostBuilder webHostBuilder)
            {
                WebHost.ConfigureWebDefaults(webHostBuilder);
                configure(webHostBuilder);
            });
        }
    }

    Microsoft.AspNetCore下的 WebHost  类的下的ConfigureWebDefaults方法源码,如下:

    // Microsoft.AspNetCore.WebHost
    using System;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.HostFiltering;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Hosting.StaticWebAssets;
    using Microsoft.AspNetCore.HttpOverrides;
    using Microsoft.AspNetCore.Server.Kestrel.Core;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using Microsoft.Extensions.Options;
    
    internal static void ConfigureWebDefaults(IWebHostBuilder builder)
    {
        builder.ConfigureAppConfiguration(delegate(WebHostBuilderContext ctx, IConfigurationBuilder cb)
        {
            if (ctx.HostingEnvironment.IsDevelopment())
            {
                StaticWebAssetsLoader.UseStaticWebAssets(ctx.HostingEnvironment, ctx.Configuration);
            }
        });
        builder.UseKestrel(delegate(WebHostBuilderContext builderContext, KestrelServerOptions options)
        {
            options.Configure(builderContext.Configuration.GetSection("Kestrel"));
        }).ConfigureServices(delegate(WebHostBuilderContext hostingContext, IServiceCollection services)
        {
            services.PostConfigure(delegate(HostFilteringOptions options)
            {
                if (options.AllowedHosts == null || options.AllowedHosts.Count == 0)
                {
                    string[] array = hostingContext.Configuration["AllowedHosts"]?.Split(new char[1]
                    {
                        ';'
                    }, StringSplitOptions.RemoveEmptyEntries);
                    options.AllowedHosts = ((array != null && array.Length != 0) ? array : new string[1]
                    {
                        "*"
                    });
                }
            });
            services.AddSingleton((IOptionsChangeTokenSource<HostFilteringOptions>)new ConfigurationChangeTokenSource<HostFilteringOptions>(hostingContext.Configuration));
            services.AddTransient<IStartupFilter, HostFilteringStartupFilter>();
            if (string.Equals("true", hostingContext.Configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
            {
                services.Configure(delegate(ForwardedHeadersOptions options)
                {
                    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
                    options.KnownNetworks.Clear();
                    options.KnownProxies.Clear();
                });
                services.AddTransient<IStartupFilter, ForwardedHeadersStartupFilter>();
            }
            services.AddRouting();
        }).UseIIS()
            .UseIISIntegration();
    }
    View Code

    启动类Startup

    启动类中就是两个方法,一个是依赖注入和服务注册ConfigureServices,一个是中间件配置Configure

    • ConfigureServices (IServiceCollection services):依赖注入和服务注册,例如数据库上下文、控制器、其他的依赖注入等
    • Configure (IApplicationBuilder app, IWebHostEnvironment env):用于指定应用响应 HTTP 请求的方式。 可通过将中间件组件添加到 IApplicationBuilder 实例来配置请求管道,例如路由、认证、跨越等

    生成项目的Startup 类的代码一般如下:

    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.HttpsPolicy;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    
    namespace WebRazor
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddRazorPages();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
                app.UseHttpsRedirection();
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapRazorPages();
                });
            }
        }
    }
    View Code

    配置

    概念

    ASP.NET Core 中的配置是使用一个或多个配置提供程序执行的。 配置提供程序使用各种配置源从键值对读取配置数据:

    • 设置文件,例如 appsettings.json
    • 环境变量
    • Azure Key Vault
    • Azure 应用程序配置
    • 命令行参数
    • 已安装或已创建的自定义提供程序
    • 目录文件
    • 内存中的 .NET 对象

    读取自定义的Json配置文件

    参考:文件配置提供程序

    在Program类的后面添加ConfigureAppConfiguration,红色的代码,如下:

    备注:可以查看上面创建主机的方法CreateDefaultBuilder的源码,里面就有这个 ConfigureAppConfiguration的方法的具体实现。

            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    })
                    .ConfigureAppConfiguration((hostingContext, config) =>
                    {
                        config.AddJsonFile("appsettings22.json", optional: true, reloadOnChange: true);
                    });

    模型验证

    参考:模型验证

    认证与授权

    参考: 关于WEB Service&WCF&WebApi实现身份验证之WebApi篇

    认证

    全局认证

    参考:全局认证限制:必须登录后才能访问

    单页面SAP认证

    使用 SPA 标识--.net core3.0起

    使用identityserver4

    授权

    角色授权
    策略授权

    ASP.NET Core 中的Areas

    文件

    参考:

    在 ASP.NET Core 中上传文件

    ASP.NET Core Blazor 文件上传

    ASP.NET Core 中的文件提供程序

    文件和流 I/O

    ASP.NET Core MVC

    工作原理:

    启动入口

    Global.asax ==》Startup.cs

    Global.asax 下面是一个MvcApplication类集成,该类继承System.Web.HttpApplication,类下面是一个Application_Start方法,Application_Start方法中包含注册区域、注册全局的Filter、注册路由、合并压缩、打包工具Combres。

    HttpApplication类定义对 ASP.NET 应用程序内所有应用程序对象公用的方法、属性和事件。 此类是用户在 Global.asax 文件中定义的应用程序的基类

    简单的流程

    用户请求是通过【视图V】进入==》通过【控制器C】处理==》从【模型M】获取数据==》最后返回给【视图V】。

    也知道ASP.NET MVC中的约定可以通过修改配置来修改。

    疑问

    控制器、视图、模型是怎么根据约定来传递数据的

    当时有一个疑问困惑好久,就是数据是怎么通过控制器中发送到视图的?

    虽然知道能根据控制器内的方法来识别对应的视图名称,但是控制器方法的return是怎么把模型数据发送到视图的@model,当时记得自己折腾好久都没整明白,百度搜索找到很少相关,最后看到《ASP.NET MVC 5框架揭秘》这本书才知道是和视图引擎ViewEngine有关,视图引擎会把控制器绑定的数据和视图关联起来

    MVC中的http

    是怎么使用http通信的???

    修改约定

    只搜索cshtml,默认会搜索多种视图文件

    在Global.asax中增加

    /配置视图搜索位置、文件类型,以提高性能

    ViewEngines.Engines.Clear();

    ViewEngines.Engines.Add(new CustomLocationViewEngine());

    然后把CustomLocationViewEngine.cs文件放到App_Start

    配置类/注册类:

     Global.asax 下面类的方法内的注册信息是怎么被框架加载识别的?

    ps:框架已经锁定好MvcApplication、Application_Start的名称的了,如果更改名称就会报错,也就是说只要在框架制定的方法中添加方法,框架在第一次启动时会自动加载Application_Start内的方法。

    IIS 服务器

    IIS基础

    参考:

    IIS官网文档

    IIS体系结构简介

    使用 IIS 在 Windows 上托管 ASP.NET Core

    推荐使用程内托管

    IIS运行原理 + 三个重要组件

    • 协议侦听器(HTTP.sys)
      • 协议侦听器接收特定于协议的请求,将其发送到IIS进行处理,然后将响应返回给请求者。例如,当客户端浏览器从Internet请求网页时,HTTP侦听器HTTP.sys接收请求并将其发送到IIS进行处理。IIS处理请求后,HTTP.sys将响应返回到客户端浏览器。
    • 万维网发布服务(WWW服务)
      • 在IIS中,WWW服务不再管理工作进程。而是,WWW服务是HTTP侦听器HTTP.sys的侦听器适配器。作为侦听器适配器,WWW服务主要负责配置HTTP.sys,在配置更改时更新HTTP.sys以及在请求进入请求队列时通知WAS。

        此外,WWW服务继续收集网站的计数器。由于性能计数器仍然是WWW服务的一部分,因此它们是HTTP特定的,不适用于WAS。

    • Windows进程激活服务(WAS)
      • WAS中的配置管理:

        • 在启动时,WAS从ApplicationHost.config文件中读取某些信息,并将该信息传递到服务器上的侦听器适配器。侦听器适配器是在WAS和协议侦听器(例如HTTP.sys)之间建立通信的组件。侦听器适配器收到配置信息后,便会配置其相关的协议侦听器,并准备使侦听器侦听请求。

          对于WCF,侦听器适配器包括协议侦听器的功能。因此,将基于WAS的信息配置WCF侦听器适配器(例如NetTcpActivator)。配置NetTcpActivator后,它将侦听使用net.tcp协议的请求。有关WCF侦听器适配器的更多信息,请参见MSDN上的WAS激活体系结构

      •  WAS流程管理:

        • WAS管理HTTP和非HTTP请求的应用程序池和辅助进程。当协议侦听器接收到客户端请求时,WAS将确定工作进程是否正在运行。如果应用程序池已经具有正在处理请求的工作进程,则侦听器适配器将请求传递到工作进程进行处理。如果应用程序池中没有工作进程,则WAS将启动一个工作进程,以便侦听器适配器可以将请求传递给它进行处理。

    项目部署到 IIS 步骤 

    安装.net core托管捆绑包,下载对应版本:当前 .NET Core 托管捆绑包安装程序(直接下载)

    双击应用程序成,改为:无托管代码

    如果是64位的:鼠标右键=》设置应用程序池默认设置,启用32位应用程序这里要改为Lalse

    标识设置为有权限的账号

    Kestrel 服务器

    参考: 官方文档Kestrel

    http证书

    要用dotnet run启动项目

    注意:此方法只是http能使用,而且需要网卡在使用的状态(因为是),https还是不能使用,还需要继续深入了解https

    在项目文件下用命令(dotnet run)启动时,大部分浏览器(IE不会)会报错:ERR_HTTP2_INADEQUATE_TRANSPORT_SECURITY

    端口要根据实际设置,可以查看Properties文件夹下的文件launchSettings.json

    Program类要添加下面红色代码,这里只是端口的方式,还有更多方式(如TCPSocket、Limits等),可以参考官方文档,

    或者参考官方教程的示例代码,文件夹路径:aspnetcorefundamentalsserverskestrelsamples3.xKestrelSample

    端口如果不设置可以用0,就是随机端口

    using System.Net; //要引用的命名空间
    
            public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>()
                        .ConfigureKestrel(serverOptions =>
                        {
                            serverOptions.Listen(IPAddress.Loopback, 5001); //端口为0就是随机端口
                        });
                    });

    然后项目文件的地址栏中输入cmd进入命令窗口,然后通过命令启动项目:dotnet run,注入,不能直接用vs启动,否则还是不能访问

    然后只能用红色框内的地址访问:http://127.0.0.1:5001/,(注意:用本机的https://localhost访问还是会失败)

    发布后的生产环境

    好像说需要安装https证书,具体还要验证。

    不过按上面设置后,发布后也能正常使用,安全问题???

    视图组件

    参考:

    官网文档视图组件

    视图组件封装:.Net MVC&&datatables.js&&bootstrap做一个界面的CRUD有多简单--邹琼俊

    ASP.NET/MVC/Core的HTTP请求流程

    测试、调试和疑难解答

    .NET中的测试

    Razor Pages 单元测试

    测试控制器

    测试中间件

    远程调试

    快照调试

    Visual Studio 中的快照调试

    集成测试

    负载测试和压力测试

    故障排除和调试

    Logging

    Azure 和 IIS 疑难解答

    Azure 和 IIS 错误参考

  • 相关阅读:
    MYSQL进阶学习笔记十:MySQL慢查询!(视频序号:进阶_23-24)
    MYSQL进阶学习笔记九:MySQL事务的应用!(视频序号:进阶_21-22)
    MYSQL学习拓展一:MySQL 存储过程之游标的使用!
    MYSQL进阶学习笔记八:MySQL MyISAM的表锁!(视频序号:进阶_18-20)
    linux初级学习笔记十:linux grep及正则表达式!(视频序号:04_4)
    linux初级学习笔记九:linux I/O管理,重定向及管道!(视频序号:04_3)
    MYSQL进阶学习笔记七:MySQL触发器的创建,应用及管理!(视频序号:进阶_16,17)
    linux shell 字符串操作
    How to Install JAVA 8 (JDK/JRE 8u111) on Debian 8 & 7 via PPA
    iptables Data filtering详解
  • 原文地址:https://www.cnblogs.com/qingyunye/p/12900321.html
Copyright © 2020-2023  润新知