• 如何让你的.NET WebAPI程序支持HTTP3?


    下面我将总结构建Http3的经验,以Token Gateway的项目为例,请注意使用Http3之前你需要知道它的限制,

    Windows

    • Windows 11 版本 22000 或更高版本/Windows Server 2022。
    • TLS 1.3 或更高版本的连接。

    Linux

    • 已安装 libmsquic 包。

    实现讲解

    首先我们需要拉取我们的代码

    git clone https://gitee.com/hejiale010426/Gateway.git
    cd Gateway
    

    然后我们打开Program.cs

    #region FreeSql类型转换
    
    Utils.TypeHandlers.TryAdd(typeof(Dictionary<string, string>), new StringJsonHandlerstring, string>>());
    Utils.TypeHandlers.TryAdd(typeof(RouteMatchEntity), new StringJsonHandler());
    Utils.TypeHandlers.TryAdd(typeof(List), new StringJsonHandler>());
    Utils.TypeHandlers.TryAdd(typeof(string[]), new StringJsonHandler<string[]>());
    
    #endregion
    
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Configuration.GetSection(nameof(JwtOptions))
        .Get();
    
    builder.WebHost.UseKestrel(options =>
    {
        // 配置多个域名证书
        options.ConfigureHttpsDefaults(adapterOptions =>
        {
            adapterOptions.ServerCertificateSelector = (_, name) =>
            {
                // 从Certificate服务中获取
                if (string.IsNullOrEmpty(name) ||
                    !CertificateService.CertificateEntityDict.TryGetValue(name, out var certificate)) return new X509Certificate2();
    
                var path = Path.Combine("/data/", certificate.Path);
    
                if (File.Exists(path)) return new X509Certificate2(path, certificate.Password);
    
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"证书文件不存在:{path}");
                Console.ResetColor();
                throw new Exception($"证书文件不存在:{path}");
            };
        });
    });
    
    builder.WebHost.ConfigureKestrel(kestrel =>
    {
        kestrel.Limits.MaxRequestBodySize = null;
        
        kestrel.ListenAnyIP(8081, portOptions =>
        {
            portOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
            portOptions.UseHttps();
        });
    
        kestrel.ListenAnyIP(8080, portOptions =>
        {
            portOptions.Protocols = HttpProtocols.Http1AndHttp2;
        });
    });
    
    #region Jwt
    
    builder.Services
        .AddAuthorization()
        .AddJwtBearerAuthentication();
    
    #endregion
    
    builder.Services.Configure(options =>
    {
        options.Limits.MaxRequestBodySize = int.MaxValue; 
    });
    
    builder.Services.Configure(x =>
    {
        x.ValueLengthLimit = int.MaxValue;
        x.MultipartBodyLengthLimit = int.MaxValue; // if don't set default value is: 128 MB
        x.MultipartHeadersLengthLimit = int.MaxValue;
    });
    
    builder.Services.ConfigureHttpJsonOptions(options =>
    {
        options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
        options.SerializerOptions.Converters.Add(new JsonDateTimeConverter());
    });
    
    builder.Services.AddHostedService();
    
    builder.Services.AddCors(options =>
    {
        options.AddPolicy("AllowAll",
            builder => builder
                .SetIsOriginAllowed(_ => true)
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
    });
    
    builder.Configuration.GetSection(nameof(RequestOptions)).Get();
    builder.Services.AddMemoryCache();
    
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    builder.Services.AddSingleton();
    
    builder.Services.AddSingleton();
    
    builder.Services.AddSingleton(_ =>
    {
        var directory = new DirectoryInfo("/data");
        if (!directory.Exists)
        {
            directory.Create();
        }
    
        return new FreeSqlBuilder()
            .UseConnectionString(DataType.Sqlite, builder.Configuration.GetConnectionString("DefaultConnection"))
            .UseMonitorCommand(cmd => Console.WriteLine($"Sql:{cmd.CommandText}")) //监听SQL语句
            .UseAutoSyncStructure(true) //自动同步实体结构到数据库,FreeSql不会扫描程序集,只有CRUD时才会生成表。
            .Build();
    });
    
    // 使用内存加载配置 
    builder.Services.AddReverseProxy()
        .LoadFromMemory(GatewayService.Routes, GatewayService.Clusters);
    
    var app = builder.Build();
    
    app.UseCors("AllowAll");
    
    app.UseMiddleware();
    app.UseMiddleware();
    
    // 配置MiniApis服务
    app.MapRequestLog();
    app.MapStaticFileProxy();
    app.MapFileStorage();
    app.MapGateway();
    app.MapAuthority();
    app.MapCertificate();
    app.MapSetting();
    
    app.UseAuthentication();
    app.UseAuthorization();
    
    app.MapReverseProxy();
    
    await app.RunAsync();
    

    上面是完整的代码,我们不过多讲解,只讲解HTTP3需要哪些配置

    首先,我们的Gateway支持动态加载证书,而HTTP3是强制使用证书的,我们在这里提供了动态配置HTTP3的实现。

    builder.WebHost.UseKestrel(options =>
    {
        // 配置多个域名证书
        options.ConfigureHttpsDefaults(adapterOptions =>
        {
            adapterOptions.ServerCertificateSelector = (_, name) =>
            {
                // 从Certificate服务中获取
                if (string.IsNullOrEmpty(name) ||
                    !CertificateService.CertificateEntityDict.TryGetValue(name, out var certificate)) return new X509Certificate2();
    
                var path = Path.Combine("/data/", certificate.Path);
    
                if (File.Exists(path)) return new X509Certificate2(path, certificate.Password);
    
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine($"证书文件不存在:{path}");
                Console.ResetColor();
                throw new Exception($"证书文件不存在:{path}");
            };
        });
    });
    

    上面配置好了证书,下面我们配置启用HTTP3,下面我们对于容器会监听俩个端口8080,8081,8080是Http端口,所以不需要开启HTTP3,我们在监听8081的时候修改了协议为HttpProtocols.Http1AndHttp2AndHttp3,然后portOptions.UseHttps()强制使用HTTPS,Http1AndHttp2AndHttp3是自动支持多个协议,如果HTTP3不支持则会降级支持HTTP2如果HTTP2不支持则降级支持HTTP1,由于浏览器不确定你是否支持HTTP3所以会先请求一个HTTP2或HTTP1协议的请求,如果支持的话框架会自动给响应头返回一个Alt-Svc的值。

    builder.WebHost.ConfigureKestrel(kestrel =>
    {
        kestrel.Limits.MaxRequestBodySize = null;
        
        kestrel.ListenAnyIP(8081, portOptions =>
        {
            portOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
            portOptions.UseHttps();
        });
    
        kestrel.ListenAnyIP(8080, portOptions =>
        {
            portOptions.Protocols = HttpProtocols.Http1AndHttp2;
        });
    });
    

    上面俩个配置完成以后我们修改我们的Dockerfile,由于微软提供的默认的镜像是不提供libmsquic,所以我们需要自己写一个Dockerfile,打开我们Gateway项目中的Dockerfile,并且添加libmsquic的构建流程

    FROM mcr.microsoft.com/dotnet/aspnet:8.0.1-bookworm-slim-amd64 AS base
    USER root
    RUN  apt update \
        && apt-get install -y --no-install-recommends curl \
        && curl -sSL -O https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb \
        && dpkg -i packages-microsoft-prod.deb \
        && rm packages-microsoft-prod.deb \
        && apt-get update \
        && apt-get install -y libmsquic \
        && apt-get purge -y --auto-remove wget && apt-get clean && rm -rf /var/lib/apt/lists/*
    WORKDIR /app
    EXPOSE 8080
    EXPOSE 8081
    
    FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
    ARG BUILD_CONFIGURATION=Release
    WORKDIR /src
    COPY ["src/Gateway/Gateway.csproj", "src/Gateway/"]
    RUN dotnet restore "./src/Gateway/Gateway.csproj"
    COPY . .
    WORKDIR "/src/src/Gateway"
    RUN dotnet build "./Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
    
    FROM build AS publish
    ARG BUILD_CONFIGURATION=Release
    RUN dotnet publish "./Gateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
    
    FROM base AS final
    WORKDIR /app
    COPY --from=publish /app/publish .
    ENTRYPOINT ["dotnet", "Gateway.dll"]
    

    在构建镜像的时候需要使用root权限,否则可能导致权限不足构建失败,上面完成了我们本地的镜像构建和.NET Core的HTTP3的启用,然后需要我们构建好了镜像就可以在服务器中跑一个容器了,在运行容器的时候还会有一个坑,下面我们来慢慢讲解,

    部署服务

    打开我们的服务器使用Linux服务器打开,下面是我们的Gateway的一个Compose版本,由于Docker端口监听默认使用的是tcp,所以我们需要监听俩个协议,因为HTTP3是基于UDP实现的,这也是坑之一,还有如果登录失败可能是映射目录权限不够创建Sqlite文件失败导致。

    services:
      gateway-api:
        image: registry.token-ai.cn:8300/gateway
        restart: always
        environment:
          USER: root
          PASS: Aa010426.
        ports:
          - 8080:8080
          - 8081:8081/udp
          - 8081:8081/tcp
        volumes:
          - ./data:/data/
    
      gateway-web:
        image: registry.cn-shenzhen.aliyuncs.com/tokengo/gateway-web
        restart: always
        privileged: true
        environment:
          api_url: http://这里是你上面的Gateway-api能在浏览器本地请求的地址:8200/
        ports:
          - 1000:80
    
    
    

    然后指向我们的sudo docker-compose up -d

    指向完成以后我们打开我们的gateway-web的界面,并且登录进去,如果你没有设置环境变量的话默认密码是rootAa010426.

    打开我们的代理设置,添加一个集群:

    打开路由,点击添加路由,

    打开证书管理,点击添加证书:

    将我们的证书上传以后点击右上角的刷新缓存,则会生效,还需要注意将我们的域名解析到服务器当中。上面我们用的是gitea.token-ai.cn,注意的是自签证书似乎不能使用。上面操作完成以后点击我们右上角的刷新缓存,然后访问我们的https://gitea.token-ai.cn:8081,然后打开浏览器的F12,我们可以看到我们的,我们的协议除了第一个都是h3协议,这是因为第一个请求是不确定你是否支持h3所以发起一个h1或h2的协议然后,如果你的响应头响应了Alt-Svc则会下次请求使用h3,

    还需要注意的是,Alt-Svc:h3=":8081"; ma=86400的8081是前端访问的端口,这个是需要和访问端口一致,如果不一致也不会使用h3。

    注意事项

    某些浏览器不一定支持所以需要先确认浏览器是否开启QUIC

    还需要确认服务器防火墙是否开启UDP

    然后根据上面的文档一步一步来即可,或者可以加群询问群主。

    结尾

    感谢 https://www.cnblogs.com/feinian 大佬提供的quic镜像构建
    来着token的分享

    开源地址:https://gitee.com/hejiale010426/Gateway
    github: https://github.com/239573049/Gateway
    技术交流群:737776595

  • 相关阅读:
    ES6 入门教程 24 Module 的语法 24.4 import 命令 & 24.5 模块的整体加载
    理解位运算,左移、右移、与、或、非
    Docker--harbor
    全面理解NFT-Web3.0基本载体
    7.【红黑树】定义、性质、操作(查、增、删)
    Nginx:负载均衡(策略讲解+配置举例)
    LVS-DR+keepalived
    Springcloud(一):springcloud使用nacos作为注册中心和配置中心
    Nginx基础02:配置文件nginx.conf(Part1)
    Java实现Excel转PDF的两种方法总结
  • 原文地址:https://www.cnblogs.com/hejiale010426/p/17985452