# Sisk Framework documentation > Sisk is a lightweight, agnostic and robust .NET web development framework. This file contains the complete Sisk documentation (中文 (简体)). The API reference is not included; read https://docs.sisk-framework.org/api/index.md for the type index. --- # 入门 Source: https://docs.sisk-framework.org/zh-cn/docs/getting-started.html 欢迎阅读 Sisk 文档! Sisk 是一个开源的轻量级 .NET HTTP 框架。您可以使用它构建独立的 Web 服务,将 HTTP 模块嵌入现有应用程序,或在反向代理后运行服务,仅使用所需的配置。 Sisk 的价值观包括代码透明性、模块化、性能和可扩展性。它能够处理不同的应用模式,包括 RESTful API、JSON-RPC 服务、WebSocket、Server-Sent Events(服务器发送事件)以及静态文件服务。 它的主要特性包括: | 资源 | 描述 | | ------- | --------- | | [路由](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md) | 一个支持前缀、自定义方法、路径变量、值转换器等功能的路径路由器。 | | [请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md) | 也称为 *中间件*,提供接口以构建您自己的请求处理程序,可在操作前后处理请求。 | | [压缩](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) | 使用 Sisk 轻松压缩响应内容。 | | [WebSocket](https://docs.sisk-framework.org/zh-cn/docs/features/websockets.md) | 提供接受完整 WebSocket 的路由,用于读取和写入客户端。 | | [服务器发送事件](https://docs.sisk-framework.org/zh-cn/docs/features/server-sent-events.md) | 向支持 SSE 协议的客户端发送服务器事件。 | | [日志](https://docs.sisk-framework.org/zh-cn/docs/features/logging.md) | 简化的日志记录。记录错误、访问,按大小定义轮转日志,同一日志的多输出流等。 | | [多主机](https://docs.sisk-framework.org/zh-cn/docs/advanced/multi-host-setup.md) | 为多个端口提供 HTTP 服务器,每个端口拥有自己的路由器,每个路由器拥有自己的应用程序。 | | [服务器处理程序](https://docs.sisk-framework.org/zh-cn/docs/advanced/http-server-handlers.md) | 扩展您自己的 HTTP 服务器实现。通过扩展、改进和新功能进行自定义。 | ## 第一步 Sisk 可以在任何 .NET 环境中运行。在本指南中,我们将教您如何使用 .NET 创建 Sisk 应用程序。如果您尚未安装,请从 [此处](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) 下载 SDK。 在本教程中,我们将介绍如何创建项目结构、接收请求、获取 URL 参数以及发送响应。本指南将重点使用 C# 构建一个简单的服务器。您也可以使用您喜欢的编程语言。 > [!NOTE] > 您可能对快速入门项目感兴趣。请查看 [此仓库](https://github.com/sisk-http/quickstart) 获取更多信息。 ## 创建项目 我们将项目命名为 “My Sisk Application”。在您设置好 .NET 后,可以使用以下命令创建项目: ```bash dotnet new console -n my-sisk-application ``` 接下来,进入项目目录并使用 .NET 工具安装 Sisk: ```bash cd my-sisk-application dotnet add package Sisk.HttpServer ``` 您可以在[此处](https://www.nuget.org/packages/Sisk.HttpServer/)找到在项目中安装 Sisk 的其他方式。 现在,让我们创建 HTTP 服务器的实例。此示例中,我们将其配置为监听 5000 端口。 ## 构建 HTTP 服务器 Sisk 允许您手动一步一步构建应用程序,因为它会路由到 HttpServer 对象。但这对大多数项目来说可能不太方便。因此,我们可以使用构建器方法,使我们的应用更容易启动和运行。 ```csharp {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseListeningPort("http://localhost:5000/") .Build(); app.Router.MapGet("/", request => { return new HttpResponse() { Status = 200, Content = new StringContent("Hello, world!") }; }); await app.StartAsync(); } } ``` 了解 Sisk 的每个关键组件非常重要。稍后在本文档中,您将进一步了解 Sisk 的工作原理。 ## 手动(高级)设置 您可以在文档的[此章节](https://docs.sisk-framework.org/zh-cn/docs/advanced/manual-setup.md)了解每个 Sisk 机制的工作原理,其中解释了 HttpServer、Router、ListeningPort 以及其他组件之间的行为和关系。 --- # 安装 Source: https://docs.sisk-framework.org/zh-cn/docs/installing.html 您可以通过 Nuget、dotnet cli 或 [其他选项](https://www.nuget.org/packages/Sisk.HttpServer/) 安装 Sisk。您可以通过在开发者控制台中运行以下命令轻松设置 Sisk 环境: ```sh dotnet add package Sisk.HttpServer ``` 此命令将在您的项目中安装 Sisk 的最新版本。 --- # 本机 AOT 支持 Source: https://docs.sisk-framework.org/zh-cn/docs/native-aot.html [.NET Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) 允许发布本机 .NET 应用程序,这些应用程序是自给自足的,不需要在目标主机上安装 .NET 运行时。此外,Native AOT 提供诸如: - 应用程序大小大大减小 - 初始化速度大大提高 - 内存消耗降低 Sisk Framework 本质上允许几乎所有功能使用 Native AOT,而无需对源代码进行改造以适应 Native AOT。 ## 不支持的功能 然而,Sisk Framework 使用了反射,尽管很少,为某些功能提供支持。下面提到的功能可能在本机代码执行期间部分可用或完全不可用: - [自动扫描路由器模块](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.AutoScanModules.md):此资源扫描执行程序集中的嵌入类型,并注册符合 [路由器模块](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md) 的类型。此资源需要可以在程序集修剪期间排除的类型。 Sisk 中的所有其他功能都与 AOT 兼容。通常会找到一个或多个方法,这些方法会产生 AOT 警告,但相同的方法(如果未在此处提及)具有重载,指示传递类型、参数或类型信息,以帮助 AOT 编译器编译对象。 --- # 部署 Sisk 应用程序 Source: https://docs.sisk-framework.org/zh-cn/docs/deploying.html 部署 Sisk 应用程序的过程包括将项目发布到生产环境中。虽然这个过程相对简单,但有一些细节需要注意,以避免对部署的基础设施造成安全和稳定性的损害。 理想情况下,在进行了所有可能的测试后,您应该准备好将应用程序部署到云端。 ## 发布应用程序 发布 Sisk 应用程序或服务是生成生产就绪和优化的二进制文件。在这个例子中,我们将编译二进制文件以在安装了 .NET Runtime 的机器上运行。 您需要在机器上安装 .NET SDK 来构建应用程序,并在目标服务器上安装 .NET Runtime 来运行应用程序。您可以在 [这里](https://learn.microsoft.com/en-us/dotnet/core/install/linux) 学习如何在 Linux 服务器上安装 .NET Runtime,[Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows?tabs=net70) 和 [Mac OS](https://learn.microsoft.com/en-us/dotnet/core/install/macos)。 在项目所在的文件夹中,打开终端并使用 .NET 发布命令: ```shell $ dotnet publish -r linux-x64 -c Release ``` 这将在 `bin/Release/publish/linux-x64` 中生成二进制文件。 > [!NOTE] > 如果您的应用程序使用 Sisk.ServiceProvider 包,您应该将 `service-config.json` 文件复制到主机服务器上,连同 `dotnet publish` 生成的所有二进制文件。 > 您可以预先配置文件,包括环境变量、监听端口和主机,以及其他服务器配置。 下一步是将这些文件传输到将要托管应用程序的服务器。 之后,给二进制文件授予执行权限。假设我们的项目名称为 "my-app": ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` 运行应用程序后,检查是否有任何错误消息。如果没有产生错误消息,则表示应用程序正在运行。 此时,应用程序可能无法从外部网络访问,因为尚未配置访问规则,例如防火墙。我们将在下一步中考虑这一点。 您应该拥有应用程序监听的虚拟主机地址。这是手动在应用程序中设置的,并取决于您如何实例化 Sisk 服务。 如果您 **不** 使用 Sisk.ServiceProvider 包,您应该在定义 HttpServer 实例的地方找到它: ```cs HttpServer server = HttpServer.Emit(5000, out HttpServerConfiguration config, out var host, out var router); // sisk 应该监听 http://localhost:5000/ ``` 手动关联 ListeningHost: ```cs config.ListeningHosts.Add(new ListeningHost("https://localhost:5000/", router)); ``` 或者,如果您使用 Sisk.ServiceProvider 包,在您的 `service-config.json` 中: ```json { "Server": { }, "ListeningHost": { "Ports": [ "http://localhost:5000/" ] } } ``` 从这里,我们可以创建一个反向代理来监听您的服务并使流量在开放网络上可用。 ## 代理应用程序 代理服务意味着不直接将 Sisk 服务暴露在外部网络中。这是一种常见的服务器部署做法,因为: - 允许您在应用程序中关联 SSL 证书; - 创建访问规则以避免过载; - 控制带宽和请求限制; - 为应用程序分离负载均衡器; - 防止安全损害基础设施。 您可以通过反向代理如 [Nginx](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-7.0&tabs=linux-ubuntu#install-nginx) 或 [Apache](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache?view=aspnetcore-7.0) 来提供应用程序,或者使用 HTTP-over-DNS 隧道,如 [Cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/)。 另外,请记得正确解析代理的转发头,以通过 [转发解析器](https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.md) 获取客户端信息,例如 IP 地址和主机。 创建隧道、防火墙配置并运行应用程序后,下一步是创建应用程序服务。 > [!NOTE] > 在非 Windows 系统上,直接在 Sisk 服务中使用 SSL 证书是不可能的。这是 HttpListener 的实现细节,HttpListener 是 Sisk 中 HTTP 队列管理的核心模块,其实现因操作系统而异。您可以在 [将证书关联到 IIS 的虚拟主机](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis) 中使用 SSL 证书。对于其他系统,强烈推荐使用反向代理。 ## 创建服务 创建服务将使您的应用程序始终可用,即使在重启服务器实例或发生不可恢复的崩溃后。 在这个简单的教程中,我们将使用前一个教程的内容作为示例,以保持服务始终活跃。 1. 访问服务配置文件所在的文件夹: ```sh cd /etc/systemd/system ``` 2. 创建 `my-app.service` 文件并包含以下内容: ```ini {title="my-app.service"} [Unit] Description=<关于您的应用程序的描述> [Service] # 设置将启动服务的用户 User=<将启动服务的用户> # ExecStart 路径不是相对于 WorkingDirectory 的。 # 将其设置为可执行文件的完整路径 WorkingDirectory=/home/htdocs ExecStart=/home/htdocs/my-app # 设置服务在崩溃后始终重启 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ``` 3. 重启服务管理器模块: ```sh $ sudo systemctl daemon-reload ``` 4. 从文件名启动新创建的服务并检查是否正在运行: ```sh $ sudo systemctl start my-app $ sudo systemctl status my-app ``` 5. 现在,如果您的应用程序正在运行 ("Active: active"),请启用服务以在系统重启后继续运行: ```sh $ sudo systemctl enable my-app ``` 现在您已经准备好向所有人展示您的 Sisk 应用程序。 --- # 使用 SSL Source: https://docs.sisk-framework.org/zh-cn/docs/ssl.html 在需要安全性的环境中进行开发时,使用 SSL 可能是必要的,例如大多数 Web 开发场景。Sisk 基于 HttpListener 工作,而 HttpListener 不支持原生 HTTPS,只支持 HTTP。不过,有一些变通方法可以让你在 Sisk 中使用 SSL。见下文: ## 通过 Sisk.Cadente.CoreEngine - 可用平台:Linux、macOS、Windows - 难度:easy 可以在 Sisk 项目中使用实验性的 [**Cadente**](https://docs.sisk-framework.org/zh-cn/docs/cadente.md) 引擎,而无需在计算机或项目中进行额外配置。你需要在项目中安装 `Sisk.Cadente.CoreEngine` 包,才能在 Sisk 服务器中使用 Cadente 服务器。 要配置 SSL,可以使用构建器的 `UseSsl` 和 `UseEngine` 方法: ```csharp using var http = HttpServer.CreateBuilder() .UseEngine() .UseSsl(CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) ``` > 注意:此包仍处于实验阶段。 ## 通过 Windows 上的 IIS - 可用平台:Windows - 难度:medium 如果你使用 Windows,可以通过 IIS 为你的 HTTP 服务器启用 SSL。为此,建议你事先阅读 [此教程](https://docs.sisk-framework.org/zh-cn/docs/registering-namespace.md),以便在你的应用程序监听的主机不是 “localhost” 时进行相应配置。 要实现此功能,需要通过 Windows 功能安装 IIS。IIS 对 Windows 和 Windows Server 用户免费提供。要在应用程序中配置 SSL,请准备好 SSL 证书,即使是自签名证书也可以。随后,你可以查看 [如何在 IIS 7 或更高版本上设置 SSL](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis)。 ## 通过 mitmproxy - 可用平台:Linux、macOS、Windows - 难度:easy **mitmproxy** 是一种拦截代理工具,允许开发者和安全测试人员检查、修改和记录客户端(如网页浏览器)与服务器之间的 HTTP 和 HTTPS 流量。你可以使用 **mitmdump** 实用程序在客户端和 Sisk 应用之间启动反向 SSL 代理。 1. 首先,在你的机器上安装 [mitmproxy](https://mitmproxy.org/)。 2. 启动你的 Sisk 应用。此示例中,我们使用 8000 端口作为不安全的 HTTP 端口。 3. 启动 mitmproxy 服务器,在安全端口 8001 上监听: ```sh mitmdump --mode reverse:http://localhost:8000/ -p 8001 ``` 即可开始使用!你已经可以通过 `https://localhost:8001/` 访问你的应用。即使你的应用未运行,也可以启动 `mitmdump`。 另外,你可以在项目中添加对 [mitmproxy helper](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Helpers.mitmproxy) 的引用。这仍然要求在电脑上已安装 mitmproxy。 ## 通过 Sisk.SslProxy 包 - 可用平台:Linux、macOS、Windows - 难度:easy > [!IMPORTANT] > > Sisk.SslProxy 包已被 `Sisk.Cadente.CoreEngine` 包取代并不再维护。 Sisk.SslProxy 包是一种在 Sisk 应用上启用 SSL 的简易方式。但它是一个 **极其实验性的** 包。使用此包可能不够稳定,但你可以成为少数为其可行性和稳定性做出贡献的人之一。要开始使用,可以通过以下方式安装 Sisk.SslProxy 包: ```sh dotnet add package Sisk.SslProxy ``` > [!NOTE] > > 必须在 Visual Studio 包管理器中启用 “Include prerelease” 才能安装 Sisk.SslProxy。 同样,这仍是一个实验项目,切勿考虑将其投入生产环境。 目前,Sisk.SslProxy 能处理大多数 HTTP/1.1 功能,包括 HTTP Continue、Chunked-Encoding、WebSockets 和 SSE。更多关于 SslProxy 的信息请参阅 [此处](https://docs.sisk-framework.org/zh-cn/docs/extensions/ssl-proxy.md)。 --- # Cadente Source: https://docs.sisk-framework.org/zh-cn/docs/cadente.html Cadente 是 Sisk 的一个实验性的托管 HTTP/1.1 监听器实现。它作为默认的 `System.Net.HttpListener` 的替代品,提供了更大的控制和灵活性,尤其是在非 Windows 平台上。 ## 概述 默认情况下,Sisk 使用 `HttpListener` (来自 `System.Net`) 作为其底层 HTTP 服务器引擎。虽然 `HttpListener` 在 Windows 上是稳定和高性能的(因为它使用内核模式的 HTTP.sys 驱动程序),但其在 Linux 和 macOS 上的实现是托管的,并且历史上存在一些限制,例如缺乏本地 SSL 支持(需要反向代理,如 Nginx 或 Sisk.SslProxy)和不同的性能特征。 Cadente 的目标是通过提供一个完全托管的 HTTP/1.1 服务器(用 C# 编写)来解决这些问题。其主要目标是: - **本地 SSL 支持:** 在所有平台上无需外部代理或复杂配置即可工作。 - **跨平台一致性:** 在 Windows、Linux 和 macOS 上具有相同的行为。 - **性能:** 设计为高性能的替代品,以替代托管的 `HttpListener`。 - **独立性:** 与 `System.Net.HttpListener` 解耦,隔离 Sisk 免受该组件在 .NET 中可能的未来弃用或缺乏维护的影响。 > [!WARNING] > **实验状态** > > Cadente 目前处于实验阶段(Beta)。不建议在关键的生产环境中使用。API 和行为可能会改变。 ## 安装 Cadente 作为一个单独的包提供。要将其用于 Sisk,您需要 `Sisk.Cadente.CoreEngine` 包。 ```bash dotnet add package Sisk.Cadente.CoreEngine --prerelease ``` ## 使用 Sisk 要将 Cadente 用作 Sisk 应用程序的 HTTP 引擎,您需要配置 `HttpServer` 以使用 `CadenteHttpServerEngine` 代替默认引擎。 `CadenteHttpServerEngine` 将 Cadente 的 `HttpHost` 适配到 Sisk 所需的 `HttpServerEngine` 抽象。 ```csharp using Sisk.Core.Http; using Sisk.Cadente.CoreEngine; using var host = HttpServer.CreateBuilder() .UseEngine() .UseSsl(certificate: CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) .Build(); await host.StartAsync(); ``` ### 高级配置 您可以通过将设置操作传递给 `CadenteHttpServerEngine` 构造函数来自定义底层的 `HttpHost` 实例。这对于配置超时或其他低级设置很有用。 ```csharp using var engine = new CadenteHttpServerEngine(host => { // 配置客户端读/写超时 host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## 独立使用 虽然 Cadente 主要设计用于 Sisk,但也可以将其用作独立的 HTTP 服务器(类似于 `HttpListener`)。 ```csharp using Sisk.Cadente; var host = new HttpHost(15000) { Handler = new MyHostHandler() }; host.Start(); Thread.Sleep(-1); class MyHostHandler : HttpHostHandler { public override async Task OnContextCreatedAsync(HttpHost host, HttpHostContext context) { context.Response.StatusCode = 200; using var writer = new StreamWriter(context.Response.GetResponseStream()); await writer.WriteLineAsync("Hello, world!"); } } ``` --- # 在 Windows 上配置命名空间保留 Source: https://docs.sisk-framework.org/zh-cn/docs/registering-namespace.html > [!NOTE] > 此配置是可选的,仅在您希望 Sisk 在 Windows 上使用 HttpListener 引擎监听除 “localhost” 之外的主机时才需要。 Sisk 使用 HttpListener 网络接口,该接口将虚拟主机绑定到系统以监听请求。 在 Windows 上,此绑定有些限制,只允许将 localhost 绑定为有效主机。当尝试监听其他主机时,服务器会抛出访问被拒绝错误。本教程说明如何授予授权,以便在系统上监听任意您想要的主机。 ```bat {title="Namespace Setup.bat"} @echo off :: 在此插入前缀,不带空格或引号 SET PREFIX= SET DOMAIN=%ComputerName%\%USERNAME% netsh http add urlacl url=%PREFIX% user=%DOMAIN% pause ``` 在 `PREFIX` 中,填写服务器将监听的前缀(“监听主机->端口”)。它必须使用 URL 方案、主机、端口,并在末尾加上斜杠,例如: ```bat {title="Namespace Setup.bat"} SET PREFIX=http://my-application.example.test/ ``` 这样您就可以在应用程序中通过以下方式进行监听: ```csharp {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseListeningPort("http://my-application.example.test/") .Build(); app.Router.MapGet("/", request => { return new HttpResponse() { Status = 200, Content = new StringContent("Hello, world!") }; }); await app.StartAsync(); } } ``` --- # Changelogs Source: https://docs.sisk-framework.org/zh-cn/docs/changelogs.html 每对 Sisk 进行的更改都会通过更改日志记录。你可以在 [这里](https://github.com/sisk-http/archive/tree/master/changelogs) 查看所有 Sisk 版本的更改日志。 --- # 常见问题 Source: https://docs.sisk-framework.org/zh-cn/docs/faq.html 关于 Sisk 的常见问题。 ## Sisk 是开源的吗? 完全开源。Sisk 使用的所有源代码都已发布并经常在 [GitHub](https://github.com/sisk-http) 上更新。 ## 是否接受贡献? 只要贡献符合 [Sisk 哲学](/),所有贡献都非常欢迎!贡献不仅限于代码!您可以通过文档、测试、翻译、捐款和帖子等方式贡献。 ## Sisk 是否有资金支持? 不。目前没有任何组织或项目为 Sisk 提供资金支持。 ## 我可以在生产环境中使用 Sisk 吗? 绝对可以。该项目已经开发超过三年,并在商业应用中进行了大量测试,这些应用自那时起就已投入生产。Sisk 被用作重要的商业项目的主要基础设施。 已经编写了一份关于如何在不同系统和环境中 [部署](https://docs.sisk-framework.org/zh-cn/docs/deploying.md) 的指南,并且可用。 ## Sisk 是否具有身份验证、监控和数据库服务? 不。Sisk 不具备这些功能。它是一个用于开发 HTTP 网络应用程序的框架,但它仍然是一个最小的框架,仅提供应用程序运行所需的功能。 您可以使用任何第三方库来实现所需的所有服务。Sisk 的设计初衷是中立、灵活和与任何东西一起工作。 ## 为什么我应该使用 Sisk 而不是 <框架>? 我不知道。你告诉我。 Sisk 是为满足 .NET 中的 HTTP 网络应用程序的通用场景而创建的。已建立的项目,例如 ASP.NET,解决了各种问题,但具有不同的偏见。与较大的框架不同,Sisk 需要用户了解他们正在做什么和正在构建什么。网络开发和 HTTP 协议的基本概念对于使用 Sisk 至关重要。 Sisk 更接近 Node.js 的 Express,而不是 ASP.NET Core。它是一个高级抽象,允许您创建具有所需 HTTP 逻辑的应用程序。 ## 学习 Sisk 需要什么? 您需要: - 网络开发(HTTP、Restful 等) - .NET 仅此而已。只要您对这两个主题有基本的了解,您就可以花几小时时间开发一个使用 Sisk 的高级应用程序。 ## 我可以使用 Sisk 开发商业应用程序吗? 绝对可以。 Sisk是在 MIT 许可下创建的,这意味着您可以在任何商业项目中使用 Sisk,无论是商业还是非商业项目,都无需专有许可。 我们要求的是,在您的应用程序中,您需要有一个关于在您的项目中使用的开源项目的通知,并且 Sisk 在其中。 --- # 路由 Source: https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.html The [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) 是构建服务器的第一步。它负责保存 [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) 对象,这些对象是将 URL 及其方法映射到服务器执行的操作的端点。每个操作负责接收请求并向客户端返回响应。 路由是路径表达式(“路径模式”)与它们可以监听的 HTTP 方法的配对。当向服务器发出请求时,服务器会尝试找到匹配该请求的路由,然后调用该路由的操作并将产生的响应返回给客户端。 在 Sisk 中定义路由有多种方式:可以是静态的、动态的或自动扫描的,使用属性定义,或直接在 Router 对象中定义。 ```cs Router mainRouter = new Router(); // 将 GET / 路由映射到以下操作 mainRouter.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); ``` 要了解路由能够做什么,需要先了解请求能够做什么。一个 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 包含了你所需的一切。Sisk 还提供了一些额外功能,以加快整体开发。 对于服务器接收到的每个操作,都会调用类型为 [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md) 的委托。该委托包含一个参数,持有一个包含所有关于服务器接收的请求的必要信息的 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md)。该委托返回的对象必须是 [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) 或通过 [implicit response types](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/responses.md#implicit-response-types) 映射到它的对象。 ## 匹配路由 当 HTTP 服务器收到请求时,Sisk 会搜索满足请求路径表达式的路由。该表达式始终在路由和请求路径之间进行测试,不考虑查询字符串。 此测试没有优先级,并且仅针对单一路由。当没有路由与该请求匹配时,返回 [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) 响应给客户端。当路径模式匹配但 HTTP 方法不匹配时,返回 [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) 响应给客户端。 Sisk 会检查路由冲突的可能性以避免这些问题。定义路由时,Sisk 会查找可能与正在定义的路由冲突的路由。此测试包括检查路径和路由设置接受的方法。 ### 使用路径模式创建路由 对于新应用,优先使用 `Map*` 方法。它们在调用点保持 HTTP 方法可见,并匹配当前的 `Router` API。较旧的 `SetRoute` 方法仍作为兼容包装存在,但新示例应使用 `Map`、`MapGet`、`MapPost`、`MapPut`、`MapDelete`、`MapPatch`、`MapAny`、`MapOptions` 或 `MapHead`。 ```cs // Map* 方法是定义特定 HTTP 方法路由的常用方式。 mainRouter.MapGet("/hey/", (request) => { string name = request.RouteParameters["name"].GetString(); return new HttpResponse($"Hello, {name}"); }); mainRouter.MapPost("/form", (request) => { var formData = request.GetFormContent(); return new HttpResponse(); // 空的 200 OK }); // 当需要路由选项时,Map 也可以接收 Route 实例。 mainRouter.Map(Route.Get("/image.png", (request) => { var imageStream = File.OpenRead("image.png"); return new HttpResponse() { // StreamContent 内部 // 流在发送后会被释放 // 响应。 Content = new StreamContent(imageStream) }; })); // 多个参数 mainRouter.MapGet("/hey//surname/", (request) => { string name = request.RouteParameters["name"].GetString(); string surname = request.RouteParameters["surname"].GetString(); return new HttpResponse($"Hello, {name} {surname}!"); }); ``` [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) 属性包含了收到请求的路径变量的所有信息。 服务器接收到的每个路径在执行路径模式测试之前都会被规范化,遵循以下规则: - 所有空的路径段都会被移除,例如:`////foo//bar` 会变成 `/foo/bar`。 - 路径匹配是**区分大小写**的,除非 [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) 被设置为 `true`。 [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) 和 [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) 属性返回一个 [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md) 对象,其中每个索引属性返回一个非空的 [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md),可用作 option/monad 将其原始值转换为受管理的对象。 下面的示例读取路由参数 “id” 并从中获取一个 `Guid`。如果参数不是有效的 Guid,则会抛出异常;如果服务器未处理 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md),则会向客户端返回 500 错误。 ```cs mainRouter.MapGet("/user/", (request) => { Guid id = request.RouteParameters["id"].GetGuid(); return new HttpResponse($"User id: {id}"); }); ``` > [!NOTE] > 路径的尾部 `/` 在请求和路由路径中都会被忽略,也就是说,如果你尝试访问定义为 `/index/page` 的路由,也可以使用 `/index/page/` 进行访问。 > > 你也可以通过启用 [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) 来强制 URL 以 `/` 结尾。 ### 使用类实例创建路由 你也可以使用属性 [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md) 通过反射动态定义路由。这样,类的实例中实现了该属性的方法将在目标路由器中定义其路由。 要将方法定义为路由,必须使用 [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md) 标记,例如该属性本身或 [RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md)。方法可以是 static、实例、public 或 private。需要从对象映射实例和静态路由方法时使用 `MapInstance`。只想从类型映射静态路由方法时使用 `MapType`。 ```cs {title="Controller/MyController.cs"} public class MyController { // 将匹配 GET / [RouteGet] HttpResponse Index(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Index!"); return res; } // 静态方法也适用 [RouteGet("/hello")] static HttpResponse Hello(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Hello world!"); return res; } } ``` 下面的代码会将 `MyController` 的 `Index` 和 `Hello` 方法都定义为路由,因为两者都被标记为路由,并且提供了类的实例而不是类型。如果提供的是类型,则只会定义静态方法。 ```cs var myController = new MyController(); mainRouter.MapInstance(myController); ``` 若只想映射类型的静态路由方法,使用: ```cs mainRouter.MapType(); ``` 自 Sisk 0.16 版本起,可以启用 AutoScan,自动搜索实现 `RouterModule` 的用户自定义类并将其自动关联到路由器。AOT 编译不支持此功能。 ```cs mainRouter.AutoScanModules(); ``` 上述指令会搜索所有实现 `ApiController` 的类型,但**不包括该类型本身**。两个可选参数指示该方法如何搜索这些类型。第一个参数表示搜索这些类型的程序集,第二个参数指示这些类型的定义方式。 ## 正则路由 如果不想使用默认的 HTTP 路径匹配方法,可以将路由标记为使用正则表达式解释。 ```cs Route indexRoute = new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", IndexPage); mainRouter.Map(indexRoute); ``` 或使用 [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) 类: ```cs mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request => { return new HttpResponse("hello, world"); })); ``` 你还可以将正则模式中的捕获组写入 [HttpRequest.RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) 内容: ```cs {title="Controller/MyController.cs"} public class MyController { [RegexRoute(RouteMethod.Get, @"/uploads/(?.*\.(jpeg|jpg|png))")] static HttpResponse RegexRoute(HttpRequest request) { string filename = request.RouteParameters["filename"].GetString(); return new HttpResponse().WithContent($"Acessing file {filename}"); } } ``` ## 前缀路由 你可以使用 [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) 属性为类或模块中的所有路由添加前缀,并将前缀设为字符串。 下面的示例使用 BREAD 架构(Browse、Read、Edit、Add 和 Delete): ```cs {title="Controller/Api/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController { // GET /api/users [RouteGet] public async Task Browse() { ... } // GET /api/users/ [RouteGet("/")] public async Task Read() { ... } // PATCH /api/users/ [RoutePatch("/")] public async Task Edit() { ... } // POST /api/users [RoutePost] public async Task Add() { ... } // DELETE /api/users/ [RouteDelete("/")] public async Task Delete() { ... } } ``` 在上述示例中,省略了 HttpResponse 参数,转而通过全局上下文 [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md) 使用。更多内容请参见下节。 ## 没有请求参数的路由 路由可以在没有 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 参数的情况下定义,并仍然能够在请求上下文中获取请求及其组件。这里考虑一个抽象类 `ControllerBase`,它作为 API 所有控制器的基础,并提供 `Request` 属性以获取当前的 [HttpRequest]。 ```cs {title="Controller/ControllerBase.cs"} public abstract class ControllerBase { // 从当前线程获取请求 public HttpRequest Request { get => HttpContext.Current.Request; } // 以下代码在调用时,从当前 HTTP 会话获取数据库,若不存在则创建一个新的 public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd(); } } ``` 并让所有子类能够在不传入请求参数的情况下使用路由语法: ```cs {title="Controller/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController : ControllerBase { [RoutePost] public async Task Create() { // 从当前请求读取 JSON 数据 UserCreationDto? user = await Request.GetJsonContentAsync(); ... Database.Users.Add(user); return new HttpResponse(201); } } ``` 更多关于当前上下文和依赖注入的细节,请参见 [dependency injection](https://docs.sisk-framework.org/zh-cn/docs/features/instancing.md) 教程。 ## 任意方法路由 你可以定义仅通过路径匹配而跳过 HTTP 方法的路由。这在路由回调内部进行方法验证时非常有用。 ```cs // 将匹配任意 HTTP 方法的 / mainRouter.MapAny("/", callbackFunction); ``` ## 任意路径路由 任意路径路由会测试 HTTP 服务器收到的任何路径,前提是路由方法也被测试。如果路由方法是 `RouteMethod.Any` 且路由在路径表达式中使用了 [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md),则该路由将监听 HTTP 服务器的所有请求,且不能再定义其他路由。 ```cs // 以下路由将匹配所有 POST 请求 mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction); ``` ## 忽略大小写的路由匹配 默认情况下,路由与请求的解释是区分大小写的。要使其忽略大小写,请启用此选项: ```cs mainRouter.MatchRoutesIgnoreCase = true; ``` 这也会为使用正则匹配的路由启用 `RegexOptions.IgnoreCase` 选项。 ## 未找到 (404) 回调处理程序 你可以为请求未匹配到任何已知路由时创建自定义回调。 ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // 自 v0.14 起 Content = new HtmlContent("

Not found

") // 旧版本 Content = new StringContent("

Not found

", Encoding.UTF8, "text/html") }; }; ``` ## 方法不允许 (405) 回调处理程序 你也可以为请求匹配路径但不匹配方法时创建自定义回调。 ```cs mainRouter.MethodNotAllowedErrorHandler = (context) => { return new HttpResponse(405) { Content = new StringContent($"Method not allowed for this route.") }; }; ``` ## 错误处理 在请求生命周期内(从前置执行请求处理程序、通过路由操作、到后置执行请求处理程序和数值处理程序)可能会抛出异常。这些异常由以下机制管理: - 如果 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 为 `true`,异常会正常抛出且不会被 Sisk 捕获,如果异常未被捕获,HTTP 服务器可能会中断。 - 如果 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 为 `false`,异常会被 Sisk 捕获并处理。随后,如果已定义 `Router.CallbackErrorHandler`,它将使用捕获的异常和请求上下文被调用,并且**不会**转发到标准错误输出。如果未定义 `Router.CallbackErrorHandler`,异常将转发到标准错误输出,客户端将收到 HTTP 500 错误响应。如果未定义标准错误输出,错误将被静默忽略。 注意:在 `Router.CallbackErrorHandler` 中,你可以设置错误日志、访问日志、两者或都不记录的日志模式,并修改默认的日志写入行为: ```csharp router.CallbackErrorHandler = (ex, ctx) => { ctx.LogMode = LogOutput.Both; // 覆盖日志模式,使错误同时记录在访问日志和错误日志中 } ``` ## 内部错误处理程序 路由回调在服务器执行期间可能抛出错误。如果未正确处理,HTTP 服务器的整体功能可能会被终止。路由器提供了一个回调,用于在路由回调失败时防止服务中断。 此方法仅在 [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 设置为 false 时可达。 ```cs mainRouter.CallbackErrorHandler = (ex, context) => { return new HttpResponse(500) { Content = new StringContent($"Error: {ex.Message}") }; }; ``` --- # 请求处理 Source: https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.html 请求处理程序,也称为“中间件”,是在路由器上执行请求之前或之后运行的函数。它们可以在每个路由或每个路由器上定义。 请求处理程序有两种类型: - **BeforeResponse**:定义请求处理程序将在调用路由器操作之前执行。 - **AfterResponse**:定义请求处理程序将在调用路由器操作之后执行。在此上下文中发送 HTTP 响应将覆盖路由器的操作响应。 两种请求处理程序都可以覆盖实际的路由器回调函数响应。顺便说一下,请求处理程序可用于验证请求,例如身份验证、内容或任何其他信息,如存储信息、日志或其他可在响应前后执行的步骤。 ![](https://docs.sisk-framework.org/assets/img/requesthandlers1.png) 通过这种方式,请求处理程序可以中断所有执行并在完成循环之前返回响应,丢弃过程中的其他所有内容。 示例:假设用户身份验证请求处理程序未对其进行身份验证。它将阻止请求生命周期继续并导致挂起。如果这种情况发生在第二个位置的请求处理程序中,则第三个及之后的处理程序将不会被评估。 ![](https://docs.sisk-framework.org/assets/img/requesthandlers2.png) ## 创建请求处理程序 要创建请求处理程序,我们可以创建一个继承 [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) 接口的类,格式如下: ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { // 返回 null 表示请求循环可以继续 return null; } else { // 返回 HttpResponse 对象表示此响应将覆盖相邻的响应。 return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` 在上面的示例中,我们指出如果请求中存在 `Authorization` 头部,则应继续执行,下一个请求处理程序或路由器回调将被调用,以后者为准。如果请求处理程序通过其属性 [ExecutionMode](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.ExecutionMode.md) 在响应之后执行并返回非 null 值,它将覆盖路由器的响应。 每当请求处理程序返回 `null` 时,表示请求必须继续,调用下一个对象,或循环以路由器的响应结束。 如果你继承内置的 [RequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandler.md) 类,可以返回 `Next()` 来明确表达此意图: ```cs public class AuthenticateUserRequestHandler : RequestHandler { public override HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization is not null) return Next(); return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } ``` 对于需要 I/O 的处理程序,继承自 [AsyncRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.AsyncRequestHandler.md): ```cs public class LoadUserRequestHandler : AsyncRequestHandler { public override async Task ExecuteAsync(HttpRequest request, HttpContext context) { var user = await UserRepository.FindAsync(request.Headers.Authorization, request.DisconnectToken); if (user is null) return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); request.Bag.Set(user); return Next(); } } ``` 也可以使用 `RequestHandler.Create` 或 `AsyncRequestHandler.Create` 创建小型内联处理程序: ```cs var requireJson = RequestHandler.Create((request, context) => { if (request.Headers.ContentType?.Contains("application/json") == true) return null; return new HttpResponse(System.Net.HttpStatusCode.UnsupportedMediaType); }); ``` ## 将请求处理程序关联到单个路由 你可以为路由定义一个或多个请求处理程序。 ```cs {title="Router.cs"} mainRouter.Map(RouteMethod.Get, "/", IndexPage, new IRequestHandler[] { new AuthenticateUserRequestHandler(), // before request handler new ValidateJsonContentRequestHandler(), // before request handler // -- method IndexPage will be executed here new WriteToLogRequestHandler() // after request handler }); ``` 或者创建一个 [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) 对象: ```cs {title="Router.cs"} Route indexRoute = Route.Get("/", IndexPage); indexRoute.RequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; mainRouter.Map(indexRoute); ``` ## 将请求处理程序关联到路由器 你可以定义一个全局请求处理程序,它将在路由器的所有路由上运行。 ```cs {title="Router.cs"} mainRouter.GlobalRequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; ``` ## 将请求处理程序关联到属性 你可以在方法属性上与路由属性一起定义请求处理程序。 ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { return new HttpResponse() { Content = new StringContent("Hello world!") }; } } ``` 请注意,需要传递所需的请求处理程序类型,而不是对象实例。这样,请求处理程序将由路由器解析器实例化。你可以使用 [ConstructorArguments](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandlerAttribute.ConstructorArguments.md) 属性在类构造函数中传递参数。 示例: ```cs {title="Controller/MyController.cs"} [RequestHandler("arg1", 123, ...)] public HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` 你也可以创建实现 RequestHandler 的自定义属性: ```cs {title="Middleware/Attributes/AuthenticateAttribute.cs"} public class AuthenticateAttribute : RequestHandlerAttribute { public AuthenticateAttribute() : base(typeof(AuthenticateUserRequestHandler), ConstructorArguments = new object?[] { "arg1", 123, ... }) { ; } } ``` 并像下面这样使用: ```cs {title="Controller/MyController.cs"} [Authenticate] static HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` ## 绕过全局请求处理程序 在路由上定义全局请求处理程序后,你可以在特定路由上忽略此请求处理程序。 ```cs {title="Router.cs"} var myRequestHandler = new AuthenticateUserRequestHandler(); mainRouter.GlobalRequestHandlers = new IRequestHandler[] { myRequestHandler }; Route publicRoute = Route.Get("/", IndexPage); publicRoute.Name = "My route"; publicRoute.BypassGlobalRequestHandlers = new IRequestHandler[] { myRequestHandler, // ok: the same instance of what is in the global request handlers new AuthenticateUserRequestHandler() // wrong: will not skip the global request handler }; mainRouter.Map(publicRoute); ``` > [!NOTE] > 如果要绕过请求处理程序,必须使用之前实例化的相同引用来跳过。创建另一个请求处理程序实例将不会跳过全局请求处理程序,因为其引用会改变。请记住在 GlobalRequestHandlers 和 BypassGlobalRequestHandlers 中使用相同的请求处理程序引用。 --- # 请求 Source: https://docs.sisk-framework.org/zh-cn/docs/fundamentals/requests.html 请求是表示 HTTP 请求消息的结构体。[HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 对象包含了在整个应用程序中处理 HTTP 消息的实用函数。 一个 HTTP 请求由方法、路径、版本、头部和正文组成。 在本文档中,我们将教您如何获取这些元素。 ## 获取请求方法 要获取收到的请求的方法,可以使用 Method 属性: ```cs static HttpResponse Index(HttpRequest request) { HttpMethod requestMethod = request.Method; ... } ``` 此属性返回由 [HttpMethod](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpmethod) 对象表示的请求方法。 > [!NOTE] > 与路由方法不同,此属性不提供 [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 项。相反,它返回真实的请求方法。 ## 获取请求 URL 组件 您可以通过请求的某些属性获取 URL 的各种组件。以下示例使用的 URL 为: ``` http://localhost:5000/user/login?email=foo@bar.com ``` | 组件名称 | 描述 | 组件值 | | --- | --- | --- | | [Path](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Path.md) | 获取请求路径。 | `/user/login` | | [FullPath](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullPath.md) | 获取请求路径和查询字符串。 | `/user/login?email=foo@bar.com` | | [FullUrl](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullUrl.md) | 获取完整的 URL 请求字符串。 | `http://localhost:5000/user/login?email=foo@bar.com` | | [Host](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Host.md) | 获取请求主机。 | `localhost` | | [Authority](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Authority.md) | 获取请求主机和端口。 | `localhost:5000` | | [QueryString](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.QueryString.md) | 获取请求查询字符串。 | `?email=foo@bar.com` | | [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) | 以命名值集合的形式获取请求查询。 | `{StringValueCollection object}` | | [IsSecure](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsSecure.md) | 判断请求是否使用 SSL(true)或未使用(false)。 | `false` | 您也可以使用 [HttpRequest.Uri](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Uri.md) 属性,它将上述所有信息合并在一个对象中。 ## 请求元数据和取消 Sisk 还会为每个请求附加操作元数据。这些属性对日志、追踪、本地化、诊断以及长时间运行的操作非常有用: | 属性或方法 | 用途 | | --- | --- | | [RequestId](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestId.md) | 请求的唯一标识符。启用 [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) 可将其作为 `X-Request-Id` 返回。 | | [RequestedAt](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestedAt.md) | Sisk 创建请求对象的时间点。 | | [RemoteAddress](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RemoteAddress.md) | 从连接解析得到的客户端地址,或来自您的 [ForwardingResolver](https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.md)。 | | [Culture](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Culture.md) | 从 `Accept-Language` 解析得到的最佳语言区域,若未匹配则回退到当前语言区域。 | | [DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) | 当客户端断开连接时(如果配置的 HTTP 引擎支持)发出的取消令牌。 | | [Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) | 在请求处理程序和路由操作之间共享的键/值存储。 | | [GetRawHttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRawHttpRequest.md) | 用于诊断的请求文本表示。 | ## 获取请求正文 某些请求包含正文,例如表单、文件或 API 事务。您可以通过以下属性获取请求正文: ```cs // 将请求正文作为字符串获取,使用请求的编码作为解码器 string body = request.Body; // 或者获取字节数组 byte[] bodyBytes = request.RawBody; // 或者直接流式读取 Stream requestStream = request.GetRequestStream(); // 或者异步读取正文 Memory bodyMemory = await request.GetBodyContentsAsync(); ``` 也可以通过属性 [HasContents](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.HasContents.md) 判断请求是否有正文,以及通过 [IsContentAvailable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsContentAvailable.md) 判断 HTTP 服务器是否已完整接收远端的内容。 `GetRequestStream` 只能读取一次。如果使用此方法读取,`RawBody` 和 `Body` 的值也将不可用。请求流在请求上下文结束时会自动释放,无需手动 `Dispose`。此外,您可以使用 [HttpRequest.RequestEncoding](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestEncoding.md) 属性获取用于手动解码请求的最佳编码。 服务器对读取请求内容有大小限制,这同样适用于 [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) 和 [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md)。这些属性会将整个输入流复制到本地缓冲区,大小等同于 [HttpRequest.ContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.ContentLength.md)。 如果发送的内容超过用户配置的 [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md),服务器会返回 413 Content Too Large 响应。若未配置限制或限制过大,当客户端发送的内容超过 [Int32.MaxValue](https://learn.microsoft.com/en-us/dotnet/api/system.int32.maxvalue)(约 2 GB)且尝试通过上述属性访问时,服务器会抛出 [OutOfMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.outofmemoryexception?view=net-8.0)。此时仍可通过流式方式处理内容。 > [!NOTE] > 虽然 Sisk 允许这样做,但始终建议遵循 HTTP 语义,在不允许的请求方法中不要获取或提供内容。请阅读 [RFC 9110 “HTTP Semantics”](https://httpwg.org/spec/rfc9110.html)。 ## 读取 JSON 请求 对于 JSON API,建议使用内置的 JSON 辅助方法,而不是手动读取 `Body` 并反序列化。它们使用 [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) 并默认使用 [HttpRequest.DefaultJsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DefaultJsonSerializerOptions.md)。 ```cs public record CreateUserRequest(string Name, string Email); router.MapPost("/users", (HttpRequest request) => { CreateUserRequest? body = request.GetJsonContent(); if (body is null) return new HttpResponse(System.Net.HttpStatusCode.BadRequest); return new HttpResponse(System.Net.HttpStatusCode.Created); }); ``` 在已经是异步路由或希望在取消时停止反序列化的情况下,使用异步重载: ```cs router.MapPost("/users", async (HttpRequest request) => { CreateUserRequest? body = await request.GetJsonContentAsync(request.DisconnectToken); if (body is null) return new HttpResponse(System.Net.HttpStatusCode.BadRequest); return new HttpResponse(System.Net.HttpStatusCode.Created); }); ``` 您可以为特定端点传入自定义的 [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions): ```cs var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true }; UserDto? user = request.GetJsonContent(options); ``` 对于 Native AOT 或对裁剪敏感的应用程序,使用由 `JsonSerializerContext` 生成的 `JsonTypeInfo` 重载: ```cs [JsonSerializable(typeof(CreateUserRequest))] public partial class AppJsonSerializerContext : JsonSerializerContext { } CreateUserRequest? body = await request.GetJsonContentAsync( AppJsonSerializerContext.Default.CreateUserRequest, request.DisconnectToken); ``` JSON 辅助方法同样遵循“一次读取”规则:在 Sisk 通过 `GetJsonContent`、`GetJsonContentAsync`、`Body` 或 `RawBody` 读取请求流后,您不能再通过 `GetRequestStream()` 读取相同的正文。 ## 获取请求上下文 HTTP Context 是 Sisk 专有的对象,用于存储 HTTP 服务器、路由、路由器和请求处理程序的信息。它可以帮助您在对象难以组织的环境中保持清晰。 您可以使用静态方法 `HttpContext.GetCurrentContext()` 获取当前正在执行的 [HttpContext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md)。该方法返回当前线程正在处理的请求的上下文。 ```cs HttpContext context = HttpContext.GetCurrentContext(); ``` ### 日志模式 [HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) 属性允许您控制当前请求的日志行为。您可以为特定请求启用或禁用日志,覆盖默认的服务器配置。 ```cs // 为此请求禁用日志 context.LogMode = LogOutputMode.None; ``` ### 请求包 [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) 对象保存了从一个请求处理程序传递到另一个点的信息,并可在最终目的地消费。该对象也可被在路由回调之后运行的请求处理程序使用。 > [!TIP] > 此属性也可以通过 [HttpRequest.Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) 访问。 ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public string Identifier { get; init; } = Guid.NewGuid().ToString(); public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { context.RequestBag.Add("AuthenticatedUser", new User("Bob")); return null; } else { return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` 上述请求处理程序会在请求包中定义 `AuthenticatedUser`,随后可在最终回调中使用: ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { User authUser = request.Context.RequestBag["AuthenticatedUser"]; return new HttpResponse() { Content = new StringContent($"Hello, {authUser.Name}!") }; } } ``` 您也可以使用 `Bag.Set()` 与 `Bag.Get()` 辅助方法按类型单例获取或设置对象。 `TypedValueDictionary` 类同样提供 `GetValue` 与 `SetValue` 方法以获得更细粒度的控制。 ```cs {title="Middleware/Authenticate.cs"} public class Authenticate : RequestHandler { public override HttpResponse? Execute(HttpRequest request, HttpContext context) { request.Bag.Set(authUser); } } ``` ```csharp {title="Controller/MyController.cs"} [RouteGet("/")] [RequestHandler] public static HttpResponse GetUser(HttpRequest request) { var user = request.Bag.Get(); ... } ``` ## 获取表单数据 您可以使用下面的示例将表单数据获取为 [StringKeyStoreCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringKeyStoreCollection.md): ```cs {title="Controller/Auth.cs"} [RoutePost("/auth")] public HttpResponse Index(HttpRequest request) { var form = request.GetFormContent(); string? username = form["username"]; string? password = form["password"]; if (AttempLogin(username, password)) { ... } } ``` 当请求正文可能较大或需要取消支持时,使用异步版本: ```cs var form = await request.GetFormContentAsync(request.DisconnectToken); ``` ## 获取 multipart 表单数据 Sisk 的 HTTP 请求允许您获取上传的 multipart 内容,例如文件、表单字段或任何二进制内容。 ```cs {title="Controller/Auth.cs"} [RoutePost("/upload-contents")] public HttpResponse Index(HttpRequest request) { // 以下方法将整个请求输入读取为 // MultipartObject 数组 var multipartFormDataObjects = request.GetMultipartFormContent(); foreach (MultipartObject uploadedObject in multipartFormDataObjects) { // Multipart 表单数据提供的文件名。 // 若对象不是文件则返回 null。 Console.WriteLine("File name : " + uploadedObject.Filename); // multipart 表单数据对象的字段名。 Console.WriteLine("Field name : " + uploadedObject.Name); // multipart 表单数据的内容长度。 Console.WriteLine("Content length : " + uploadedObject.ContentLength); // 根据文件头部判断图像格式(针对已知的内容类型)。 // 若内容不是已识别的常见文件格式,则此方法返回 // MultipartObjectCommonFormat.Unknown Console.WriteLine("Common format : " + uploadedObject.GetCommonFileFormat()); } } ``` 在异步路由中使用 [GetMultipartFormContentAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContentAsync.md): ```cs var multipartFormDataObjects = await request.GetMultipartFormContentAsync(request.DisconnectToken); ``` 您可以进一步阅读 Sisk 的 [Multipart form objects](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) 以及其方法、属性和功能。 ## 检测客户端断开 自 Sisk v1.15 起,框架通过 [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) 提供取消令牌。当配置的 HTTP 引擎支持断开检测时,若客户端在响应完成前关闭连接,该令牌会被取消。这对于在客户端不再等待结果时停止长时间运行的操作非常有用。 ```csharp router.MapGet("/connect", async (HttpRequest req) => { // 从请求获取断开令牌 var dc = req.DisconnectToken; await LongOperationAsync(dc); return new HttpResponse(); }); ``` 该令牌并非所有 HTTP 引擎都兼容,每个引擎都需要相应实现。 默认的基于 `System.Net.HttpListener` 的 Sisk 引擎不支持客户端断开检测。使用默认引擎时,`DisconnectToken` 为 `CancellationToken.None`;实际上它是一个不可取消的令牌,应视为不可用。 [Cadente 引擎](https://docs.sisk-framework.org/zh-cn/docs/cadente.md) 支持 `DisconnectToken`。如果您的路由依赖断开感知的取消,请使用 Cadente 或其他明确实现此行为的引擎。即使使用支持的引擎,取消也是协作式的:将令牌传递给异步 API 并在自己的长时间运行工作中检查它。 ## Server‑sent events 支持 Sisk 支持 [Server‑sent events](https://developer.mozilla.org/en-US/docs/cn/Web/API/Server-sent_events),允许以流的方式发送块并保持服务器与客户端之间的连接。 调用 [HttpRequest.GetEventSource](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) 方法会将 HttpRequest 置于监听状态。此时该 HTTP 请求的上下文不再期待 HttpResponse,因为它会与服务器端事件发送的包交叉。 发送完所有包后,回调必须返回 [Close](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequestEventSource.Close.md) 方法,以向服务器发送最终响应并指示流已结束。 无法预知所有将要发送的包的总长度,因此无法使用 `Content‑Length` 头部来确定连接结束。 大多数浏览器默认情况下,服务器端事件不支持发送除 GET 方法之外的 HTTP 头或方法。因此,在使用需要特定请求头的 event‑source 请求时需格外小心,因为它们可能不会携带这些头。 此外,大多数浏览器在客户端未调用 [EventSource.close](https://developer.mozilla.org/en-US/docs/cn/Web/API/EventSource/close) 方法时会重新启动流,这会导致服务器端产生无限的额外处理。为避免此类问题,通常会发送一个最终包,指示事件源已完成所有包的发送。 下面的示例展示了浏览器如何与支持 Server‑side events 的服务器通信。 ```html {title="sse-example.html"} Fruits:
    ``` 随后逐步向客户端发送消息: ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/event-source")] public async Task ServerEventsResponse(HttpRequest request) { var serverEvents = await request.GetEventSourceAsync (); string[] fruits = new[] { "Apple", "Banana", "Watermelon", "Tomato" }; foreach (string fruit in fruits) { await serverEvents.SendAsync(fruit); await Task.Delay(1500); } return await serverEvents.CloseAsync(); } } ``` 运行此代码时,预期得到类似下图的结果: ## 解析代理的 IP 与主机 Sisk 可与代理一起使用,因此在客户端到代理的事务中,IP 地址可能会被代理端点替换。 您可以在 Sisk 中使用 [forwarding resolvers](https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.md) 定义自己的解析器。 ## 头部编码 某些实现的头部编码可能会出现问题。在 Windows 上不支持 UTF‑8 头部,因而使用 ASCII。Sisk 内置了编码转换器,可用于解码错误编码的头部。 此操作成本较高,默认情况下已禁用,可通过 [HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) 启用。 --- # 响应 Source: https://docs.sisk-framework.org/zh-cn/docs/fundamentals/responses.html 响应表示对 HTTP 请求的 HTTP 响应对象。它们由服务器发送给客户端,以指示对资源、页面、文档、文件或其他对象的请求。 HTTP 响应由状态、头部和内容组成。 在本文档中,我们将教您如何使用 Sisk 构建 HTTP 响应。 ## 设置 HTTP 状态 自 HTTP/1.0 起,HTTP 状态列表保持不变,Sisk 支持所有状态码。 ```cs HttpResponse res = new HttpResponse(); res.Status = System.Net.HttpStatusCode.Accepted; // 202 ``` 或使用流式语法: ```cs new HttpResponse() .WithStatus(200) // or .WithStatus(HttpStatusCode.Ok) // or .WithStatus(HttpStatusInformation.Ok); ``` 您可以在[此处](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httpstatuscode)查看可用的 HttpStatusCode 完整列表。您也可以使用 [HttpStatusInformation](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpStatusInformation.md) 结构提供自定义状态码。 ## 正文和内容类型 Sisk 支持原生 .NET 内容对象在响应中发送正文。例如,您可以使用 [StringContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.stringcontent) 类发送 JSON 响应: ```cs HttpResponse res = new HttpResponse(); res.Content = new StringContent(myJson, Encoding.UTF8, "application/json"); ``` 如果您未在头部显式定义 `Content-Length`,服务器将始终尝试根据您在内容中定义的内容计算 `Content-Length`。如果服务器无法从响应内容隐式获取 Content-Length 头部,响应将使用分块传输编码(Chunked-Encoding)发送。 您也可以通过发送 [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent) 或使用方法 [GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) 来流式传输响应。 ## 响应头 您可以添加、编辑或删除响应中发送的头部。下面的示例展示了如何向客户端发送重定向响应。 ```cs HttpResponse res = new HttpResponse(); res.Status = HttpStatusCode.Moved; res.Headers.Add(HttpKnownHeaderNames.Location, "/login"); ``` 或使用流式语法: ```cs new HttpResponse(301) .WithHeader("Location", "/login"); ``` 当您使用 HttpHeaderCollection 的 [Add](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Add.md) 方法时,您是在不更改已发送头部的情况下向请求添加头部。[Set](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Set.md) 方法会用指定的值替换同名头部。HttpHeaderCollection 的索引器内部调用 Set 方法来替换头部。 您还可以使用 [GetHeaderValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.GetHeaderValue.md) 方法检索头部值。该方法有助于获取响应头部和内容头部(如果设置了内容)的值。 ```cs // 返回 "Content-Type" 头部的值,同时检查 response.Headers 和 response.Content.Headers string? contentType = response.GetHeaderValue("Content-Type"); ``` ## 发送 Cookie Sisk 提供了便于在客户端定义 Cookie 的方法。通过此方法设置的 Cookie 已经进行 URL 编码,并符合 RFC-6265 标准。 ```cs HttpResponse res = new HttpResponse(); res.SetCookie("cookie-name", "cookie-value"); ``` 或使用流式语法: ```cs new HttpResponse(301) .WithCookie("cookie-name", "cookie-value", expiresAt: DateTime.Now.Add(TimeSpan.FromDays(7))); ``` 同一方法还有其他[更完整的版本](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.CookieHelper.SetCookie.md)。 ## 分块响应 您可以将传输编码设置为分块(chunked),以发送大型响应。 ```cs HttpResponse res = new HttpResponse(); res.SendChunked = true; ``` 使用分块编码时,Content-Length 头部会自动省略。 ## 响应流 响应流是一种受管方式,允许您以分段方式发送响应。这比使用 HttpResponse 对象更底层,因为它需要您手动发送头部和内容,然后关闭连接。 此示例为文件打开只读流,将该流复制到响应输出流,并且不会将整个文件加载到内存中。这对于提供中等或大型文件非常有用。 ```cs // 获取响应输出流 using var fileStream = File.OpenRead("my-big-file.zip"); var responseStream = request.GetResponseStream(); // 设置响应编码以使用分块传输编码 // 同时在使用时不应发送 content-length 头部 // 分块编码 responseStream.SendChunked = true; responseStream.SetStatus(200); responseStream.SetHeader(HttpKnownHeaderNames.ContentType, contentType); // 将文件流复制到响应输出流 fileStream.CopyTo(responseStream.ResponseStream); // 关闭流 return responseStream.Close(); ``` ## GZip、Deflate 和 Brotli 压缩 您可以在 Sisk 中发送压缩内容的响应。首先,将您的 [HttpContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent) 对象封装在以下压缩器之一中,以向客户端发送压缩响应。 ```cs router.MapGet("/hello.html", request => { string myHtml = "..."; return new HttpResponse () { Content = new GZipContent(new HtmlContent(myHtml)), // or Content = new BrotliContent(new HtmlContent(myHtml)), // or Content = new DeflateContent(new HtmlContent(myHtml)), }; }); ``` 您也可以在流中使用这些压缩内容。 ```cs router.MapGet("/archive.zip", request => { // do not apply "using" here. the HttpServer will discard your content // after sending the response. var archive = File.OpenRead("/path/to/big-file.zip"); return new HttpResponse () { Content = new GZipContent(archive) } }); ``` 使用这些内容时,Content-Encoding 头部会自动设置。 ## 自动压缩 可以通过 [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) 属性自动压缩 HTTP 响应。该属性会自动将路由器的响应内容封装为可压缩内容,只要响应未继承自 [CompressedContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.CompressedContent.md),并且该内容被请求接受。 对于一次请求,只会选择一种可压缩内容,依据 Accept-Encoding 头部按以下顺序选择: - [BrotliContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.BrotliContent.md) (br) - [GZipContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.GZipContent.md) (gzip) - [DeflateContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.DeflateContent.md) (deflate) 如果请求声明接受其中任意一种压缩方式,响应将自动进行压缩。 ## 隐式响应类型 您可以使用除 HttpResponse 之外的其他返回类型,但需要配置路由器如何处理每种对象类型。 其概念是始终返回引用类型并将其转换为有效的 HttpResponse 对象。返回 HttpResponse 的路由不会进行任何转换。 值类型(结构体)不能用作返回类型,因为它们与 [RouterCallback](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterCallback.md) 不兼容,因此必须包装在 ValueResult 中才能在处理程序中使用。 请参考以下未在返回类型中使用 HttpResponse 的路由模块示例: ```cs [RoutePrefix("/users")] public class UsersController : RouterModule { public List Users = new List(); [RouteGet] public IEnumerable Index(HttpRequest request) { return Users.ToArray(); } [RouteGet("")] public User View(HttpRequest request) { int id = request.RouteParameters["id"].GetInteger(); User dUser = Users.First(u => u.Id == id); return dUser; } [RoutePost] public ValueResult Create(HttpRequest request) { User fromBody = request.GetJsonContent()!; Users.Add(fromBody); return true; } } ``` 有了这些,现在需要在路由器中定义它如何处理每种对象。对象始终是处理程序的第一个参数,输出类型必须是有效的 HttpResponse。此外,路由的输出对象不应为 null。 对于 ValueResult 类型,无需指明输入对象是 ValueResult 以及仅 T,因为 ValueResult 是从其原始组件反射得到的对象。 类型关联并不比较已注册的类型与路由回调返回对象的类型,而是检查路由结果的类型是否可分配给已注册的类型。 注册 Object 类型的处理程序将作为所有先前未验证类型的回退。值处理程序的插入顺序也很重要,因此注册 Object 处理程序会忽略所有其他特定类型的处理程序。始终先注册具体的值处理程序以确保顺序。 ```cs Router r = new Router(); r.MapInstance(new UsersController()); r.RegisterValueHandler(apiResult => { return new HttpResponse() { Status = apiResult.Success ? HttpStatusCode.OK : HttpStatusCode.BadRequest, Content = apiResult.GetHttpContent(), Headers = apiResult.GetHeaders() }; }); r.RegisterValueHandler(bvalue => { return new HttpResponse() { Status = bvalue ? HttpStatusCode.OK : HttpStatusCode.BadRequest }; }); r.RegisterValueHandler>(enumerableValue => { return new HttpResponse(string.Join("\n", enumerableValue)); }); // registering an value handler of object must be the last // value handler which will be used as an fallback r.RegisterValueHandler(fallback => { return new HttpResponse() { Status = HttpStatusCode.OK, Content = JsonContent.Create(fallback) }; }); ``` ## 延迟操作 当请求到达路由器时,首先会经过[请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md),在路由操作中处理,然后再由后执行的请求处理程序处理。路由操作的结果会传递给值处理程序,值处理程序的结果则作为响应发送给客户端。 此生命周期在异步上下文中进行。该异步上下文公开变量,用户可以将其添加到 [HttpContext Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) 中,以在处理程序和路由操作之间共享数据。路由操作返回的值会加入此异步上下文,可由值处理程序访问。 延迟操作是在周期结束时执行的操作,即在向客户端交付响应之后,但仍在同一异步上下文中。这些操作可用于执行不必在发送响应前完成的长时间任务,例如保存日志、更新数据库、发送电子邮件等。 异常仍会在延迟操作中被捕获,并以与请求生命周期中任何位置抛出的异常相同的方式处理。不同之处在于客户端已经收到响应,因此异常由默认错误处理机制处理。 使用 [HttpContext.EnqueueDeferredAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.EnqueueDeferredAction.md) 方法延迟执行操作。该方法接收一个表示要执行的操作的异步函数以及可选的超时时间以限制操作的执行时间。如果操作未在时间限制内完成,将被取消。 ```csharp [RoutePost("/send-mail")] public HttpResponse SendMail(HttpRequest request) { string to = request.Query["to"].GetString(); string subject = request.Query["subject"].GetString(); string body = request.Query["body"].GetString(); if (string.IsNullOrWhiteSpace(to) || string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(body)) { throw new ApiException("Missing required parameters."); } // 调度一个长时间运行的操作,该操作将在向客户端发送响应后执行,但仍在同一请求的异步上下文中 request.Context.EnqueueDeferredAction(async (ct) => { await EmailService.SendEmailAsync(to, subject, body); }, timeout: TimeSpan.FromSeconds(30)); return new HttpResponse() { Status = 200, Content = new StringContent("Sending the email...") }; } ``` ## 关于可枚举对象和数组的说明 实现了 [IEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.ienumerable?view=net-8.0) 的隐式响应对象会在通过定义的值处理程序转换之前,通过 `ToArray()` 方法读取到内存中。为此,`IEnumerable` 对象会被转换为对象数组,响应转换器始终接收 `Object[]` 而非原始类型。 考虑以下情形: ```csharp using var host = HttpServer.CreateBuilder(12300) .UseRouter(r => { r.RegisterValueHandler>(stringEnumerable => { return new HttpResponse("String array:\n" + string.Join("\n", stringEnumerable)); }); r.RegisterValueHandler>(stringEnumerable => { return new HttpResponse("Object array:\n" + string.Join("\n", stringEnumerable)); }); r.MapGet("/", request => { return (IEnumerable)["hello", "world"]; }); }) .Build(); ``` 在上述示例中,`IEnumerable` 转换器**永远不会被调用**,因为输入对象始终是 `Object[]`,且无法转换为 `IEnumerable`。然而,下面接收 `IEnumerable` 的转换器会收到其输入,因为其值是兼容的。 如果您确实需要处理将被枚举的对象类型,则需要使用反射获取集合元素的类型。所有可枚举对象(列表、数组和集合)都会被 HTTP 响应转换器转换为对象数组。 如果启用了 [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) 属性,服务器会自动处理实现了 [IAsyncEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-8.0) 的值,类似于 `IEnumerable` 的处理方式。此选项在 `HttpServerConfiguration` 中默认启用;异步枚举会被转换为阻塞枚举器,然后再转换为同步的对象数组。仅在您为异步序列提供自定义值处理程序或流式响应策略时才禁用它。 --- # 日志 Source: https://docs.sisk-framework.org/zh-cn/docs/features/logging.html 您可以配置 Sisk 自动写入访问日志和错误日志。可以定义日志轮转、扩展名和频率。 [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) 类提供了一种异步写入日志并将其保存在可等待写入队列中的方式。`LogStream` 类实现了 `IAsyncDisposable`,确保在流关闭之前写入所有未完成的日志。 本文将向您展示如何为应用程序配置日志记录。 ## 基于文件的访问日志 将日志写入文件时,会打开文件、写入行文本,然后在每行写入后关闭文件。采用此过程是为了保持日志写入的响应性。 ```cs {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream("logs/access.log"); }) .Build(); ... await app.StartAsync(); } } ``` 上述代码会将所有传入请求写入 `logs/access.log` 文件。请注意,如果文件不存在会自动创建,但其所在的文件夹不会自动创建。无需手动创建 `logs/` 目录,因为 LogStream 类会自动创建它。 ## 基于流的日志记录 您可以通过在构造函数中传入 `TextWriter` 对象,将日志写入 `TextWriter` 实例,例如 `Console.Out`: ```cs {title="Program.cs"} using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream(Console.Out); }) .Build(); ``` 对于基于流的日志写入的每条消息,都会调用 `TextWriter.Flush()` 方法。 ## 访问日志格式化 您可以通过预定义变量自定义访问日志格式。考虑下面这行代码: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]"; ``` 它会写出类似如下的消息: 29/mar./2023 15:21:47 -0300 Executed ::1 http://localhost:5555/ [200 OK] 689B -> 707B in 84ms [Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/111.0.0.0 Safari/537.36] 您可以按照下表所述的格式来定义日志文件: | 值 | 表示含义 | 示例 | |-------------------|------------------------------------------------------|--------------------------------------| | %dd | 月份的日期(两位数字) | 05 | | %dmmm | 月份的全称 | July | | %dmm | 月份的缩写(三个字母) | Jul | | %dm | 月份数字(两位数字) | 07 | | %dy | 年份(四位数字) | 2023 | | %th | 12 小时制的小时 | 03 | | %tH | 24 小时制的小时(HH) | 15 | | %ti | 分钟(两位数字) | 30 | | %ts | 秒(两位数字) | 45 | | %tm | 毫秒(三位数字) | 123 | | %tz | 时区偏移(UTC 总小时) | +03:00 | | %ri | 客户端的远程 IP 地址 | 192.168.1.100 | | %rm | HTTP 方法(大写) | GET | | %rs | URI 方案(http/https) | https | | %ra | URI 权威(域名) | example.com | | %rh | 请求的主机 | www.example.com | | %rp | 请求的端口 | 443 | | %rz | 请求的路径 | /path/to/resource | | %rq | 查询字符串 | ?key=value&another=123 | | %sc | HTTP 响应状态码 | 200 | | %sd | HTTP 响应状态描述 | OK | | %lin | 请求的可读大小 | 1.2 KB | | %linr | 请求的原始大小(字节) | 1234 | | %lou | 响应的可读大小 | 2.5 KB | | %lour | 响应的原始大小(字节) | 2560 | | %lms | 以毫秒为单位的耗时 | 120 | | %ls | 执行状态 | Executed | | %{header-name} | 表示请求的 `header-name` 头部。 | `Mozilla/5.0 (platform; rv:gecko [...]` | | %{:header-name} | 表示响应的 `header-name` 头部。 | `application/json` | 您也可以使用 `HttpServerConfiguration.DefaultAccessLogFormat` 来使用默认的访问日志格式。 ## 轮转日志 您可以配置 HTTP 服务器在日志文件达到一定大小时将其轮转为压缩的 .gz 文件。大小会按照您定义的阈值定期检查。 ```cs LogStream errorLog = new LogStream("logs/error.log") .ConfigureRotatingPolicy( maximumSize: 64 * SizeHelper.UnitMb, dueTime: TimeSpan.FromHours(6)); ``` 上述代码会每六小时检查一次 LogStream 的文件是否已达到 64 MB 限制。如果已达到,则会将文件压缩为 .gz 文件,并随后清理 `access.log`。 在此过程中,文件写入会被锁定,直至文件压缩并清理完成。此期间产生的所有写入行都会进入队列,等待压缩结束后再写入。 此功能仅适用于基于文件的 LogStream。 ## 错误日志记录 当服务器不将错误抛给调试器时,会在有错误时将其转发到日志写入。您可以通过以下方式配置错误写入: ```cs config.ThrowExceptions = false; config.ErrorsLogsStream = new LogStream("error.log"); ``` 只有当错误未被回调或 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 属性捕获时,此属性才会向日志写入内容。 服务器写入的错误日志始终包含日期时间、请求头(不包括正文)、错误堆栈以及内部异常堆栈(如果有的话)。 ## 其他日志实例 您的应用程序可以拥有零个或多个 LogStream,日志通道数量没有限制。因此,您可以将应用程序的日志定向到除默认 AccessLog 或 ErrorLog 之外的其他文件。 ```cs LogStream appMessages = new LogStream("messages.log"); appMessages.WriteLine("Application started at {0}", DateTime.Now); ``` ## 扩展 LogStream 您可以扩展 `LogStream` 类以写入自定义格式,兼容当前的 Sisk 日志引擎。下面的示例演示如何通过 Spectre.Console 库将彩色消息写入控制台: ```cs {title="CustomLogStream.cs"} public class CustomLogStream : LogStream { protected override void WriteLineInternal(string line) { base.WriteLineInternal($"[{DateTime.Now:g}] {line}"); } } ``` 另一种为每个请求/响应自动写入自定义日志的方式是创建一个 [HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md)。下面的示例更为完整。它将请求和响应的主体以 JSON 形式写入控制台,可用于一般的请求调试。此示例使用了 ContextBag 和 HttpServerHandler。 ```cs {title="Program.cs"} class Program { static async Task Main(string[] args) { var app = HttpServer.CreateBuilder(host => { host.UseListeningPort(5555); host.UseHandler(); }); app.Router.MapAny("/json", request => { return new HttpResponse() .WithContent(JsonContent.Create(new { method = request.Method.Method, path = request.Path, specialMessage = "Hello, world!!" })); }); await app.StartAsync(); } } ``` ```cs {title="JsonMessageHandler.cs"} class JsonMessageHandler : HttpServerHandler { protected override void OnHttpRequestOpen(HttpRequest request) { if (request.Method != HttpMethod.Get && request.Headers["Content-Type"]?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true) { // 此时连接已打开,客户端已发送声明内容为 JSON 的头部。下面的代码读取内容并将其保存在请求中。 // // 如果在请求处理阶段未读取内容,GC 可能会在向客户端发送响应后回收该内容,导致响应关闭后内容不可用。 // _ = request.RawBody; // 在上下文中添加提示,标记此请求包含 JSON 正文 request.Bag.Add("IsJsonRequest", true); } } protected override async void OnHttpRequestClose(HttpServerExecutionResult result) { string? requestJson = null, responseJson = null, responseMessage; if (result.Request.Bag.ContainsKey("IsJsonRequest")) { // 使用 CypherPotato.LightJson 库重新格式化 JSON var content = result.Request.Body; requestJson = JsonValue.Deserialize(content, new JsonOptions() { WriteIndented = true }).ToString(); } if (result.Response is { } response) { var content = response.Content; responseMessage = $"{(int)response.Status} {HttpStatusInformation.GetStatusCodeDescription(response.Status)}"; if (content is HttpContent httpContent && // 检查响应是否为 JSON httpContent.Headers.ContentType?.MediaType?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true) { string json = await httpContent.ReadAsStringAsync(); responseJson = JsonValue.Deserialize(json, new JsonOptions() { WriteIndented = true }).ToString(); } } else { // 获取内部服务器处理状态 responseMessage = result.Status.ToString(); } StringBuilder outputMessage = new StringBuilder(); if (requestJson != null) { outputMessage.AppendLine("-----"); outputMessage.AppendLine($">>> {result.Request.Method} {result.Request.Path}"); if (requestJson is not null) outputMessage.AppendLine(requestJson); } outputMessage.AppendLine($"<<< {responseMessage}"); if (responseJson is not null) outputMessage.AppendLine(responseJson); outputMessage.AppendLine("-----"); await Console.Out.WriteLineAsync(outputMessage.ToString()); } } ``` --- # Server Sent Events Source: https://docs.sisk-framework.org/zh-cn/docs/features/server-sent-events.html Sisk 开箱即支持通过 Server Sent Events 发送消息。您可以创建一次性和持久的连接,在运行时获取这些连接并使用它们。 此功能受到浏览器的某些限制,例如只能发送文本消息且无法永久关闭连接。服务器端关闭的连接会导致客户端每隔 5 秒(某些浏览器为 3 秒)尝试重新连接。 这些连接对于在服务器向客户端发送事件时,无需客户端每次请求信息非常有用。 ## 创建 SSE 连接 SSE 连接的工作方式类似普通的 HTTP 请求,但不是在发送响应后立即关闭连接,而是保持连接打开以发送消息。 调用 [HttpRequest.GetEventSource()](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) 方法时,请求会进入等待状态,同时创建 SSE 实例。 ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.Send("Hello, world!"); return sse.Close(); }); ``` 在上述代码中,我们创建了一个 SSE 连接并发送了 “Hello, world” 消息,随后从服务器端关闭了 SSE 连接。 > [!NOTE] > 当关闭服务器端连接时,默认情况下客户端会在该端尝试重新连接,连接会被重新启动,方法会再次执行,永无止境。 > > 通常在服务器关闭连接时会转发一个终止消息,以防止客户端再次尝试重新连接。 ## 追加 Header 如果需要发送 Header,可以在发送任何消息之前使用 [HttpRequestEventSource.AppendHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.AppendHeader.md) 方法。 ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.AppendHeader("Header-Key", "Header-value"); sse.Send("Hello!"); return sse.Close(); }); ``` 请注意,必须在发送任何消息之前发送 Header。 ## Wait-For-Fail 连接 当服务器因可能的客户端断开而无法继续发送消息时,连接通常会被终止。此时连接会自动结束,类的实例也会被丢弃。 即使重新连接,类的实例也无法工作,因为它绑定到之前的连接。在某些情况下,您可能稍后仍需要此连接,并且不想通过路由的回调方法来管理它。 为此,我们可以为 SSE 连接指定标识符,并在以后(甚至在路由回调之外)使用该标识符获取它们。此外,我们使用 [WaitForFail](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.WaitForFail.md) 标记连接,以防止路由被终止并自动关闭连接。 在 `WaitForFail` 模式下,SSE 连接会等待因断开导致的发送错误,或等待配置的空闲容忍时间到期后,路由才会恢复并关闭连接。 ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource("my-index-connection"); sse.WaitForFail(TimeSpan.FromSeconds(15)); // 等待 15 秒未收到任何消息后终止连接 return sse.Close(); }); ``` 上述方法将创建连接、处理它并等待断开或错误。 ```cs HttpRequestEventSource? evs = server.EventSources.GetByIdentifier("my-index-connection"); if (evs != null) { // 连接仍然存活 evs.Send("Hello again!"); } ``` 上面的代码片段会尝试查找新创建的连接,如果存在,则向其发送一条消息。 所有已标识的活动服务器连接都可以在集合 [HttpServer.EventSources](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.EventSources.md) 中获取。该集合仅存储活动且已标识的连接,已关闭的连接会从集合中移除。 > [!NOTE] > 需要注意的是,保持连接活跃的上限受可能以不可控方式连接到 Sisk 的组件限制,例如 Web 代理、HTTP 内核或网络驱动,它们会在一定时间后关闭空闲连接。 > > 因此,重要的是通过定期发送 ping 或延长最大存活时间来保持连接打开。阅读下一节以更好地了解如何发送周期性 ping。 ## 设置连接 Ping 策略 Ping 策略是一种自动向客户端发送周期性消息的方式。该功能使服务器能够在不必无限期保持连接打开的情况下,判断客户端是否已断开。 ```cs [RouteGet("/sse")] public async Task Events(HttpRequest request) { using var sse = await request.GetEventSourceAsync("user-events"); sse.WithPing(ping => { ping.DataMessage = "ping-message"; ping.Interval = TimeSpan.FromSeconds(5); ping.Start(); }); await sse.WaitForFailAsync(TimeSpan.FromMinutes(10)); return await sse.CloseAsync(); } ``` 如上代码所示,每隔 5 秒会向客户端发送一次新的 ping 消息。这将保持 TCP 连接活跃,防止因闲置而被关闭。同时,当消息发送失败时,连接会自动关闭,释放连接占用的资源。 在异步路由中使用 [SendAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.SendAsync.md) 和 [CloseAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.CloseAsync.md)。如果需要在关闭前丢弃已排队的事件,请调用 [Cancel](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.Cancel.md)。 ## 查询连接 您可以使用对连接标识符的谓词搜索活动连接,以实现广播等功能。 ```cs HttpRequestEventSource[] evs = server.EventSources.Find(es => es.StartsWith("my-connection-")); foreach (HttpRequestEventSource e in evs) { e.Send("Broadcasting to all event sources that starts with 'my-connection-'"); } ``` 您也可以使用 [All](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpEventSourceCollection.All.md) 方法获取所有活动的 SSE 连接。 --- # Web 套接字 Source: https://docs.sisk-framework.org/zh-cn/docs/features/websockets.html Sisk 也支持 Web 套接字,例如接收和发送消息给客户端。 此功能在大多数浏览器中运行良好,但在 Sisk 中仍属实验性。若您发现任何错误,请在 GitHub 上报告。 ## 接收消息 WebSocket 消息按顺序接收,排队等待 `ReceiveMessageAsync` 处理。超时、操作被取消或客户端断开时,此方法不返回消息。 一次只能进行一次读或写操作,因此在使用 `ReceiveMessageAsync` 等待消息时,无法向已连接的客户端写入数据。 ```cs router.MapGet("/connect", async (HttpRequest req) => { using var ws = await req.GetWebSocketAsync(); while (await ws.ReceiveMessageAsync(timeout: TimeSpan.FromSeconds(30)) is { } receivedMessage) { string msgText = receivedMessage.GetString(); Console.WriteLine("Received message: " + msgText); await ws.SendAsync("Hello!"); } return await ws.CloseAsync(); }); ``` ## 持久连接 下面的示例展示了如何使用持久的 WebSocket 连接,接收消息、处理它们,并在完成后关闭套接字。 ```cs router.MapGet("/connect", async (HttpRequest req) => { using var ws = await req.GetWebSocketAsync(); WebSocketMessage? msg; askName: await ws.SendAsync("What is your name?"); msg = await ws.ReceiveMessageAsync(); if (msg is null) return await ws.CloseAsync(); string name = msg.GetString(); if (string.IsNullOrEmpty(name)) { await ws.SendAsync("Please, insert your name!"); goto askName; } askAge: await ws.SendAsync("And your age?"); msg = await ws.ReceiveMessageAsync(); if (msg is null) return await ws.CloseAsync(); if (!Int32.TryParse(msg?.GetString(), out int age)) { await ws.SendAsync("Please, insert an valid number"); goto askAge; } await ws.SendAsync($"You're {name}, and you are {age} old."); return await ws.CloseAsync(); }); ``` ## Ping 策略 类似于 Server Side Events 中的 ping 策略,您也可以配置 ping 策略,以在连接空闲时保持 TCP 连接打开。 ```cs ws.PingPolicy.Start( dataMessage: "ping-message", interval: TimeSpan.FromSeconds(10)); ``` ## 托管连接 接受 WebSocket 时,您可以提供标识符。已标识的套接字会注册到 [HttpServer.WebSockets](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WebSockets.md),从而使服务器能够在接受它们的路由之外查找活动连接。 ```cs router.MapGet("/connect/", async (HttpRequest req) => { string userId = req.RouteParameters["userId"].GetString(); using var ws = await req.GetWebSocketAsync(identifier: $"user:{userId}"); ws.State = userId; ws.PingPolicy.Start( dataMessage: "ping", interval: TimeSpan.FromSeconds(10)); while (await ws.ReceiveMessageAsync(TimeSpan.FromMinutes(5)) is { } message) { await ws.SendAsync("Received: " + message.GetString()); } return await ws.CloseAsync(); }); ``` 在应用程序的其他部分,可通过标识符或谓词查询该集合: ```cs HttpWebSocket? socket = server.WebSockets.GetByIdentifier("user:42"); if (socket is { IsClosed: false }) { await socket.SendAsync("Your report is ready."); } foreach (HttpWebSocket activeSocket in server.WebSockets.Find(id => id.StartsWith("user:"))) { await activeSocket.SendAsync("Broadcast message"); } ``` 每个 `HttpWebSocket` 都公开 `Identifier`、`State`、`IsClosed` 和 `PingPolicy`。该集合还提供 `All()`、`Find(...)`、`GetByIdentifier(...)`、`ActiveConnections` 和 `DropAll()`,用于服务器托管的连接策略。 --- # Discard 语法 Source: https://docs.sisk-framework.org/zh-cn/docs/features/discard-syntax.html HTTP 服务器可以用于监听来自操作的回调请求,例如 OAuth 身份验证,并在接收到该请求后丢弃。这在需要后台操作但不想为其设置整个 HTTP 应用程序的情况下很有用。 以下示例展示了如何使用 [CreateListener](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.CreateListener.md) 创建一个在端口 5555 上监听的 HTTP 服务器并等待下一个上下文: ```csharp using (var server = HttpServer.CreateListener(5555)) { // 等待下一个 HTTP 请求 var context = await server.WaitNextAsync(); Console.WriteLine($"请求路径: {context.Request.Path}"); } ``` [WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) 函数等待下一个完成的请求处理上下文。一旦获得此操作的结果,服务器已经完全处理了请求并将响应发送给客户端。 --- # 依赖注入 Source: https://docs.sisk-framework.org/zh-cn/docs/features/instancing.html 通常,会为请求的生命周期专门分配成员和实例,例如数据库连接、已验证的用户或会话令牌。实现这一点的一种可能方式是通过 [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) ,它创建一个在整个请求生命周期中都存在的字典。 该字典可以被 [请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md) 访问,并在整个请求中定义变量。例如,一个验证用户的请求处理程序将用户设置在 `HttpContext.RequestBag` 中,在请求逻辑中,可以通过 `HttpContext.RequestBag.Get()` 来检索该用户。 定义在此字典中的对象的作用域限定于请求生命周期。它们在请求结束时被释放。发送响应并不一定标志着请求生命周期的结束。当 [请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md) 在发送响应后运行时,`RequestBag` 对象仍然存在并且尚未被释放。 以下是一个示例: ```csharp {title="RequestHandlers/AuthenticateUser.cs"} public class AuthenticateUser : IRequestHandler { public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { User authenticatedUser = AuthenticateUser(request); context.RequestBag.Set(authenticatedUser); return null; // advance to the next request handler or request logic } } ``` ```csharp {title="Controllers/HelloController.cs"} [RouteGet("/hello")] [RequestHandler] public HttpResponse SayHello(HttpRequest request) { var authenticatedUser = request.Bag.Get(); return new HttpResponse() { Content = new StringContent($"Hello {authenticatedUser.Name}!") }; } ``` 这是对此操作的初步示例。`User` 的实例是在专门用于验证的请求处理程序中创建的,并且所有使用此请求处理程序的路由都保证在其 `HttpContext.RequestBag` 实例中将有一个 `User`。 可以通过诸如 [GetOrAdd](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAdd.md) 或 [GetOrAddAsync](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAddAsync.md) 之类的方法定义获取实例的逻辑,当实例尚未在 `RequestBag` 中定义时。 从 1.3 版本开始,引入了静态属性 [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md) ,允许访问当前执行的请求上下文的 `HttpContext`。这使得可以在当前请求之外暴露 `HttpContext` 的成员,并在路由对象中定义实例。 以下示例定义了一个控制器,该控制器具有常被请求上下文访问的成员。 ```csharp {title="Controllers/Controller.cs"} public abstract class Controller : RouterModule { // 获取现有的或为此请求创建新的数据库实例 protected DbContext Database => HttpContext.Current.RequestBag.GetOrAdd(() => new DbContext()); // 延迟加载存储库也很常见 protected IUserRepository Users => HttpContext.Current.RequestBag.GetOrAdd(() => new UserRepository(Database)); protected IBlogRepository Blogs => HttpContext.Current.RequestBag.GetOrAdd(() => new BlogRepository(Database)); protected IBlogPostRepository BlogPosts => HttpContext.Current.RequestBag.GetOrAdd(() => new BlogPostRepository(Database)); // 如果属性在请求包中未定义用户时访问,则以下行将抛出异常 protected User AuthenticatedUser => => HttpContext.Current.RequestBag.Get(); // 支持暴露 HttpRequest 实例 protected HttpRequest Request => HttpContext.Current.Request } ``` 并定义继承自控制器的类型: ```csharp {title="Controllers/PostsController.cs"} [RoutePrefix("/api/posts/{author}")] sealed class PostsController : Controller { protected Guid AuthorId => Request.RouteParameters["author"].GetInteger(); [RouteGet] public IAsyncEnumerable ListPosts() { return BlogPosts.GetPostsAsync(authorId: AuthorId); } [RouteGet("")] public async Task GetPost() { int postId = Request.RouteParameters["id"].GetInteger(); Post? post = await BlogPosts .FindPostAsync(post => post.Id == postId && post.AuthorId == AuthorId); return post; } } ``` 对于上面的示例,您需要在路由器中配置一个 [值处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/responses.md#implicit-response-types) ,以便路由器返回的对象转换为有效的 [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md)。 请注意,方法不带有 `HttpRequest request` 参数,如其他方法中所示。这是因为,从 1.3 版本开始,路由器支持两种类型的委托用于路由响应:[RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md) ,这是默认的委托,它接收一个 `HttpRequest` 参数,以及 [ParameterlessRouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.ParameterlessRouteAction.md) 。`HttpRequest` 对象仍然可以通过静态 `HttpContext` 上的 [Request](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Request.md) 属性访问。 在上面的示例中,我们定义了一个可释放对象 `DbContext` ,并且我们需要确保在 HTTP 会话结束时释放在 `DbContext` 中创建的所有实例。为此,我们可以使用两种方法来实现这一点。一种方法是创建一个在路由器操作之后执行的 [请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md) ,另一种方法是通过自定义 [服务器处理程序](https://docs.sisk-framework.org/zh-cn/docs/advanced/http-server-handlers.md)。 对于第一种方法,我们可以直接在继承自 `RouterModule` 的 [OnSetup](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.OnSetup.md) 方法中内联创建请求处理程序: ```csharp {title="Controllers/PostsController.cs"} public abstract class Controller : RouterModule { ... protected override void OnSetup(Router parentRouter) { base.OnSetup(parentRouter); HasRequestHandler(RequestHandler.Create( execute: (req, ctx) => { // 获取请求处理程序上下文中定义的 DbContext 并释放它 ctx.RequestBag.GetOrDefault()?.Dispose(); return null; }, executionMode: RequestHandlerExecutionMode.AfterResponse)); } } ``` > [!TIP] > > 从 Sisk 1.4 版本开始,引入了属性 [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) ,它默认启用,用于定义 HTTP 服务器是否应在 HTTP 会话关闭时释放上下文包中的所有 `IDisposable` 值。 上述方法将确保在 HTTP 会话结束时释放 `DbContext`。您可以为需要在响应结束时释放的其他成员执行此操作。 对于第二种方法,您可以创建一个自定义的 [服务器处理程序](https://docs.sisk-framework.org/zh-cn/docs/advanced/http-server-handlers.md) ,它将在 HTTP 会话结束时释放 `DbContext`。 ```csharp {title="Server/Handlers/ObjectDisposerHandler.cs"} public class ObjectDisposerHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { result.Context.RequestBag.GetOrDefault()?.Dispose(); } } ``` 并在应用程序生成器中使用它: ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder() .UseHandler() .Build(); ``` 这是处理代码清理和将请求依赖项按模块类型分离的方法,减少了路由器每个操作中重复的代码量。这与在类似 ASP.NET 的框架中使用依赖注入类似。 --- # 流式内容 Source: https://docs.sisk-framework.org/zh-cn/docs/features/content-streaming.html Sisk 支持读取和发送流式内容到和从客户端。这一功能对于在请求的生命周期中序列化和反序列化内容的内存开销非常有用。 ## 请求内容流 小内容会自动加载到 HTTP 连接缓冲区内存中,快速加载到 [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) 和 [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md)。对于较大的内容,可以使用 [HttpRequest.GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md) 方法来获取请求内容读取流。 值得注意的是,[HttpRequest.GetMultipartFormContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContent.md) 方法会将整个请求内容读入内存,因此对于读取大内容可能不太有用。 考虑以下示例: ```csharp {title="Controller/UploadDocument.cs"} [RoutePost ( "/api/upload-document/" )] public async Task UploadDocument ( HttpRequest request ) { var fileName = request.RouteParameters [ "filename" ].GetString (); if (!request.HasContents) { // 请求没有内容 return new HttpResponse ( HttpStatusInformation.BadRequest ); } var contentStream = request.GetRequestStream (); var outputFileName = Path.Combine ( AppDomain.CurrentDomain.BaseDirectory, "uploads", fileName ); using (var fs = File.Create ( outputFileName )) { await contentStream.CopyToAsync ( fs ); } return new HttpResponse () { Content = JsonContent.Create ( new { message = "文件发送成功。" } ) }; } ``` 在上面的示例中,`UploadDocument` 方法读取请求内容并将内容保存到文件中。除了 `Stream.CopyToAsync` 使用的读取缓冲区外,不会进行任何额外的内存分配。上面的示例消除了对非常大文件的内存分配压力,可以优化应用程序性能。 一个良好的做法是在可能耗时的操作中始终使用 [CancellationToken](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken),例如发送文件,因为它取决于客户端和服务器之间的网络速度。 可以通过以下方式调整 `CancellationToken`: ```csharp {title="Controller/UploadDocument.cs"} // 下面的取消令牌将在 30 秒超时时抛出异常。 CancellationTokenSource copyCancellation = new CancellationTokenSource ( delay: TimeSpan.FromSeconds ( 30 ) ); try { using (var fs = File.Create ( outputFileName )) { await contentStream.CopyToAsync ( fs, copyCancellation.Token ); } } catch (OperationCanceledException) { return new HttpResponse ( HttpStatusInformation.BadRequest ) { Content = JsonContent.Create ( new { Error = "上传超出了最大上传时间(30 秒)。" } ) }; } ``` ## 响应内容流 发送响应内容也是可能的。目前,有两种方法可以做到这一点:通过 [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) 方法和使用 [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) 类型的内容。 考虑一个需要提供图像文件的场景。可以使用以下代码: ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { // 示例方法来获取个人资料图片 var profilePictureFilename = "profile-picture.jpg"; byte[] profilePicture = await File.ReadAllBytesAsync ( profilePictureFilename ); return new HttpResponse () { Content = new ByteArrayContent ( profilePicture ), Headers = new () { ContentType = "image/jpeg", ContentDisposition = $"inline; filename={profilePictureFilename}" } }; } ``` 上面的方法每次读取图像内容时都会进行内存分配。如果图像很大,这可能会导致性能问题,并且在峰值情况下,甚至可能导致内存过载和服务器崩溃。在这些情况下,缓存可能会有所帮助,但它不会消除问题,因为仍然需要为该文件保留内存。缓存可以缓解每次请求都需要分配内存的压力,但对于大文件来说,它是不够的。 通过流式传输发送图像可以解决这个问题。与其读取整个图像内容,不如创建一个文件的读取流,并使用一个小缓冲区将其复制到客户端。 #### 通过 GetResponseStream 方法发送 [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) 方法创建一个对象,允许将 HTTP 响应的块作为内容流准备好时发送。这种方法更为手动,需要在发送内容之前定义状态、头部和内容大小。 ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; // 以这种形式发送,状态和头部必须在发送内容之前定义 var requestStreamManager = request.GetResponseStream (); requestStreamManager.SetStatus ( System.Net.HttpStatusCode.OK ); requestStreamManager.SetHeader ( HttpKnownHeaderNames.ContentType, "image/jpeg" ); requestStreamManager.SetHeader ( HttpKnownHeaderNames.ContentDisposition, $"inline; filename={profilePictureFilename}" ); using (var fs = File.OpenRead ( profilePictureFilename )) { // 以这种形式发送,内容大小也必须在发送之前定义 requestStreamManager.SetContentLength ( fs.Length ); // 如果不知道内容大小,可以使用分块编码来发送内容 requestStreamManager.SendChunked = true; // 然后,将内容写入输出流 await fs.CopyToAsync ( requestStreamManager.ResponseStream ); } } ``` #### 通过 StreamContent 发送内容 [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) 类允许将数据源作为字节流发送内容。这种发送方式更容易,消除了之前的要求,甚至允许使用 [压缩编码](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) 来减少内容大小。 ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public HttpResponse UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; return new HttpResponse () { Content = new StreamContent ( File.OpenRead ( profilePictureFilename ) ), Headers = new () { ContentType = "image/jpeg", ContentDisposition = $"inline; filename=\"{profilePictureFilename}\"" } }; } ``` > [!IMPORTANT] > > 在这种类型的内容中,不要将流封装在 `using` 块中。内容将由 HTTP 服务器在内容流完成时自动丢弃,无论是否有错误。 --- # 启用 CORS(跨源资源共享)在 Sisk Source: https://docs.sisk-framework.org/zh-cn/docs/features/cors.html Sisk 有一个工具,可以用于处理 [跨源资源共享 (CORS)](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Guides/CORS) 当公开服务时。这一功能不是 HTTP 协议的一部分,而是由 W3C 定义的 Web 浏览器的特定功能。这种安全机制可以防止 Web 页面向不同于提供 Web 页面的域发送请求。服务提供者可以允许某些域访问其资源,或者只允许一个域。 ## 同源 要识别为“同源”,请求必须在其请求中标识 [Origin](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Reference/Headers/Origin) 标头: ```http GET /api/users HTTP/1.1 Host: example.com Origin: http://example.com ... ``` 并且远程服务器必须用具有与请求的源相同值的 [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Headers/Access-Control-Allow-Origin) 标头响应: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com ... ``` 此验证是 **显式** 的:主机、端口和协议必须与请求的相同。检查示例: - 服务器响应其 `Access-Control-Allow-Origin` 为 `https://example.com`: - `https://example.net` - 域不同。 - `http://example.com` - 方案不同。 - `http://example.com:5555` - 端口不同。 - `https://www.example.com` - 主机不同。 在规范中,只允许对请求和响应的标头进行语法检查。URL 路径被忽略。默认端口(HTTP 的 80 和 HTTPS 的 443)被省略。 ```http Origin: null Origin: :// Origin: ://: ``` ## 启用 CORS 本地,您在 [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) 中有 [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) 对象。 您可以在初始化服务器时配置 CORS: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( allowOrigin: "http://example.com", allowHeaders: ["Authorization"], exposeHeaders: ["Content-Type"])) .Build(); await app.StartAsync(); } ``` 上面的代码将为 **所有响应** 发送以下标头: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Headers: Authorization Access-Control-Expose-Headers: Content-Type ``` 这些标头需要发送给所有 Web 客户端的响应,包括错误和重定向。 您可能会注意到 [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) 类有两个类似的属性:[AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) 和 [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md)。注意,其中一个是复数,另一个是单数。 - **AllowOrigin** 属性是静态的:只会发送您指定的源的标头给所有响应。 - **AllowOrigins** 属性是动态的:服务器检查请求的源是否包含在此列表中。如果找到,则会为该源的响应发送标头。 ### 通配符和自动标头 或者,您可以在响应的源中使用通配符 (`*`) 指定任何源都可以访问资源。但是,此值不允许用于具有凭据(授权标头)的请求,并且此操作 [将导致错误](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials)。 您可以通过显式列出将允许通过 [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) 属性的源,或者使用 [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) 常量作为 [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) 的值来解决这个问题。此魔术属性将为请求的 `Origin` 标头的相同值定义 `Access-Control-Allow-Origin` 标头。 您还可以使用 [AutoFromRequestMethod](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestMethod.md) 和 [AutoFromRequestHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestHeaders.md) 实现类似于 `AllowOrigin` 的行为,自动根据标头响应。 ```csharp using var host = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( // 根据请求的 Origin 标头响应 allowOrigin: CrossOriginResourceSharingHeaders.AutoAllowOrigin, // 根据 Access-Control-Request-Method 标头或请求方法响应 allowMethods: [CrossOriginResourceSharingHeaders.AutoFromRequestMethod], // 根据 Access-Control-Request-Headers 标头或发送的标头响应 allowHeaders: [CrossOriginResourceSharingHeaders.AutoFromRequestHeaders], exposeHeaders: [HttpKnownHeaderNames.ContentType, "X-Authenticated-Account-Id"], allowCredentials: true)) .Build(); ``` ## 其他应用 CORS 的方法 如果您处理 [服务提供者](https://docs.sisk-framework.org/zh-cn/docs/extensions/service-providers.md),您可以覆盖配置文件中定义的值: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(...) .UseCors(cors => { // 将覆盖配置文件中定义的源。 cors.AllowOrigin = "http://example.com"; }) .Build(); await app.StartAsync(); } ``` ## 在特定路由上禁用 CORS `UseCors` 属性可用于路由和所有路由属性,并且可以使用以下示例禁用: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { // GET /api/widgets/colors [RouteGet("/colors", UseCors = false)] public IEnumerable GetWidgets() { return new[] { "Green widget", "Red widget" }; } } ``` ## 替换响应中的值 您可以在路由器操作中显式替换或删除值: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { public IEnumerable GetWidgets(HttpRequest request) { // 删除 Access-Control-Allow-Credentials 标头 request.Context.OverrideHeaders.AccessControlAllowCredentials = string.Empty; // 替换 Access-Control-Allow-Origin request.Context.OverrideHeaders.AccessControlAllowOrigin = "https://contorso.com"; return new[] { "Green widget", "Red widget" }; } } ``` ## 预检请求 预检请求是客户端在实际请求之前发送的 [OPTIONS](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Reference/Methods/OPTIONS) 方法请求。 Sisk 服务器将始终用 `200 OK` 和适用的 CORS 标头响应请求,然后客户端可以继续实际请求。这种情况仅在路由存在并且 [RouteMethod](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 显式配置为 `Options` 时不适用。 ## 全局禁用 CORS 不可能这样做。要不使用 CORS,请不要配置它。 --- # 文件服务器 Source: https://docs.sisk-framework.org/zh-cn/docs/features/file-server.html Sisk 提供 `Sisk.Http.FileSystem` 命名空间,其中包含用于提供静态文件、目录列表和文件转换的工具。此功能允许您从本地目录提供文件,支持范围请求(音频/视频流)和自定义文件处理。 ## 提供静态文件 提供静态文件的最简方式是使用 [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md)。此方法将 URL 前缀映射到磁盘上的目录。 ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; // 将服务器根目录映射到当前目录 mainRouter.MapFileSystem("/", Directory.GetCurrentDirectory()); // 将 /assets 映射到 "public/assets" 文件夹 mainRouter.MapFileSystem( "/assets", Path.Combine(Directory.GetCurrentDirectory(), "public", "assets")); ``` 当请求匹配路由前缀时,`HttpFileServerHandler` 会在指定目录中查找文件。若找到,则提供该文件;否则返回 404 响应(若访问被拒绝则返回 403)。 在需要显式创建 `Route` 对象时,仍可使用 `HttpFileServer.CreateServingRoute`,但对应用代码而言,`MapFileSystem` 是最直接的选项。 ## HttpFileServerHandler 若需更细粒度地控制文件的提供方式,可以手动实例化并配置 `HttpFileServerHandler`。 ```cs var fileHandler = new HttpFileServerHandler("/var/www/html"); // 启用目录列表(默认禁用) fileHandler.AllowDirectoryListing = true; // 设置自定义路由前缀(此前缀将在请求路径中被裁剪) fileHandler.RoutePrefix = "/public"; // 在 /public 下注册处理器 mainRouter.MapFileSystem("/public", fileHandler); ``` ### 配置 | Property | Description | |---|---| | `RootDirectoryPath` | 用于提供文件的根目录的绝对路径或相对路径。 | | `RoutePrefix` | 解析文件时会从请求路径中裁剪的路由前缀。默认是 `/`。 | | `AllowDirectoryListing` | 若设为 `true`,在请求目录且未找到索引文件时启用目录列表。默认是 `false`。 | | `FileConverters` | 用于在提供文件前转换文件的 `HttpFileServerFileConverter` 列表。 | ## 目录列表 当 `AllowDirectoryListing` 启用且用户请求目录路径时,Sisk 将生成一个 HTML 页面列出该目录的内容。 目录列表包括: - 指向父目录的导航(`..`)。 - 子目录列表。 - 带有大小和最后修改日期的文件列表。 ## 文件转换器 文件转换器允许拦截特定文件类型并以不同方式处理。例如,您可能想对图像进行转码、即时压缩文件,或使用部分内容(Range 请求)提供文件。 Sisk 包含两个用于媒体流的内置转换器: - `HttpFileAudioConverter`:处理 `.mp3`、`.ogg`、`.wav`、`.flac`、`.ogv`。 - `HttpFileVideoConverter`:处理 `.webm`、`.avi`、`.mkv`、`.mpg`、`.mpeg`、`.wmv`、`.mov`、`.mp4`。 这些转换器支持 **HTTP Range Requests**,允许客户端在音频和视频文件中进行定位播放。 ### 创建自定义转换器 要创建自定义文件转换器,继承 `HttpFileServerFileConverter` 并实现 `CanConvert` 与 `Convert`。 ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; public class MyTextConverter : HttpFileServerFileConverter { public override bool CanConvert(FileInfo file) { // 仅适用于 .txt 文件 return file.Extension.Equals(".txt", StringComparison.OrdinalIgnoreCase); } public override HttpResponse Convert(FileInfo file, HttpRequest request) { string content = File.ReadAllText(file.FullName); // 将所有文本内容转为大写 return new HttpResponse(200) { Content = new StringContent(content.ToUpper()) }; } } ``` 然后,将其添加到处理器中: ```cs var handler = new HttpFileServerHandler("./files"); handler.FileConverters.Add(new MyTextConverter()); ``` --- # 模型上下文协议 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/mcp.html 可以使用 [Sisk.ModelContextProtocol](https://www.nuget.org/packages/Sisk.ModelContextProtocol/) 包构建为使用大型语言模型(LLM)的代理模型提供上下文的应用程序: ```bash dotnet add package Sisk.ModelContextProtocol ``` 该包公开了用于构建在 [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) 上运行的 MCP 服务器的实用类和方法。当前实现支持协议版本 `2025-06-18` 的工具。 > [!NOTE] > > 在开始之前,请注意此包仍在开发中,可能会出现不符合规范的行为。阅读 [package details](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) 以了解正在开发的内容以及哪些功能尚未实现。 ## 快速开始使用 MCP [McpProvider](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.md) 类是定义 MCP 服务器的入口点。它是一个密封的提供者对象,可在启动时进行配置。你的 Sisk 应用程序可以拥有一个或多个 MCP 提供者。 ```csharp McpProvider mcp = new McpProvider( serverName: "math-server", serverTitle: "Mathematics server", serverVersion: new Version(1, 0)); mcp.Tools.Add(new McpTool( name: "math_sum", description: "Sums one or more numbers.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]), executionHandler: async (McpToolContext context) => { var numbers = context.Arguments["numbers"].GetJsonArray().ToArray(); var sum = numbers.Sum(); return await Task.FromResult(McpToolResult.CreateText($"Sum result: {sum:N4}")); })); ``` 如果你的应用程序只提供一个 MCP 提供者,可以使用构建器的单例: ```csharp static void Main(string[] args) { using var host = HttpServer.CreateBuilder() .UseMcp(mcp => { mcp.ServerName = "math-server"; mcp.ServerTitle = "Mathematics server"; mcp.Tools.Add(new McpTool( name: "math_sum", description: "Sums one or more numbers.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]), executionHandler: async (McpToolContext context) => { var numbers = context.Arguments["numbers"].GetJsonArray().ToArray(); var sum = numbers.Sum(); return await Task.FromResult(McpToolResult.CreateText($"Sum result: {sum:N4}")); })); }) .UseRouter(router => { router.MapAny("/mcp", async (HttpRequest req) => { return await req.HandleMcpRequestAsync(); }); }) .Build(); host.Start(); } ``` 该端点必须同时接受 `GET` 和 `POST` 请求,因此 `MapAny` 是最简洁的路由映射方式。`HandleMcpRequestAsync` 返回一个 [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md),你的路由必须返回它。如果在同一个应用中需要多个提供者,请跳过单例,直接在每个路由中调用 [McpProvider.HandleRequestAsync](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.HandleRequestAsync.md): ```csharp var mathProvider = new McpProvider("math-server", "Mathematics server", new Version(1, 0)); router.MapAny("/mcp/math", async request => { return await mathProvider.HandleRequestAsync(request); }); ``` ## 为函数创建 JSON 架构 [Sisk.ModelContextProtocol] 库使用了 [LightJson](https://github.com/CypherPotato/LightJson) 的分支来处理 JSON 和 JSON 架构。此实现为各种对象提供了流式的 JSON Schema 构建器: - JsonSchema.CreateObjectSchema - JsonSchema.CreateArraySchema - JsonSchema.CreateBooleanSchema - JsonSchema.CreateNumberSchema - JsonSchema.CreateStringSchema - JsonSchema.Empty 示例: ```csharp JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]); ``` 生成以下架构: ```json { "type": "object", "properties": { "numbers": { "type": "array", "items": { "type": "number" }, "minItems": 1, "description": "The numbers to sum." } }, "required": ["numbers"] } ``` ## 处理函数调用 在 [McpTool](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpTool.md) 的 `executionHandler` 参数中定义的函数会提供一个包含调用参数的 JsonObject,可流式读取: ```csharp mcp.Tools.Add(new McpTool( name: "browser_do_action", description: "Run an browser action, such as scrolling, refreshing or navigating.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "action_name", JsonSchema.CreateStringSchema( enums: ["go_back", "refresh", "scroll_bottom", "scroll_top"], description: "The action name.") }, { "action_data", JsonSchema.CreateStringSchema( description: "Action parameter." ) } }, requiredProperties: ["action_name"]), executionHandler: async (McpToolContext context) => { // read action name. will throw if null or not a explicit string string actionName = context.Arguments["action_name"].GetString(); // action_data is defined as non-required, so it may be null here string? actionData = context.Arguments["action_data"].MaybeNull()?.GetString(); // Handle the browser action based on the actionName return await Task.FromResult( McpToolResult.CreateText($"Performed browser action: {actionName}")); })); ``` 工具参数会在你的处理程序运行之前根据架构进行验证。如果验证失败,提供者会向 MCP 客户端返回错误结果,并且不会调用工具处理程序。 ## 函数结果 [McpToolResult](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.md) 对象提供了三种方法来创建工具响应的内容: - [CreateAudio(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateAudio.md):为 MCP 客户端创建基于音频的响应。 - [CreateImage(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateImage.md):为 MCP 客户端创建基于图像的响应。 - [CreateText(string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateText.md):为 MCP 客户端创建基于文本的响应(默认)。 此外,还可以将多种不同的内容组合成单个 JSON 工具响应: ```csharp mcp.Tools.Add(new McpTool( ... executionHandler: async (McpToolContext context) => { // simulate real work byte[] browserScreenshot = await browser.ScreenshotAsync(); return McpToolResult.Combine( McpToolResult.CreateText("Heres the screenshot of the browser:"), McpToolResult.CreateImage(browserScreenshot, "image/png") ) })); ``` 当前提供者处理初始化、`tools/list`、`tools/call`、`ping` 和 `notifications/*`。不支持的 JSON-RPC 方法会返回 JSON-RPC 错误响应。 ## 持续工作 模型上下文协议是一种用于代理模型和向其提供内容的应用程序的通信协议。它是一个新协议,因此其规范经常会因废弃、添加新特性或破坏性更改而更新。 在开始构建代理应用之前,务必了解 [Model Context Protocol](https://modelcontextprotocol.io/docs/cn/getting-started/intro) 所解决的问题。 同时阅读 [Sisk.ModelContextProtocol](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) 包的规范,以了解其进展、状态以及可以实现的功能。 --- # JSON-RPC 扩展 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/json-rpc.html Sisk 提供了一个实验性的 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) API 模块,帮助你创建更简洁的应用程序。此扩展严格实现 JSON-RPC 2.0 传输接口,并提供通过 HTTP GET、POST 请求以及 Sisk 的 WebSocket 进行传输。 你可以使用下面的命令通过 Nuget 安装此扩展。请注意,在实验/测试版中,需要在 Visual Studio 中启用搜索预发布包的选项。 ```bash dotnet add package Sisk.JsonRpc ``` ## 传输接口 JSON-RPC 是一种无状态、异步的远程过程调用(RPC)协议,使用 JSON 进行数据通信。JSON-RPC 请求通常通过 ID 标识,响应则使用相同的 ID 返回。并非所有请求都需要响应,这类请求称为“通知”。 [JSON-RPC 2.0 规范](https://www.jsonrpc.org/specification) 详细说明了传输的工作方式。该传输方式与使用场景无关。Sisk 通过 HTTP 实现此协议,遵循 [JSON-RPC over HTTP](https://www.jsonrpc.org/historical/json-rpc-over-http.html) 的规范,部分支持 GET 请求,完全支持 POST 请求。WebSocket 也得到支持,提供异步消息通信。 JSON-RPC 请求示例: ```json { "jsonrpc": "2.0", "method": "Sum", "params": [1, 2, 4], "id": 1 } ``` 成功响应示例: ```json { "jsonrpc": "2.0", "result": 7, "id": 1 } ``` ## JSON-RPC 方法 下面的示例展示了如何使用 Sisk 创建 JSON-RPC API。一个数学运算类执行远程操作并将序列化后的响应返回给客户端。 ```csharp {title="Program.cs"} using var app = HttpServer.CreateBuilder(port: 5555) .UseJsonRPC((sender, args) => { // 将所有标记了 WebMethod 的方法添加到 JSON-RPC 处理器 args.Handler.Methods.AddMethodsFromType(new MathOperations()); // 将 /service 路由映射为处理 JSON-RPC POST 与 GET 请求 args.Router.MapPost("/service", args.Handler.Transport.HttpPost); args.Router.MapGet("/service", args.Handler.Transport.HttpGet); // 将 JSON-RPC WebSocket 传输映射到 GET /ws args.Router.MapGet("/ws", args.Handler.Transport.WebSocket); }) .Build(); await app.StartAsync(); ``` ```csharp {title="MathOperations.cs"} public class MathOperations { [WebMethod] public float Sum(float a, float b) { return a + b; } [WebMethod] public double Sqrt(float a) { return Math.Sqrt(a); } } ``` 上述示例会将 `Sum` 和 `Sqrt` 方法映射到 JSON-RPC 处理器,这些方法可通过 `GET /service`、`POST /service` 和 `GET /ws` 访问。方法名不区分大小写。 方法参数会自动反序列化为对应的类型。也支持使用具名参数的请求。JSON 序列化由 [LightJson](https://github.com/CypherPotato/LightJson) 库完成。当类型未能正确反序列化时,你可以为该类型创建特定的 [JSON 转换器](https://github.com/CypherPotato/LightJson?tab=readme-ov-file#json-converters),并将其关联到 [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md)。 你还可以直接在方法中获取 JSON-RPC 请求的 `$.params` 原始对象。 ```csharp {title="MathOperations.cs"} [WebMethod] public float Sum(JsonArray|JsonObject @params) { ... } ``` 为实现上述功能,`@params` 必须是方法中的 **唯一** 参数,且名称必须恰好为 `params`(在 C# 中,需要使用 `@` 来转义该参数名)。 参数反序列化同时支持具名对象和位置数组。例如,下面的方法可以通过两种请求方式远程调用: ```csharp [WebMethod] public float AddUserToStore(string apiKey, User user, UserStore store) { ... } ``` 对于数组请求,必须遵循参数顺序。 ```json { "jsonrpc": "2.0", "method": "AddUserToStore", "params": [ "1234567890", { "name": "John Doe", "email": "john@example.com" }, { "name": "My Store" } ], "id": 1 } ``` ## 自定义序列化器 你可以在 [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md) 属性中自定义 JSON 序列化器。通过该属性可以启用使用 [JSON5](https://json5.org/) 进行消息反序列化。虽然这并非 JSON-RPC 2.0 的规范要求,JSON5 作为 JSON 的扩展,允许更易读、书写更友好的格式。 ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder ( 5556 ) .UseJsonRPC ( ( o, e ) => { // 使用已清理的名称比较器。该比较器仅比较名称中的字母和数字,忽略其他符号。例如: // foo_bar10 == FooBar10 e.Handler.JsonSerializerOptions.PropertyNameComparer = new JsonSanitizedComparer ( ); // 为 JSON 解释器启用 JSON5。即使启用此功能,仍然支持普通 JSON e.Handler.JsonSerializerOptions.SerializationFlags = LightJson.Serialization.JsonSerializationFlags.Json5; // 将 POST /service 路由映射到 JSON RPC 处理器 e.Router.MapPost ( "/service", e.Handler.Transport.HttpPost ); } ) .Build ( ); host.Start ( ); ``` --- # SSL 代理 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/ssl-proxy.html > [!WARNING] > 此功能是实验性的,不应在生产环境中使用。如果您想让 Sisk 与 SSL 协作,请参阅 [此文档](https://docs.sisk-framework.org/zh-cn/docs/deploying.md#proxying-your-application)。 Sisk SSL 代理是一个模块,提供了 Sisk 中 [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) 的 HTTPS 连接,并将 HTTPS 消息路由到不安全的 HTTP 上下文。该模块是为使用 [HttpListener](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httplistener?view=net-8.0) 运行的服务提供 SSL 连接而构建的,因为它不支持 SSL。 代理在同一应用程序中运行,并侦听 HTTP/1.1 消息,将其以相同的协议转发给 Sisk。目前,此功能是高度实验性的,可能不稳定到不能在生产环境中使用。 目前,SslProxy 支持几乎所有 HTTP/1.1 功能,例如 keep-alive、分块编码、WebSockets 等。对于打开到 SSL 代理的连接,会创建一个到目标服务器的 TCP 连接,并将代理转发到已建立的连接。 SslProxy 可以与 HttpServer.CreateBuilder 一起使用,如下所示: ```csharp using var app = HttpServer.CreateBuilder(port: 5555) .UseRouter(r => { r.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); }) // 添加 SSL 到项目 .UseSsl( sslListeningPort: 5567, new X509Certificate2(@".\ssl.pfx", password: "12345") ) .Build(); app.Start(); ``` 您必须为代理提供有效的 SSL 证书。为了确保证书被浏览器接受,请记得将其导入到操作系统中,以便正确功能。 --- # 基本身份验证 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/basic-auth.html Basic Auth 包添加了一个请求处理程序,能够处理基本身份验证方案,并且只需进行很少的配置和努力,即可在 Sisk 应用程序中使用。 基本 HTTP 身份验证是一种最小的输入形式,通过用户 ID 和密码对请求进行身份验证,会话由客户端完全控制,并且没有身份验证或访问令牌。 ![Basic Auth](https://docs.sisk-framework.org/assets/img/basic-auth.svg) 有关基本身份验证方案的更多信息,请参阅 [MDN 规范](https://developer.mozilla.org/pt-BR/docs/cn/Web/HTTP/Authentication)。 ## 安装 要开始使用,请在您的项目中安装 Sisk.BasicAuth 包: > dotnet add package Sisk.BasicAuth 您可以在 [Nuget 存储库](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0) 中查看更多有关如何在项目中安装它的信息。 ## 创建身份验证处理程序 您可以控制整个模块或个别路由的身份验证方案。为此,让我们首先编写第一个基本身份验证处理程序。 在以下示例中,连接到数据库,检查用户是否存在以及密码是否有效,然后将用户存储在上下文包中。 ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "要进入此页面,请提供您的凭据。"; } public override HttpResponse? OnValidating(BasicAuthenticationCredentials credentials, HttpContext context) { DbContext db = new DbContext(); // 在这种情况下,我们使用电子邮件作为用户 ID 字段,因此我们将使用电子邮件查找用户。 User? user = db.Users.FirstOrDefault(u => u.Email == credentials.UserId); if (user == null) { return base.CreateUnauthorizedResponse("抱歉!没有找到此电子邮件的用户。"); } // 验证此用户的凭据密码是否有效。 if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("无效的凭据。"); } // 将已登录的用户添加到 HTTP 上下文中 // 并继续执行 context.Bag.Add("loggedUser", user); return null; } } ``` 因此,只需将此请求处理程序与我们的路由或类关联即可。 ```cs public class UsersController { [RouteGet("/")] [RequestHandler(typeof(UserAuthHandler))] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"您好,{loggedUser.Name}!"; } } ``` 或使用 [RouterModule](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.md) 类: ```cs public class UsersController : RouterModule { public ClientModule() { // 此类中的所有路由都将由 UserAuthHandler 处理。 base.HasRequestHandler(new UserAuthHandler()); } [RouteGet("/")] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"您好,{loggedUser.Name}!"; } } ``` ## 备注 基本身份验证的主要责任由客户端承担。存储、缓存控制和加密都在客户端本地处理。服务器只接收凭据并验证是否允许访问。 请注意,此方法不是最安全的,因为它将大量责任放在客户端上,客户端可能很难跟踪和维护其凭据的安全性。另外,密码必须在安全连接上下文(SSL)中传输,因为它们没有内置加密。请求头的简短拦截可能会暴露用户的访问凭据。 对于生产环境中的应用程序,请选择更强大的身份验证解决方案,并避免使用太多现成的组件,因为它们可能无法适应项目的需求,并最终将其暴露在安全风险之中。 --- # 服务提供者 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/service-providers.html 服务提供者是一种将 Sisk 应用程序移植到不同环境的方式,使用可移植的配置文件。该功能允许您在不修改应用程序代码的情况下更改服务器端口、参数和其他选项。该模块依赖于 Sisk 构造语法,可以通过 `UsePortableConfiguration` 方法进行配置。 一个配置提供者是通过 `IConfigurationProvider` 实现的,它提供了一个配置读取器,可以接收任何实现。默认情况下,Sisk 提供了一个 JSON 配置读取器,但也有一个用于 INI 文件的包。您也可以创建自己的配置提供者并注册它: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigReader(); }) .Build(); ``` 如前所述,默认提供者是一个 JSON 文件。默认情况下,文件名为 `service-config.json`,它在运行进程的当前目录中搜索,而不是可执行文件目录。 您可以选择更改文件名,以及 Sisk 应该在哪里查找配置文件: ```csharp using Sisk.Core.Http; using Sisk.Core.Http.Hosting; using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("config.toml", createIfDontExists: true, lookupDirectories: ConfigurationFileLookupDirectory.CurrentDirectory | ConfigurationFileLookupDirectory.AppDirectory); }) .Build(); ``` 上面的代码将在运行进程的当前目录中查找 `config.toml` 文件。如果找不到,它将在可执行文件所在的目录中查找。如果文件不存在,`createIfDontExists` 参数将被尊重,创建一个空文件,在最后测试的路径中(基于 `lookupDirectories`),并在控制台中抛出一个错误,防止应用程序初始化。 > [!TIP] > > 您可以查看 INI 配置读取器和 JSON 配置读取器的源代码,以了解如何实现 `IConfigurationProvider`。 ## 从 JSON 文件读取配置 默认情况下,Sisk 提供了一个配置提供者,用于从 JSON 文件读取配置。该文件遵循一个固定的结构,包含以下参数: ```json { "Server": { "DefaultEncoding": "UTF-8", "ThrowExceptions": true, "IncludeRequestIdHeader": true }, "ListeningHost": { "Label": "My sisk application", "Ports": [ "http://localhost:80/", "https://localhost:443/", // 配置文件也支持注释 ], "CrossOriginResourceSharingPolicy": { "AllowOrigin": "*", "AllowOrigins": [ "*" ], // 新增于 0.14 "AllowMethods": [ "*" ], "AllowHeaders": [ "*" ], "MaxAge": 3600 }, "Parameters": { "MySqlConnection": "server=localhost;user=root;" } } } ``` 从配置文件创建的参数可以在服务器构造函数中访问: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithParameters(paramCollection => { string databaseConnection = paramCollection.GetValueOrThrow("MySqlConnection"); }); }) .Build(); ``` 每个配置读取器提供了一种读取服务器初始化参数的方式。一些属性指示应在进程环境中设置,而不是在配置文件中定义,例如敏感的 API 数据、API 密钥等。 ## 配置文件结构 JSON 配置文件由以下属性组成:
    属性 必需 描述
    Server 必需 代表服务器本身及其设置。
    Server.AccessLogsStream 可选 默认为 console。指定访问日志输出流。可以是文件名、null 或 console。
    Server.ErrorsLogsStream 可选 默认为 null。指定错误日志输出流。可以是文件名、null 或 console。
    Server.MaximumContentLength 可选 默认为 0。指定最大内容长度(以字节为单位)。零表示无限。
    Server.IncludeRequestIdHeader 可选 默认为 false。指定是否应发送 X-Request-Id 标头。
    Server.ThrowExceptions 可选 默认为 true。指定是否应抛出未处理的异常。设置为 false 时为生产环境,设置为 true 时为调试环境。
    ListeningHost 必需 代表服务器监听主机。
    ListeningHost.Label 可选 代表应用程序标签。
    ListeningHost.Ports 必需 代表一个字符串数组,匹配 ListeningPort 语法。
    ListeningHost.CrossOriginResourceSharingPolicy 可选 设置应用程序的 CORS 标头。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials 可选 默认为 false。指定 Allow-Credentials 标头。
    ListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders 可选 默认为 null。此属性期望一个字符串数组。指定 Expose-Headers 标头。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin 可选 默认为 null。此属性期望一个字符串。指定 Allow-Origin 标头。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins 可选 默认为 null。此属性期望一个字符串数组。指定多个 Allow-Origin 标头。请参阅 AllowOrigins 以获取更多信息。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowMethods 可选 默认为 null。此属性期望一个字符串数组。指定 Allow-Methods 标头。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders 可选 默认为 null。此属性期望一个字符串数组。指定 Allow-Headers 标头。
    ListeningHost.CrossOriginResourceSharingPolicy.MaxAge 可选 默认为 null。此属性期望一个整数。指定 Max-Age 标头(以秒为单位)。
    ListeningHost.Parameters 可选 指定提供给应用程序设置方法的属性。
    --- # INI 配置提供程序 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/ini-configuration.html Sisk 有一种除了 JSON 之外的获取启动配置的方法。实际上,任何实现 [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader.md) 的管道都可以与 [PortableConfigurationBuilder.WithConfigurationPipeline](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.PortableConfigurationBuilder.md)一起使用,读取服务器配置从任何文件类型。 [Sisk.IniConfiguration](https://www.nuget.org/packages/Sisk.IniConfiguration/) 包提供了一个基于流的 INI 文件读取器,不会为常见的语法错误抛出异常,并且具有简单的配置语法。该包可以在 Sisk 框架之外使用,为需要高效 INI 文档读取器的项目提供灵活性。 ## 安装 要安装包,可以从以下开始: ```bash $ dotnet add package Sisk.IniConfiguration ``` 您也可以安装核心包,它不包括 INI [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader),也不包括 Sisk 依赖项,只包括 INI 序列化器: ```bash $ dotnet add package Sisk.IniConfiguration.Core ``` 使用主包,您可以在代码中使用它,如下面的示例所示: ```cs class Program { static HttpServerHostContext Host = null!; static void Main(string[] args) { Host = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("app.ini", createIfDontExists: true); // 使用 IniConfigurationReader 配置读取器 config.WithConfigurationPipeline(); }) .UseRouter(r => { r.MapGet("/", SayHello); }) .Build(); Host.Start(); } static HttpResponse SayHello(HttpRequest request) { string? name = Host.Parameters["name"] ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` 上面的代码将在进程的当前目录(CurrentDirectory)中查找 app.ini 文件。INI 文件如下所示: ```ini [Server] # 支持多个监听地址 Listen = http://localhost:5552/ Listen = http://localhost:5553/ ThrowExceptions = false AccessLogsStream = console [Cors] AllowMethods = GET, POST AllowHeaders = Content-Type, Authorization AllowOrigin = * [Parameters] Name = "Kanye West" ``` ## INI 风格和语法 当前实现风格: - 属性和节名称是 **大小写不敏感** 的。 - 属性名称和值是 **修剪** 的,除非值被引号括起来。 - 值可以用单引号或双引号括起来。引号内可以有换行符。 - 支持使用 `#` 和 `;` 的注释。**尾部注释也是允许的**。 - 属性可以有多个值。 详细来说,Sisk 中使用的 INI 解析器的“风格”文档 [可在此文档中找到](https://github.com/sisk-http/archive/blob/master/ext/ini-reader-syntax.md)。 使用以下 INI 代码作为示例: ```ini One = 1 Value = 这是一个值 Another value = "这个值 有一个换行符" ; 下面的代码有一些颜色 [some section] Color = Red Color = Blue Color = Yellow ; 不要使用黄色 ``` 解析它: ```csharp // 从字符串解析 INI 文本 IniDocument doc = IniDocument.FromString(iniText); // 获取一个值 string? one = doc.Global.GetOne("one"); string? anotherValue = doc.Global.GetOne("another value"); // 获取多个值 string[]? colors = doc.GetSection("some section")?.GetMany("color"); ``` ## 配置参数 | 节和名称 | 允许多个值 | 描述 | | ---------------- | --------------------- | ----------- | | `Server.Listen` | Yes | 服务器监听地址/端口。 | | `Server.Encoding` | No | 服务器默认编码。 | | `Server.MaximumContentLength` | No | 服务器最大内容长度(以字节为单位)。 | | `Server.IncludeRequestIdHeader` | No | 指定 HTTP 服务器是否应发送 X-Request-Id 标头。 | | `Server.ThrowExceptions` | No | 指定是否应抛出未处理的异常。 | | `Server.AccessLogsStream` | No | 指定访问日志输出流。 | | `Server.ErrorsLogsStream` | No | 指定错误日志输出流。 | | `Cors.AllowMethods` | No | 指定 CORS Allow-Methods 标头值。 | | `Cors.AllowHeaders` | No | 指定 CORS Allow-Headers 标头值。 | | `Cors.AllowOrigins` | No | 指定多个 Allow-Origin 标头,逗号分隔。 [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) 有更多信息。 | | `Cors.AllowOrigin` | No | 指定一个 Allow-Origin 标头。 | | `Cors.ExposeHeaders` | No | 指定 CORS Expose-Headers 标头值。 | | `Cors.AllowCredentials` | No | 指定 CORS Allow-Credentials 标头值。 | | `Cors.MaxAge` | No | 指定 CORS Max-Age 标头值。 --- # API 文档 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/api-documentation.html `Sisk.Documenting` 扩展允许您自动为 Sisk 应用程序生成 API 文档。它利用您的代码结构和特性来创建一个完整的文档站点,支持导出为 Open API(Swagger)格式。 > [!WARNING] > 此软件包目前仍在开发中,尚未发布。其行为和 API 可能会在未来的更新中发生更改。 由于此软件包尚未在 NuGet 上提供,您必须将源代码直接合并到项目中或将其作为项目依赖引用。您可以在[此处](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Documenting)访问源代码。 要使用 `Sisk.Documenting`,您需要在应用程序构建器中注册它,并在路由处理程序上使用文档特性进行装饰。 ### 注册文档生成 在您的 `HttpServerHostContextBuilder` 上使用 `UseApiDocumentation` 扩展方法,以从提供应用程序的同一路由器公开生成的 API 文档。 ```csharp using Sisk.Documenting; using Sisk.Documenting.Exporters; // ... host.UseApiDocumentation( context: new ApiGenerationContext() { ApplicationName = "My Application", ApplicationDescription = "Description of my application.", ApplicationVersion = "1.0.0" }, routerPath: "/api/docs", exporter: new OpenApiExporter() { ServerUrls = ["http://localhost:5555/"] }); ``` - **context**:定义有关您的应用程序的元数据,例如名称、描述和版本。 - **routerPath**:文档用户界面(或 JSON)可访问的 URL 路径。 - **exporter**:配置文档的导出方式。`OpenApiExporter` 启用 Open API(Swagger)支持。 ### 为端点编写文档 您可以在路由处理方法上使用 `[ApiEndpoint]` 和 `[ApiQueryParameter]` 特性来描述端点。 ### `ApiEndpoint` `[ApiEndpoint]` 特性允许您为端点提供描述。 ```csharp [ApiEndpoint(Description = "Returns a greeting message.")] public HttpResponse Index(HttpRequest request) { ... } ``` ### `ApiQueryParameter` `[ApiQueryParameter]` 特性记录端点接受的查询字符串参数。 ```csharp [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { ... } ``` - **name**:查询参数的名称。 - **IsRequired**:指定该参数是否为必需。 - **Description**:参数的人类可读描述。 - **Type**:预期的数据类型(例如 "string", "int")。 ### `ApiEndpoint` 为端点添加通用信息的注解。 * **Name** (string, required in constructor):API 端点的名称。 * **Description** (string):对端点功能的简要描述。 * **Group** (string):允许对端点进行分组(例如按控制器或模块)。 * **InheritDescriptionFromXmlDocumentation** (bool, default: `true`):如果为 `true`,且未设置 `Description`,则尝试使用方法的 XML 文档摘要。 ### `ApiHeader` 记录端点期望或使用的特定 HTTP 头部。 * **HeaderName** (string, required in constructor):头部的键(例如 "Authorization")。 * **Description** (string):描述该头部的用途。 * **IsRequired** (bool):指示该头部在请求中是否为必需。 ### `ApiParameter` 为端点定义通用参数,常用于表单字段或未被其他特性覆盖的请求体参数。 * **Name** (string, required in constructor):参数的名称。 * **TypeName** (string, required in constructor):参数的数据类型(例如 "string", "int")。 * **Description** (string):参数的描述。 * **IsRequired** (bool):指示该参数是否为必需。 ### `ApiParametersFrom` 自动从指定类或类型的属性生成参数文档。 * **Type** (Type, required in constructor):要反射属性的类 `Type`。 ### `ApiPathParameter` 记录路径变量(例如 `/users/{id}`)。 * **Name** (string, required in constructor):路径参数的名称。 * **Description** (string):描述该参数代表的含义。 * **Type** (string):预期的数据类型。 ### `ApiQueryParameter` 记录查询字符串参数(例如 `?page=1`)。 * **Name** (string, required in constructor):查询参数的键。 * **Description** (string):描述该参数。 * **Type** (string):预期的数据类型。 * **IsRequired** (bool):指示该查询参数是否必须存在。 ### `ApiRequest` 描述预期的请求体。 * **Description** (string, required in constructor):请求体的描述。 * **Example** (string):包含请求体示例的原始字符串。 * **ExampleLanguage** (string):示例的语言(例如 "json", "xml")。 * **PayloadType** (Type):如果设置,示例和模式将根据此类型自动生成(前提是已配置的上下文处理程序支持)。 ### `ApiResponse` 描述端点可能的响应。 * **StatusCode** (HttpStatusCode, required in constructor):返回的 HTTP 状态码(例如 `HttpStatusCode.OK`)。 * **Description** (string):描述此响应的情况。 * **Example** (string):包含响应体示例的原始字符串。 * **ExampleLanguage** (string):示例的语言。 * **PayloadType** (Type):如果设置,示例和模式将根据此类型自动生成(前提是已配置的上下文处理程序支持)。 ## 类型处理程序 类型处理程序负责将您的 .NET 类型(类、枚举等)转换为文档示例。这对于基于数据模型自动生成请求和响应体示例特别有用。 这些处理程序在 `ApiGenerationContext` 中进行配置。 ```csharp using Sisk.Documenting.Content; var context = new ApiGenerationContext() { // ... BodyExampleTypeHandler = new JsonContentTypeHandler(), ParameterExampleTypeHandler = new JsonContentTypeHandler(), ContentSchemaTypeHandler = new JsonContentTypeHandler() }; ``` ### JsonContentTypeHandler `JsonContentTypeHandler` 是内置处理程序,可生成 JSON 示例、参数示例和 JSON 模式。它实现了 `IExampleBodyTypeHandler`、`IExampleParameterTypeHandler` 和 `IContentSchemaTypeHandler`。 它可以使用特定的 `JsonSerializerOptions` 或 `IJsonTypeInfoResolver` 进行自定义,以匹配您应用程序的序列化逻辑。 ```csharp var jsonHandler = new JsonContentTypeHandler(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); context.BodyExampleTypeHandler = jsonHandler; context.ParameterExampleTypeHandler = jsonHandler; context.ContentSchemaTypeHandler = jsonHandler; ``` ### 自定义类型处理程序 您可以实现自己的处理程序以支持其他格式(如 XML)或自定义示例生成方式。 #### IExampleBodyTypeHandler 实现此接口以为请求和响应类型生成体示例。 ```csharp public class XmlExampleTypeHandler : IExampleBodyTypeHandler { public BodyExampleResult? GetBodyExampleForType(Type type) { // Generate XML string for the type string xmlContent = MyXmlGenerator.Generate(type); return new BodyExampleResult(xmlContent, "xml"); } } ``` #### IExampleParameterTypeHandler 实现此接口以从类型生成详细的参数描述(供 `[ApiParametersFrom]` 使用)。 ```csharp public class CustomParameterHandler : IExampleParameterTypeHandler { public ParameterExampleResult[] GetParameterExamplesForType(Type type) { var properties = type.GetProperties(); var examples = new List(); foreach (var prop in properties) { examples.Add(new ParameterExampleResult( name: prop.Name, typeName: prop.PropertyType.Name, isRequired: true, description: "Generated description" )); } return examples.ToArray(); } } ``` ## 导出器 导出器负责将收集的 API 文档元数据转换为可供其他工具使用或向用户展示的特定格式。 ### OpenApiExporter 默认提供的导出器是 `OpenApiExporter`,它生成符合 [OpenAPI Specification 3.0.0](https://spec.openapis.org/oas/v3.0.0) 的 JSON 文件。 ```csharp new OpenApiExporter() { OpenApiVersion = "3.0.0", ServerUrls = new[] { "http://localhost:5555" }, Contact = new OpenApiContact() { Name = "Support", Email = "support@example.com", Url = "https://example.com/support" }, License = new OpenApiLicense() { Name = "MIT", Url = "https://opensource.org/licenses/MIT" }, TermsOfService = "https://example.com/terms" } ``` ### 创建自定义导出器 您可以通过实现 `IApiDocumentationExporter` 接口来创建自己的导出器。这使您能够以 Markdown、HTML、Postman Collection 或任何其他自定义格式输出文档。 该接口要求实现一个方法:`ExportDocumentationContent`。 ```csharp using Sisk.Core.Http; using Sisk.Documenting; public class MyCustomExporter : IApiDocumentationExporter { public HttpContent ExportDocumentationContent(ApiDocumentation documentation) { // 1. Process the documentation object var sb = new StringBuilder(); sb.AppendLine($"# {documentation.ApplicationName}"); foreach(var endpoint in documentation.Endpoints) { sb.AppendLine($"## {endpoint.Method} {endpoint.Path}"); sb.AppendLine(endpoint.Description); } // 2. Return the content as an HttpContent return new StringContent(sb.ToString(), Encoding.UTF8, "text/markdown"); } } ``` 然后,在配置中直接使用它: ```csharp host.UseApiDocumentation( // ... exporter: new MyCustomExporter() ); ``` ### 完整示例 下面是一个完整示例,演示如何设置 `Sisk.Documenting` 并为一个简单的控制器编写文档。 ```csharp using Sisk.Core.Entity; using Sisk.Core.Http; using Sisk.Core.Routing; using Sisk.Documenting; using Sisk.Documenting.Annotations; using Sisk.Documenting.Exporters; using var host = HttpServer.CreateBuilder(5555) .UseCors(CrossOriginResourceSharingHeaders.CreatePublicContext()) .UseApiDocumentation( context: new ApiGenerationContext() { ApplicationName = "My application", ApplicationDescription = "It greets someone." }, routerPath: "/api/docs", exporter: new OpenApiExporter() { ServerUrls = ["http://localhost:5555/"] }) .UseRouter(router => { router.MapInstance(new MyController()); }) .Build(); await host.StartAsync(); class MyController { [RouteGet] [ApiEndpoint(Description = "Returns a greeting message.")] [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { string? name = request.Query["name"].MaybeNullOrEmpty() ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` 在此示例中,访问 `/api/docs` 将提供 “My application” API 的生成文档,描述 `GET /` 端点及其 `name` 参数。 --- # 手动(高级)设置 Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/manual-setup.html 当您需要自行组装服务器组件时使用手动设置,例如一个进程必须暴露多个主机、端口、路由器或自定义服务器配置。对于大多数应用程序,构建器 API 更简洁,应该优先使用。手动设置在您想直接控制四个核心部件时非常有用:`Router`、一个或多个 `ListeningHost` 对象、`HttpServerConfiguration`,以及最终的 `HttpServer`。 首先,我们需要了解请求/响应的概念。它非常简单:每个请求必须有一个响应。Sisk 也遵循这一原则。让我们创建一个方法,以 HTML 返回 “Hello, World!” 消息,并指定状态码和头部。 ```csharp // Program.cs using Sisk.Core.Http; using Sisk.Core.Routing; static HttpResponse IndexPage(HttpRequest request) { HttpResponse indexResponse = new HttpResponse { Status = System.Net.HttpStatusCode.OK, Content = new HtmlContent(@"

    Hello, world!

    ") }; return indexResponse; } ``` 下一步是将此方法关联到一个 HTTP 路由。 ## 路由器 路由器是请求路由的抽象,充当请求与响应之间的桥梁。路由器管理服务路由、函数和错误。 一个路由器可以拥有多个路由,每个路由可以在该路径上执行不同的操作,例如执行函数、提供页面或返回服务器资源。 让我们创建第一个路由器,并将 `IndexPage` 方法关联到根路径。 ```csharp Router mainRouter = new Router(); mainRouter.MapGet("/", IndexPage); ``` 现在我们的路由器可以接收请求并发送响应。然而,`mainRouter` 并未绑定到主机或服务器,单独使用是无效的。下一步是创建我们的 ListeningHost。 ## 监听主机和端口 一个 [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) 可以托管一个路由器,并为同一路由器提供多个监听端口。一个 [ListeningPort](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningPort.md) 是 HTTP 服务器将监听的前缀。 这里,我们可以创建一个指向两个端点的 `ListeningHost`: ```csharp ListeningHost myHost = new ListeningHost { Router = mainRouter, Ports = new ListeningPort[] { new ListeningPort("http://localhost:5000/") } }; ``` 现在我们的 HTTP 服务器将监听指定的端点,并将请求转发到我们的路由器。 ## 服务器配置 服务器配置负责大部分 HTTP 服务器本身的行为。在此配置中,我们可以将 `ListeningHosts` 与服务器关联。 ```csharp HttpServerConfiguration config = new HttpServerConfiguration(); config.ListeningHosts.Add(myHost); // 将我们的 ListeningHost 添加到此服务器配置中 ``` 常用服务器配置选项: | Property | Default | 使用场景 | 备注 | | --- | --- | --- | --- | | [RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) | `RequestListenAction.Accept` | 服务应拒绝非本地请求,除非它们通过受信任的反向代理进入。 | 仅在部署拓扑明确时将其设为 `Drop`。 | | [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) | `false` | 客户端或代理需要在 `X-Request-Id` 响应头中看到 Sisk 请求 ID。 | 与包含 `HttpRequest.RequestId` 的日志配合使用。 | | [IdleConnectionTimeout](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IdleConnectionTimeout.md) | `120` 秒 | 空闲的 Keep-Alive 连接应在适当时机关闭。 | 由 HTTP 引擎实现。 | | [NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) | `false` | 您收到的头部存在编码不匹配。 | 处理会有额外开销,除非必要请保持关闭。 | | [SendSiskHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.SendSiskHeader.md) | `true` | 您想隐藏或暴露 `X-Powered-By` Sisk 头部。 | 为更严格的生产环境头部策略请禁用。 | | [OptionsLogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.OptionsLogMode.md) | `LogOutput.Both` | 您想减少或重定向自动 `OPTIONS` 处理产生的日志。 | 使用与路由相同的日志模式值。 | | [AsyncRequestProcessing](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AsyncRequestProcessing.md) | `true` | 您需要确定性的单请求处理以便诊断。 | 禁用会限制吞吐量。 | | [DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) | `true` | 实现了 `IDisposable` 的请求袋值应自动释放。 | 除非在其他地方管理所有权,否则保持启用。 | | [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) | `true` | 值处理器应将异步可枚举转换为阻塞的可枚举值。 | 当您自行实现异步流处理时请禁用。 | | [KeepAlive](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.KeepAlive.md) | `true` | 响应后连接应保持可复用。 | 对于不善于处理持久连接的客户端或中间件请禁用。 | | [ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) | `false` | GET 路由应重定向到带尾随斜杠的 URL。 | 仅适用于非正则路由。 | | [MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) | `0` | 请求体需要大小限制。 | `0` 表示无限制,直至框架或内存上限。 | | [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) | `false` | 当客户端支持时应自动压缩响应。 | 已经是 `CompressedContent` 的响应不会再次压缩。 | 接下来,我们可以创建 HTTP 服务器: ```csharp HttpServer server = new HttpServer(config); server.Start(); // 启动服务器 Console.ReadKey(); // 防止应用程序退出 ``` 现在我们可以编译可执行文件并使用以下命令运行 HTTP 服务器: ```bash dotnet watch ``` 运行时,打开浏览器并访问服务器路径,您应该会看到: --- # 请求生命周期 Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/request-lifecycle.html 下面通过一个 HTTP 请求的示例解释请求的完整生命周期。 - **接收请求:** 每个请求在请求本身和将要发送给客户端的响应之间创建一个 HTTP 上下文。该上下文来自 Sisk 内置的监听器,可以是 [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0)、[Kestrel](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-9.0) 或 [Cadente](https://blog.sisk-framework.org/posts/2025-01-29-cadente-experiment/)。 - 外部请求验证:对请求进行 [HttpServerConfiguration.RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) 的验证。 - 如果请求是外部的且属性为 `Drop`,则在不向客户端返回响应的情况下关闭连接,`HttpServerExecutionStatus = RemoteRequestDropped`。 - 转发解析器配置:如果配置了 [ForwardingResolver](https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.md),它将在请求的原始主机上调用 [OnResolveRequestHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.OnResolveRequestHost.md) 方法。 - DNS 匹配:在解析出主机且配置了多个 [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) 时,服务器会为请求查找相应的主机。 - 如果没有匹配的 ListeningHost,服务器返回 400 Bad Request 响应给客户端,并向 HTTP 上下文返回 `HttpServerExecutionStatus = DnsUnknownHost` 状态。 - 如果匹配到 ListeningHost,但其 [Router](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.Router.md) 尚未初始化,服务器返回 503 Service Unavailable 响应给客户端,并向 HTTP 上下文返回 `HttpServerExecutionStatus = ListeningHostNotReady` 状态。 - 路由绑定:将相应 ListeningHost 的路由器与接收到的 HTTP 服务器关联。 - 如果路由器已经关联到另一个 HTTP 服务器,则会抛出 `InvalidOperationException`,因为路由器会主动使用服务器的配置资源,这在 HTTP 服务器初始化期间才会发生,而不会在创建 HTTP 上下文时发生。 - 预定义头部: - 如果已配置,则在响应中预定义 `X-Request-Id` 头部。 - 如果已配置,则在响应中预定义 `X-Powered-By` 头部。 - 内容大小验证:仅当配置值大于零时,验证请求内容是否小于 [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md)。 - 如果请求发送的 `Content-Length` 大于配置的值,服务器返回 413 Payload Too Large 响应给客户端,并向 HTTP 上下文返回 `HttpServerExecutionStatus = ContentTooLarge` 状态。 - `OnHttpRequestOpen` 事件会对所有已配置的 HTTP 服务器处理程序触发。 - **路由操作:** 服务器为接收到的请求调用路由器。 - 如果路由器未找到匹配请求的路由: - 如果已配置 [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) 属性,则调用该操作,并将操作的响应转发给 HTTP 客户端。 - 如果该属性为 null,则返回默认的 404 Not Found 响应给客户端。 - 如果路由器找到匹配的路由,但该路由的方法与请求的方法不匹配: - 如果已配置 [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) 属性,则调用该操作,并将操作的响应转发给 HTTP 客户端。 - 如果该属性为 null,则返回默认的 405 Method Not Allowed 响应给客户端。 - 如果请求使用 `OPTIONS` 方法: - 仅当没有路由匹配请求方法(路由的方法未显式标记为 [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md))时,路由器才返回 200 Ok 响应给客户端。 - 如果启用了 [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) 属性,且匹配的路由不是正则表达式,请求路径未以 `/` 结尾,并且请求方法为 `GET`: - 服务器返回 307 Temporary Redirect HTTP 响应,`Location` 头部指向相同路径并在末尾添加 `/`,返回给客户端。 - `OnContextBagCreated` 事件会对所有已配置的 HTTP 服务器处理程序触发。 - 执行所有全局的带有 `BeforeResponse` 标记的 [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) 实例。 - 如果任意处理程序返回非 null 响应,则将该响应转发给 HTTP 客户端并关闭上下文。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 被禁用: - 如果已启用 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义该属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 执行路由中定义的且带有 `BeforeResponse` 标记的所有 [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) 实例。 - 如果任意处理程序返回非 null 响应,则将该响应转发给 HTTP 客户端并关闭上下文。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 被禁用: - 如果已启用 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义该属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 调用路由器的操作并将其转换为 HTTP 响应。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 被禁用: - 如果已启用 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义该属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 执行所有全局的带有 `AfterResponse` 标记的 [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) 实例。 - 如果任意处理程序返回非 null 响应,则该处理程序的响应替换之前的响应并立即转发给 HTTP 客户端。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 被禁用: - 如果已启用 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义该属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 执行路由中定义的且带有 `AfterResponse` 标记的所有 [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) 实例。 - 如果任意处理程序返回非 null 响应,则该处理程序的响应替换之前的响应并立即转发给 HTTP 客户端。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 被禁用: - 如果已启用 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义该属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - **处理响应:** 当响应准备好后,服务器会为发送给客户端做准备。 - 根据当前 [ListeningHost.CrossOriginResourceSharingPolicy](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.CrossOriginResourceSharingPolicy.md) 的配置,在响应中定义跨域资源共享策略(CORS)头部。 - 将响应的状态码和头部发送给客户端。 - 将响应内容发送给客户端: - 如果响应内容是 [ByteArrayContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent) 的子类,则直接将响应字节复制到响应输出流。 - 如果不满足前述条件,则将响应序列化为流并复制到响应输出流。 - 关闭流并丢弃响应内容。 - 如果启用了 [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md),则会丢弃请求上下文中所有继承自 [IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable) 的对象。 - `OnHttpRequestClose` 事件会对所有已配置的 HTTP 服务器处理程序触发。 - 如果服务器抛出异常,则 `OnException` 事件会对所有已配置的 HTTP 服务器处理程序触发。 - 如果路由允许访问日志且 [HttpServerConfiguration.AccessLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AccessLogsStream.md) 不为 null,则会向日志输出写入一行日志。 - 如果路由允许错误日志、出现异常且 [HttpServerConfiguration.ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) 不为 null,则会向错误日志输出写入一行日志。 - 如果服务器正通过 [HttpServer.WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) 等待请求,则释放互斥锁并使上下文对用户可用。 --- # 转发解析器 Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.html Forwarding Resolver 是一个帮助解码通过请求、代理、CDN 或负载均衡器识别客户端信息的工具。当您的 Sisk 服务通过反向或正向代理运行时,客户端的 IP 地址、主机和协议可能与原始请求不同,因为这是从一个服务转发到另一个服务。此 Sisk 功能允许您在处理请求之前控制并解析这些信息。这些代理通常会提供有用的头部来识别其客户端。 目前,使用 [ForwardingResolver](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.md) 类,可以解析客户端的 IP 地址、主机以及使用的 HTTP 协议。自 Sisk 1.0 版本之后,出于安全原因且因服务而异,服务器不再提供标准实现来解码这些头部。 例如,`X-Forwarded-For` 头部包含了转发请求的 IP 地址信息。代理使用此头部将信息链传递给最终服务,并包含所有使用的代理的 IP,包括客户端的真实地址。问题在于:有时很难识别客户端的远程 IP,并且没有特定的规则来识别此头部。强烈建议阅读下面即将实现的头部文档: - 阅读关于 `X-Forwarded-For` 头部的文档[此处](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns)。 - 阅读关于 `X-Forwarded-Host` 头部的文档[此处](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Headers/X-Forwarded-Host)。 - 阅读关于 `X-Forwarded-Proto` 头部的文档[此处](https://developer.mozilla.org/en-US/docs/cn/Web/HTTP/Headers/X-Forwarded-Proto)。 ## ForwardingResolver 类 此类拥有三个虚方法,允许为每个服务提供最合适的实现。每个方法负责通过代理解析请求中的信息:客户端的 IP 地址、请求的主机以及使用的安全协议。默认情况下,Sisk 将始终使用原始请求中的信息,而不解析任何头部。 下面的示例展示了如何使用此实现。该示例通过 `X-Forwarded-For` 头部解析客户端的 IP,并在请求中转发了多个 IP 时抛出错误。 > [!IMPORTANT] > 请勿在生产代码中使用此示例。始终检查实现是否适合使用。在实现之前请阅读头部文档。 ```cs class Program { static void Main(string[] args) { using var host = HttpServer.CreateBuilder() .UseForwardingResolver() .UseListeningPort(5555) .Build(); host.Router.MapAny(Route.AnyPath, request => new HttpResponse("Hello, world!!!")); host.Start(); } class Resolver : ForwardingResolver { public override IPAddress OnResolveClientAddress(HttpRequest request, IPEndPoint connectingEndpoint) { string? forwardedFor = request.Headers.XForwardedFor; if (forwardedFor is null) { throw new Exception("The X-Forwarded-For header is missing."); } string[] ipAddresses = forwardedFor.Split(','); if (ipAddresses.Length != 1) { throw new Exception("Too many addresses in the X-Forwarded-For header."); } return IPAddress.Parse(ipAddresses[0]); } } } ``` --- # Http server handlers Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/http-server-handlers.html 在 Sisk 0.16 版本中,我们引入了 `HttpServerHandler` 类,旨在扩展 Sisk 的整体行为并为 Sisk 提供额外的事件处理程序,例如处理 Http 请求、路由、上下文袋等。 该类集中处理整个 HTTP 服务器以及单个请求生命周期中发生的事件。Http 协议没有会话概念,因此无法在请求之间保留信息。Sisk 目前提供了一种方式,让您实现会话、上下文、数据库连接以及其他有用的提供程序,以帮助您的工作。 请参阅 [此页面](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) 了解每个事件的触发时机及其目的。您也可以查看 [HTTP 请求的生命周期](https://docs.sisk-framework.org/zh-cn/docs/advanced/request-lifecycle.md) 以了解请求的处理过程以及事件的触发位置。HTTP 服务器允许您同时使用多个处理程序。每次事件调用都是同步的,即它会阻塞当前线程,直至与该函数关联的所有处理程序执行完毕。 与 RequestHandlers 不同,它们不能应用于某些路由组或特定路由,而是作用于整个 HTTP 服务器。您可以在 Http Server Handler 中添加条件。此外,每个 Sisk 应用程序只会为每个 `HttpServerHandler` 定义一个单例,因此每种 `HttpServerHandler` 只会有一个实例。 使用 HttpServerHandler 的一个实际例子是:在请求结束时自动释放数据库连接。 ```cs // DatabaseConnectionHandler.cs public class DatabaseConnectionHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { var requestBag = result.Request.Context.RequestBag; // 检查请求的上下文袋中是否已定义 DbContext if (requestBag.IsSet()) { var db = requestBag.Get(); db.Dispose(); } } } public static class DatabaseConnectionHandlerExtensions { public static DbContext GetDbContext(this HttpRequest request) { return request.Bag.GetOrAdd(() => new DbContext()); } } ``` 有了上述代码,`GetDbContext` 扩展方法允许直接从 `HttpRequest` 对象创建连接上下文。未释放的连接在使用数据库时可能导致问题,因此在 `OnHttpRequestClose` 中将其终止。 您可以在构建器中或直接使用 [HttpServer.RegisterHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.RegisterHandler.md) 在 Http 服务器上注册处理程序。 ```cs // Program.cs class Program { static void Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseHandler() .Build(); app.Router.MapInstance(new UserController()); app.Start(); } } ``` 这样,`UsersController` 类即可像下面这样使用数据库上下文: ```cs // UserController.cs [RoutePrefix("/users")] public class UserController : ApiController { [RouteGet()] public async Task List(HttpRequest request) { var db = request.GetDbContext(); var users = db.Users.ToArray(); return JsonOk(users); } [RouteGet("")] public async Task View(HttpRequest request) { var db = request.GetDbContext(); int userId = request.RouteParameters["id"].GetInteger(); var user = db.Users.FirstOrDefault(u => u.Id == userId); return JsonOk(user); } [RoutePost] public async Task Create(HttpRequest request) { var db = request.GetDbContext(); var user = await request.GetJsonContentAsync(); ArgumentNullException.ThrowIfNull(user); db.Users.Add(user); await db.SaveChangesAsync(); return JsonMessage("用户已添加。"); } } ``` 上述代码使用了 `JsonOk` 和 `JsonMessage` 方法,这些方法内置于 `ApiController`,而 `ApiController` 继承自 `RouterController`: ```cs // ApiController.cs public class ApiController : RouterModule { public HttpResponse JsonOk(object value) { return new HttpResponse(200) .WithContent(JsonContent.Create(value, null, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true })); } public HttpResponse JsonMessage(string message, int statusCode = 200) { return new HttpResponse(statusCode) .WithContent(JsonContent.Create(new { Message = message })); } } ``` 开发者可以使用此类实现会话、上下文和数据库连接。提供的代码展示了一个使用 `DatabaseConnectionHandler` 的实用示例,在每个请求结束时自动释放数据库连接。 集成非常简便,只需在服务器设置期间注册处理程序。`HttpServerHandler` 类为在 HTTP 应用中管理资源和扩展 Sisk 行为提供了强大的工具集。 --- # 每个服务器的多个监听主机 Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/multi-host-setup.html Sisk Framework 一直支持在每个服务器上使用多个主机,也就是说,一个 HTTP 服务器可以监听多个端口,每个端口都有自己的路由器和在其上运行的服务。 这样,就可以轻松地在单个 HTTP 服务器上使用 Sisk 分离职责并管理服务。下面的示例展示了创建两个 ListeningHost,每个监听不同的端口,使用不同的路由器和操作。 阅读 [manually creating your app](https://docs.sisk-framework.org/zh-cn/docs/advanced/manual-setup.md) 以了解此抽象的细节。 ```cs static void Main(string[] args) { // 创建两个监听主机,每个都有自己的路由器并 // 监听各自的端口 // ListeningHost hostA = new ListeningHost(); hostA.Ports = [new ListeningPort(12000)]; hostA.Router = new Router(); hostA.Router.MapGet("/", request => new HttpResponse().WithContent("来自主机 A 的问候!")); ListeningHost hostB = new ListeningHost(); hostB.Ports = [new ListeningPort(12001)]; hostB.Router = new Router(); hostB.Router.MapGet("/", request => new HttpResponse().WithContent("来自主机 B 的问候!")); // 创建服务器配置并将两个 // 监听主机添加进去 // HttpServerConfiguration configuration = new HttpServerConfiguration(); configuration.ListeningHosts.Add(hostA); configuration.ListeningHosts.Add(hostB); // 创建使用指定配置的 HTTP 服务器 // HttpServer server = new HttpServer(configuration); // 启动服务器 server.Start(); Console.WriteLine("尝试访问主机 A:{0}", server.ListeningPrefixes[0]); Console.WriteLine("尝试访问主机 B:{0}", server.ListeningPrefixes[1]); Thread.Sleep(-1); } ``` --- # HTTP 服务器引擎 Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/server-engines.html Sisk Framework 被分成几个包,其中主包(Sisk.HttpServer)不包含一个基本的 HTTP 服务器 - 默认情况下,[HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) 被用作 Sisk 的主要引擎来执行服务器的低级别角色。 HTTP 引擎实现了 Sisk 提供的应用层以下的层次。这个层次负责连接管理、消息的序列化和反序列化、消息队列控制和与机器的 socket 通信。 [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md) 类暴露了一个 API 来实现所有必要的 HTTP 引擎功能,以便在 Sisk 的上层使用,例如路由、SSE、中间件等。这些功能不是 HTTP 引擎的责任,而是将使用 HTTP 引擎作为基础的库的子集的责任。 通过这种抽象,可以将 Sisk 移植到使用任何其他 HTTP 引擎,既可以是 .NET 也可以不是 .NET 的,例如 Kestrel。目前,Sisk 仍然使用对本地 .NET [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) 的抽象作为新项目的默认值。这个默认抽象带来了一些特定的问题,例如在不同平台上的行为未指定(HttpListener 有 Windows 和其他平台的不同实现)、缺乏对 SSL 的支持以及在 Windows 之外的性能不佳。 也有一种实验性的高性能服务器实现,纯粹用 C# 编写,作为 Sisk 的 HTTP 引擎,称为 [Cadente](https://github.com/sisk-http/core/tree/main/cadente) 项目,这是一个可以与 Sisk 或不与 Sisk 一起使用的托管服务器的实验。 ## 实现 Sisk 的 HTTP 引擎 您可以通过扩展 [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md) 类来创建一个现有 HTTP 服务器和 Sisk 之间的连接桥梁。除了这个类,您还需要实现上下文、请求和响应的抽象。 一个完整的抽象示例可以在 [GitHub](https://github.com/sisk-http/core/blob/main/src/Http/Engine/HttpListenerAbstractEngine.cs) 上找到。它看起来像这样: ```csharp /// /// 提供使用 的 实现。 /// public sealed class HttpListenerAbstractEngine : HttpServerEngine { private HttpListener _listener; private static Lazy shared = new Lazy ( () => new HttpListenerAbstractEngine () ); /// /// 获取 类的共享实例。 /// public static HttpListenerAbstractEngine Shared => shared.Value; /// /// 初始化 类的新实例。 /// public HttpListenerAbstractEngine () { _listener = new HttpListener { IgnoreWriteExceptions = true }; } /// public override TimeSpan IdleConnectionTimeout { get => _listener.TimeoutManager.IdleConnection; set => _listener.TimeoutManager.IdleConnection = value; } // ... } ``` ## 选择事件循环 在创建 HTTP 引擎时,服务器将监听请求并在单独的线程中创建上下文来处理每一个请求。为此,您需要选择一个 [HttpServerEngineContextEventLoopMechanism](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngineContextEventLoopMechanism.md): - `InlineAsynchronousGetContext` 事件循环是线性的 - HTTP 上下文处理调用发生在异步循环中。 - `UnboundAsynchronousGetContext` 事件循环通过 `BeginGetContext` 和 `EndGetContext` 方法传递。 ```csharp public override HttpServerEngineContextEventLoopMechanism EventLoopMechanism => HttpServerEngineContextEventLoopMechanism.UnboundAsynchronousGetContext; ``` 您不需要实现两个事件循环。选择最适合您的 HTTP 引擎的一个。 ## 测试 在链接您的 HTTP 引擎后,执行测试以确保所有 Sisk 功能在使用其他引擎时具有相同的行为是非常重要的。**确保 Sisk 在不同 HTTP 引擎下的行为相同是非常重要的**。 您可以访问测试仓库 [GitHub](https://github.com/sisk-http/core/tree/main/tests)。