# 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。 Sisk 1.7 支持 .NET 8、.NET 9 和 .NET 10。[Cadente 引擎](https://docs.sisk-framework.org/zh-cn/docs/cadente.md) 包仅支持 .NET 9 和 .NET 10。 --- # 本机 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 以运行应用程序。您可以了解如何在 Linux 服务器上安装 .NET Runtime [此处](https://learn.microsoft.com/en-us/dotnet/core/install/linux), [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 publish 命令: ```shell $ dotnet publish -r linux-x64 -c Release ``` 这将在 `bin/Release/publish/linux-x64` 中生成您的二进制文件。 > [!NOTE] > 如果您的应用程序使用[可移植配置文件](https://docs.sisk-framework.org/zh-cn/docs/extensions/service-providers.md),应将 `service-config.json` 与 `dotnet publish` 生成的所有二进制文件一起复制到宿主服务器。您可以保留该文件的预配置内容,包括环境变量、监听端口和主机以及其他服务器配置。 下一步是将这些文件传输到将要托管应用程序的服务器上。 之后,为二进制文件授予执行权限。在本例中,假设我们的项目名称为 “my-app”: ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` 运行应用程序后,检查是否有错误信息产生。如果没有产生错误,则说明您的应用程序正在运行。 此时,由于防火墙等访问规则尚未配置,可能无法从服务器外部的外部网络访问您的应用程序。我们将在后续步骤中处理此问题。 您应该知道应用程序监听的虚拟主机地址。该地址在应用程序中手动设置,取决于您如何实例化 Sisk 服务。 如果您**未**使用可移植配置文件,则应在定义 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)); ``` 或者如果您使用可移植配置文件,则在 `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),则可以在 Sisk 服务中使用 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 - 工作量:容易 可以在 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")) ``` > Note: this package is still in the experimental phase. ## 通过 Windows 上的 IIS - 可用平台:Windows - 工作量:中等 如果你使用 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 - 工作量:容易 **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 包(已停用) > [!CAUTION] > > Sisk.SslProxy 包已在 NuGet 上停用并弃用。它已无法针对当前的 Cadente API 编译。请勿使用它:改为使用 [Cadente 引擎](#through-the-siskcadentecoreengine) 的原生 SSL 支持。 --- # 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 等反向代理)以及性能特性各异。 Cadente 旨在通过提供一个用 C# 编写的完全托管的 HTTP/1.1 服务器来解决这些问题。其主要目标包括: - **Native SSL Support:** 在所有平台上工作,无需外部代理或复杂配置。 - **Cross-Platform Consistency:** 在 Windows、Linux 和 macOS 上表现一致。 - **Performance:** 旨在成为托管 `HttpListener` 的高性能替代方案。 - **Independence:** 与 `System.Net.HttpListener` 解耦,使 Sisk 免受该组件未来可能被弃用或缺乏维护的影响。 > [!WARNING] > **实验状态** > > Cadente 目前处于实验阶段(Beta)。尚不建议在关键生产环境中使用。API 和行为可能会更改。 ## 安装 Cadente 以独立的包形式提供。要在 Sisk 中使用它,需要 `Sisk.Cadente.CoreEngine` 包。 ```bash dotnet add package Sisk.Cadente.CoreEngine ``` `Sisk.Cadente` 和 `Sisk.Cadente.CoreEngine` 目标为 .NET 9 和 .NET 10。它们不能在目标为 .NET 8 的项目中使用,尽管 `Sisk.HttpServer` 支持 .NET 8。 ## 在 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(); ``` 当监听主机配置了 SSL 时,[HttpServer.ListeningPrefixes](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.ListeningPrefixes.md) 会使用 `https` 方案报告其地址。如果客户端向配置了 SSL 的端口发送普通 HTTP,Cadente 会返回 `301 Moved Permanently` 重定向到同一主机和路径的 `https` 地址。 ### 高级配置 可以通过向 `CadenteHttpServerEngine` 构造函数传递设置操作来自定义底层的 `HttpHost` 实例。这对于配置超时或其他底层设置非常有用。 ```csharp using var engine = new CadenteHttpServerEngine(host => { // 配置客户端读/写超时 host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## 独立使用 虽然主要为 Sisk 设计,Cadente 也可以作为独立的 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 stream = await context.Response.GetResponseStreamAsync(chunked: true); using var writer = new StreamWriter(stream); await writer.WriteLineAsync("Hello, world!"); } } ``` 当响应体较小且已在内存中时,`WriteInlineContent` 和 `WriteInlineContentAsync` 会在一次操作中写入头部和主体。响应必须包含 `Content-Length` 头部: ```csharp byte[] body = "Hello, world!"u8.ToArray(); context.Response.StatusCode = 200; context.Response.Headers.Set(new HttpHeader("Content-Length", body.Length.ToString())); await context.Response.WriteInlineContentAsync(body); ``` ## 请求验证 Cadente 在请求到达处理程序之前会验证请求的框架。自 1.7 版本起,它: - 支持在 keep-alive 连接上的流水线请求; - 拒绝不是有效 HTTP 令牌的请求方法以及包含可见 ASCII 范围之外字节(`%x21-7E`)的请求目标; - 拒绝 `Transfer-Encoding` 不是单一 `chunked` 值的请求,以及具有冲突 `Content-Length` 头的请求; - 当请求行和头部超过 8 KB 时,返回 `431 Request Header Fields Too Large` 并关闭连接。 无效请求会返回错误并关闭连接,且不会调用处理程序。 --- # 在 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(); } } ``` --- # 更新日志 Source: https://docs.sisk-framework.org/zh-cn/docs/changelogs.html 每一次对 Sisk 的更改都记录在更新日志中。您可以在[此处](https://github.com/sisk-http/archive/tree/master/changelogs)查看所有 Sisk 版本的更新日志。 ## 从 1.6 迁移到 1.7 Sisk 1.7.0 包含 `Sisk.HttpServer`、`Sisk.Cadente`、`Sisk.Cadente.CoreEngine` 和 `Sisk.IniConfiguration` 1.7.0。`Sisk.Documenting`、`Sisk.Documenting.Html` 和 `Sisk.Monitoring` 以 1.7.0-rc3 发行,`Sisk.ModelContextProtocol` 为 1.7.0-rc1。请阅读[完整的 1.7.0 更新日志](https://github.com/sisk-http/archive/blob/master/changelogs/core/v1.7/v1.7.0.md)了解所有更改。 ### 破坏性更改 - **`Route.UseRegex` 不能再被设置。** 此属性已废弃,仅用于报告路由是否为 `RegexRoute`。请改为创建一个 [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md)。带有 `UseRegex = true` 的路由属性和 `[RegexRoute]` 特性仍然可用。参见[正则路由](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md#regex-routes)。 ```cs // 之前 router.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // 之后 router.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` - **请求体默认限制为 100 MB。** [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) 的默认值已从 `0`(无限制)改为 100 MB。此限制同样适用于请求流和 WebSocket 消息。将其设为 `0` 可恢复之前的行为。参见[获取请求体](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/requests.md#getting-the-request-body)。 ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` - **静态路由在动态路由之前进行匹配。** 正则路由、前缀路由(包括文件服务器)以及自定义匹配路由会在每个静态路由之后进行测试,且不受注册顺序影响。参见[匹配优先级](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md#matching-precedence)。 - **重复的路由名称将在启动时抛出异常。** 当服务器启动时,如果有两个或以上具有相同 `Name` 的路由,将触发 `ArgumentException`,前提是已启用 [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md)(默认开启)。 - **文件服务器路由是前缀路由。** `Router.MapFileSystem` 和 `HttpFileServer.CreateServingRoute` 会创建一个 [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) 而非正则路由,并且不再受 `ForceTrailingSlash` 的影响。 - **`RouteMethod.Any` 包含 `QUERY`。** 使用 `MapAny` 映射的路由同样会响应 `QUERY` 请求。`RouteMethod.Any` 的数值已更改,请重新编译使用或比较该值的代码。 - **CORS 预检遵循 `UseCors`。** 自动的 `OPTIONS` 响应不再为 `UseCors = false` 的路由发送 CORS 头部,而显式映射的 `OPTIONS` 路由会自行处理该请求。参见[预检请求](https://docs.sisk-framework.org/zh-cn/docs/features/cors.md#preflight-requests)。 - **Cadente 拒绝格式错误的请求。** 无效的请求方法和目标、不受支持的 `Transfer-Encoding` 值以及冲突的 `Content-Length` 头部都会被拒绝,且大于 8 KB 的头部块会返回 `431 Request Header Fields Too Large`。参见[Cadente 请求验证](https://docs.sisk-framework.org/zh-cn/docs/cadente.md#request-validation)。 ### 已停止维护的包 `Sisk.BasicAuth`、`Sisk.SslProxy` 和 `Sisk.ServiceProvider` 已不再维护,并在 NuGet 上标记为已弃用,且没有替代包。请从项目中移除它们: - 用您自己的[请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md)替代 `Sisk.BasicAuth`,自行验证 `Authorization` 头部。 - 用 [Cadente 引擎](https://docs.sisk-framework.org/zh-cn/docs/cadente.md) 的原生 SSL 支持替代 `Sisk.SslProxy`。 - 用 `Sisk.HttpServer` 通过 `UsePortableConfiguration` 内置的[可移植配置](https://docs.sisk-framework.org/zh-cn/docs/extensions/service-providers.md)替代 `Sisk.ServiceProvider`。 ### 新特性 - 通过 `Route.Match`、`RouteMatch`、`Route.IsStatic` 和 `Route.AllowRewrites` 实现[自定义路由匹配](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md#custom-route-matching),以及新的[前缀路由](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md#prefix-routes)。 - 支持 HTTP [QUERY 方法](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md#query-method-routes),使用 `MapQuery`、`Route.Query`、`[RouteQuery]` 和 `Accept-Query` 头部。 - 通过 `HttpContext.Interlocked` 实现[原子上下文值](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/requests.md#atomic-context-values)。 - `%{$key}` [访问日志项](https://docs.sisk-framework.org/zh-cn/docs/features/logging.md#access-log-formatting),用于打印请求包的值;以及[安全文件写入帮助方法](https://docs.sisk-framework.org/zh-cn/docs/features/logging.md#writing-files-safely) `LogStream.SafeWriteToFile` 和 `LogStream.SafeAppendToFile`。 - 在 Cadente 中:管道化的 keep-alive 请求、`WriteInlineContent` 与 `WriteInlineContentAsync`,以及对在 SSL 端口上收到的普通 HTTP 请求进行 HTTPS 重定向。 - 在扩展中:参数示例和 `LlmsApiExporter.FooterText`(位于 [Sisk.Documenting](https://docs.sisk-framework.org/zh-cn/docs/extensions/api-documentation.md)),技能以及 `2026-07-28` 协议修订(位于 [Sisk.ModelContextProtocol](https://docs.sisk-framework.org/zh-cn/docs/extensions/mcp.md)),以及在 Sisk.Monitoring 仪表盘中的 HTTP 流量、延迟、路由和异常遥测。 ### 其他行为更改 - `HttpListener` 引擎会将每个请求分派到线程池,因此慢请求不再阻塞下一个接受的连接。其 `ContextAbortedToken` 现在返回 `CancellationToken.None` 而不是抛出异常。 - 在请求结束时以及写入日志时,由服务器处理程序抛出的异常会写入错误日志,而不是逃逸到 HTTP 引擎。参见[处理程序抛出的异常](https://docs.sisk-framework.org/zh-cn/docs/advanced/http-server-handlers.md#exceptions-thrown-by-handlers)。 - 当允许的来源取决于请求时,CORS 响应会发送 `Vary: Origin`。 - `%ri` 访问日志项使用请求开始时解析的客户端地址。 - `Sisk.HttpServer` 目标为 .NET 8、.NET 9 和 .NET 10。`Sisk.Cadente` 与 `Sisk.Cadente.CoreEngine` 目标为 .NET 9 和 .NET 10,并将其调试符号以 `.snupkg` 包形式发布。 --- # 常见问题 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(); // maps the GET / route into the following action 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) 映射到它的对象。 ## Matching routes 当 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) 的响应给客户端。路由的测试顺序在 [matching precedence](#matching-precedence) 中描述。 Sisk 会检查静态路由之间是否可能出现冲突。当服务器启动时,Sisk 会查找路径模式和方法重叠的静态路由,若发现则抛出异常。在同一次检查中,若有两个或更多路由共享相同的 [Route.Name](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Name.md),Sisk 也会抛出 `ArgumentException`。这两项检查受 [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md) 控制,默认启用。 ### Matching precedence 每个路由要么是 **静态** 要么是 **动态**,由 [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) 指示: - **Static routes** 严格遵循其路径模式,例如 `/users/`。使用 `Map*` 方法、`Route` 实例和路由属性创建的路由都是静态的。 - **Dynamic routes** 以自己的方式解释路径,可能匹配无法用路径模式描述的路径。[RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md)、[PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md)(用于 [file servers](https://docs.sisk-framework.org/zh-cn/docs/features/file-server.md))以及 [custom matching routes](#custom-route-matching) 属于动态路由。 当服务器启动时,路由器会重新排序其路由,使所有静态路由在所有动态路由之前进行测试。每个组内部保持注册顺序。这意味着静态路由始终优先于动态路由,无论它们的定义顺序如何: ```cs // dynamic: serves any GET path from the "wwwroot" directory mainRouter.MapFileSystem("/", "wwwroot"); // static: handles GET /api/status, even though it was defined after the file server mainRouter.MapGet("/api/status", request => new HttpResponse("ok")); ``` 在上面的示例中,文件服务器充当了所有未被静态路由处理的 GET 请求的回退。 两个组在验证和转换方式上也有所不同: | | 静态路由 | 动态路由 | |---|---|---| | 匹配顺序 | 首先,按注册顺序 | 在所有静态路由之后,按注册顺序 | | 路由冲突检查 | 是 | 否 | | [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) 是否应用 | 是 | 否 | > [!WARNING] > 动态路由不会进行冲突检查,因为 Sisk 无法知道它们匹配哪些路径。当两个动态路由可以匹配同一请求时,先定义的路由获胜。例如,先映射到 `/` 的文件服务器会使随后映射到 `/static` 的文件服务器不可达。 > [!IMPORTANT] > 在 1.7 之前的版本中,路由仅按注册顺序测试,正则路由可能会优先于之后定义的静态路由。如果你的应用依赖正则路由处理同样被静态路由匹配的路径,请将该逻辑移至静态路由或缩小静态路径模式的范围。 ### Creating routes using path patterns 对于新应用,推荐使用 `Map*` 方法。它们在调用位置即可看到 HTTP 方法,并且符合当前的 `Router` API。旧的 `SetRoute` 方法仍作为兼容包装存在,但新示例应使用 `Map`、`MapGet`、`MapPost`、`MapPut`、`MapDelete`、`MapPatch`、`MapAny`、`MapOptions` 或 `MapHead`。 ```cs // Map* methods are the usual way to define method-specific routes. 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(); // empty 200 ok }); // Map can also receive a Route instance when you need route options. mainRouter.Map(Route.Get("/image.png", (request) => { var imageStream = File.OpenRead("image.png"); return new HttpResponse() { // the StreamContent inner // stream is disposed after sending // the response. Content = new StreamContent(imageStream) }; })); // multiple parameters 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),可用作选项/单子,将其原始值转换为受管理的对象。 下面的示例读取路由参数 “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 以 `/` 结尾。 ### Creating routes using class instances 你也可以使用属性 [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 { // will match GET / [RouteGet] HttpResponse Index(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Index!"); return res; } // static methods works too [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` 的类型,但 **不包括该类型本身**。两个可选参数指示方法搜索这些类型的方式。第一个参数暗示搜索的程序集,第二个参数指示类型的定义方式。 ## Regex routes 如果不想使用默认的 HTTP 路径匹配方式,可以使用 [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) 类将路径解释为正则表达式来定义路由。正则路由属于 [dynamic routes](#matching-precedence)。 ```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}"); } } ``` > [!NOTE] > 正则表达式不会自动锚定:例如 `/uploads` 也会匹配 `/files/uploads/a`。使用 `^` 和 `$` 可匹配完整路径。 > [!IMPORTANT] > 自 Sisk 1.7 起,`Route.UseRegex` 属性不再可设置。该属性已废弃,仅用于报告路由是否为 [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md)。路由属性中的 `UseRegex`(如 `[Route(RouteMethod.Get, "...", UseRegex = true)]`)以及 `[RegexRoute]` 属性仍然有效。 要迁移使用 `Route.UseRegex` 创建的正则路由,请改为使用 `RegexRoute`: ```cs // before 1.7 mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // 1.7 and later mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` ## Prefix routes [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) 匹配其路径以及其下的所有嵌套路径,比较完整的路径段。它是 [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md) 使用的路由。前缀路由属于 [dynamic routes](#matching-precedence)。 ```cs // matches /docs, /docs/ and /docs/any/nested/path, but not /docsfoo mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request => { return new HttpResponse($"Requested {request.Path}"); }, null)); ``` ## Custom route matching 通过继承 [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) 并重写 [Route.Match](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Match.md) 可以创建自定义匹配规则。该方法接收请求路径和正在匹配该路由的路由器,并返回一个包含结果以及可选捕获路由参数的 [RouteMatch](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.md)。 路由器仅在 `Match` 成功后才会验证 HTTP 方法,因此 `Match` 应仅测试路径。这保证了 [405 Method Not Allowed](#method-not-allowed-405-callback-handler) 和 `OPTIONS` 响应在自定义路由中仍然有效。 ```cs {title="Routing/ExtensionRoute.cs"} // matches any path ending with the given file extension, such as /reports/2024.csv public sealed class ExtensionRoute : Route { public ExtensionRoute(string extension, RouteAction action) : base(RouteMethod.Get, extension, action) { } // this route does not follow a path pattern public override bool IsStatic => false; // the path is not a path pattern, so it cannot be redirected to its trailing-slash form public override bool AllowRewrites => false; public override RouteMatch Match(string requestPath, Router router) { StringComparison comparison = router.MatchRoutesIgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (!requestPath.EndsWith(Path, comparison)) return RouteMatch.NotMatched; var parameters = new NameValueCollection { ["file"] = requestPath[..^Path.Length] }; return new RouteMatch(true, parameters); } } ``` ```cs mainRouter.Map(new ExtensionRoute(".csv", request => { string file = request.RouteParameters["file"].GetString(); return new HttpResponse($"Exporting {file} as CSV"); })); ``` 在重写 `Match` 时,请注意: - 当路由不严格遵循路径模式时,覆盖 [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) 并返回 `false`。否则,路由器在检查冲突和应用 [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) 时会将其 `Path` 当作路径模式,可能导致错误的冲突或无效路径。 - 当路由路径无法通过 [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) 重定向到其尾部斜杠形式时,覆盖 [Route.AllowRewrites](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AllowRewrites.md) 并返回 `false`。 - `Match` 会在每个到达该路由的请求上被调用,可能会被多个线程并发调用。路由不匹配时应避免分配,并确保任何缓存状态是线程安全的。对不成功的匹配返回 [RouteMatch.NotMatched](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.NotMatched.md)。 - 通过 `router` 参数遵循 [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md)。 ## Prefixing routes 可以使用 [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) 属性为类或模块中的所有路由添加前缀,并将前缀设为字符串。 下面的示例展示了 BREAD 架构(Browse, Read, Edit, Add and 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) 使用。后续章节将进一步说明。 ## Routes without request parameter 路由可以在没有 [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 { // gets the request from the current thread public HttpRequest Request { get => HttpContext.Current.Request; } // the line below, when called, gets the database from the current HTTP session, // or creates a new one if it doesn't exist 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() { // reads the JSON data from the current request 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) 教程。 ## Any method routes 你可以定义仅通过路径匹配而忽略 HTTP 方法的路由。这在需要在路由回调内部进行方法验证时非常有用。 ```cs // will match / on any HTTP method mainRouter.MapAny("/", callbackFunction); ``` [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 包含 `GET`、`POST`、`PUT`、`PATCH`、`DELETE`、`QUERY`、`HEAD` 和 `OPTIONS`。 ## QUERY method routes Sisk 支持 HTTP `QUERY` 方法,这是一种安全且幂等的方式,将查询放在请求体中。使用 [Router.MapQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapQuery.md)、[Route.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Query.md)、[RouteMethod.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 或 [RouteQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteQueryAttribute.md) 属性来定义这些路由: ```cs mainRouter.MapQuery("/search", request => { string filter = request.Body; return new HttpResponse($"Searching for {filter}"); }); ``` ```cs {title="Controller/SearchController.cs"} public class SearchController { [RouteQuery("/products")] static HttpResponse SearchProducts(HttpRequest request) { return new HttpResponse($"Searching for {request.Body}"); } } ``` `Accept-Query` 头部用于声明在 `QUERY` 请求中接受的媒体类型,可通过 [HttpHeaderCollection.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.AcceptQuery.md) 和 [HttpKnownHeaderNames.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpKnownHeaderNames.AcceptQuery.md) 访问。 ## Any path routes 任意路径路由会测试 HTTP 服务器收到的任何路径,前提是路由方法也被测试。如果路由方法是 `RouteMethod.Any` 且路由在路径表达式中使用了 [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md),则该路由会监听所有来自 HTTP 服务器的请求,且不能再定义其他路由。 ```cs // the following route will match all POST requests mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction); ``` ## Ignore case route matching 默认情况下,路由与请求的匹配是区分大小写的。若要忽略大小写,请启用此选项: ```cs mainRouter.MatchRoutesIgnoreCase = true; ``` 这同样会为正则路由启用 `RegexOptions.IgnoreCase`,并为前缀路由启用不区分大小写的比较。 ## Not Found (404) callback handler 你可以为未匹配到任何已知路由的请求创建自定义回调。 ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // Since v0.14 Content = new HtmlContent("

Not found

") // older versions Content = new StringContent("

Not found

", Encoding.UTF8, "text/html") }; }; ``` ## Method not allowed (405) callback handler 你也可以为路径匹配但方法不匹配的请求创建自定义回调。 ```cs mainRouter.MethodNotAllowedErrorHandler = (context) => { return new HttpResponse(405) { Content = new StringContent($"Method not allowed for this route.") }; }; ``` ## Error Handling 在请求生命周期内(从预执行请求处理程序、通过路由操作、到后执行请求处理程序和数值处理程序)可能会抛出异常。这些异常由以下机制管理: - 如果 [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; // override log mode to log the error in both access and error logs } ``` ## Internal error handler 路由回调在服务器执行期间可能抛出错误。如果未正确处理,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` 时,表示请求必须继续,调用下一个对象,或者循环以路由器的响应结束。 > [!WARNING] > 请求处理程序实例只会创建一次,并在它处理的所有请求之间共享,包括并发请求。**绝不要在请求处理程序类本身中存储单个请求的状态**(如当前用户、已解析的令牌或计时器等字段或属性),因为这些值会泄漏到其他请求中。应改为将每个请求的值存储在 `HttpContext` 的 [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) 中: > > ```cs > public HttpResponse? Execute(HttpRequest request, HttpContext context) > { > // 错误:this.currentUser = FindUser(request); > context.RequestBag.Set(FindUser(request)); > return 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(), // 前置请求处理程序 new ValidateJsonContentRequestHandler(), // 前置请求处理程序 // -- 方法 IndexPage 将在此执行 new WriteToLogRequestHandler() // 后置请求处理程序 }); ``` 或者创建一个 [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:与全局请求处理程序中相同的实例 new AuthenticateUserRequestHandler() // 错误:不会跳过全局请求处理程序 }; 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` 的值也将不可用。无需在请求上下文中手动释放请求流,因为它会在创建它的 HTTP 会话结束时被释放。同时,你可以使用 [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) 限制,自 Sisk 1.7 起默认 100 MB(早期版本为 `0`,即无限制)。限制的适用方式如下: - 如果请求声明的 `Content-Length` 超过限制,服务器将在路由调用前返回 `413 Content Too Large` 响应。 - 对于未声明长度的分块或流式正文,在读取时会进行检查。通过 [RawBody]、[Body] 或 [GetRequestStream] 返回的流读取超出限制时会抛出 [InsufficientMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.insufficientmemoryexception),导致返回 `500 Internal Server Error`,除非你的代码或 [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) 处理该异常。 - 同一限制也是接收的 [WebSocket](https://docs.sisk-framework.org/zh-cn/docs/features/websockets.md) 消息的最大大小。 将属性设为 `0` 可移除限制: ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` 即使未配置限制,`Body` 和 `RawBody` 也无法缓冲超过 [Array.MaxLength](https://learn.microsoft.com/en-us/dotnet/api/system.array.maxlength)(约 2 GB)的内容,访问时会抛出异常。仍可通过流式处理更大的内容。 > [!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 // Disable logging for this request 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(); ... } ``` ### 原子上下文值 请求包不是线程安全的。当包中的数值可能在同一请求的并发代码(例如路由启动的并行任务)中被修改时,请使用 [HttpContext.Interlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Interlocked.md)。它返回一个 [HttpContextInterlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.HttpContextInterlocked.md) 对象,可对请求包中命名值进行原子操作: | 方法 | 描述 | | --- | --- | | `Add(name, value)` | 将 `value` 加到已存储的值上;如果 `name` 未设置,则存储 `value`。 | | `Exchange(name, value)` | 替换已存储的值并返回之前的值;如果 `name` 未设置,则返回 `value`。 | | `CompareExchange(name, value, comparand, notFound = -1)` | 仅当已存储的值等于 `comparand` 时才用 `value` 替换,并返回原始值;如果 `name` 未设置,则返回 `notFound`。 | | `Inspect(name)` | 返回已存储的值;如果 `name` 未设置,则返回 `null`。 | 值在请求包中以 `double` 存储。接受 `int` 或 `long` 参数的重载会将其转换为 `double`。 ```cs router.MapPost("/import", async (HttpRequest request) => { string[] lines = request.Body.Split('\n'); await Parallel.ForEachAsync(lines, async (line, cancellation) => { if (await TryImportAsync(line, cancellation)) request.Context.Interlocked.Add("imported", 1); }); double imported = request.Context.Interlocked.Inspect("imported") ?? 0; return new HttpResponse($"{imported} line(s) imported"); }); ``` > [!NOTE] > 这些操作仅在相互之间的 `Interlocked` 调用中保持原子性。直接通过 `RequestBag` 读取或写入同一键并未与它们同步。 ## 获取表单数据 您可以使用下面的示例在 [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 表单对象](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 引擎都兼容,每个引擎都需要相应实现。 默认的 Sisk 引擎基于 `System.Net.HttpListener`,不支持客户端断开检测。当使用默认引擎时,`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/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/Web/API/EventSource/close) 方法会重新启动流,导致服务器端无限额外处理。为避免此类问题,通常会发送一个最终包,指示事件源已完成所有包的发送。 下面的示例展示了浏览器如何与支持服务器端事件的服务器通信。 ```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 地址可能会被代理端点替代。 您可以使用 [forwarding resolvers](https://docs.sisk-framework.org/zh-cn/docs/advanced/forwarding-resolvers.md) 在 Sisk 中定义自己的解析器。 ## 头部编码 头部编码在某些实现中可能会出现问题。在 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 自动写入访问日志和错误日志。可以定义日志轮转、扩展名和轮转频率。 The [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` 对象(例如 `Console.Out`),将日志写入到 `TextWriter` 实例中: ```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` | | %{$key} | 表示存放在 [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) 中 `key` 键的值,使用 `ToString()` 转换。如果键未设置则为空。 | `42` | 您也可以使用 `HttpServerConfiguration.DefaultAccessLogFormat` 来使用默认的访问日志格式。 `%{$key}` 语法允许请求处理程序和路由向访问日志中添加自定义值: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %rm %rz [%sc] user=%{$UserId}"; router.MapGet("/profile", request => { request.Bag["UserId"] = "42"; return new HttpResponse("ok"); }); ``` ## 日志轮转 您可以配置 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) 提供了静态辅助方法,可创建缺失的父目录、在进程内序列化对同一文件的并发写入,并在操作失败时返回 `false` 而不是抛异常: - [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) 和 [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) 替换文件内容。 - [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) 和 [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) 追加到文件。 每个方法接受字符串或字符串数组,并提供接受 `Encoding` 的重载。 ```cs bool written = await LogStream.SafeAppendToFileAsync("logs/audit.log", $"{DateTime.Now:s} user signed in\n"); if (!written) { Console.WriteLine("Could not write the audit entry."); } ``` 锁定是进程级的:来自其他进程对同一文件的写入不会被协调。如果仅需确保目录存在,可使用 [PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) 或 [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md),它们在目录已存在时返回 `true`,在创建时返回 `false`。 ## 扩展 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 的头部 // 内容为 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 != 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` 处理。超时、操作被取消或客户端断开时,此方法不返回消息。 接收消息的最大大小为 [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md),自 Sisk 1.7 起默认 100 MB。当客户端发送更大的消息时,连接会以 `MessageTooBig` 状态关闭,`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 服务器在内容流完成时自动丢弃,无论是否有错误。 --- # 在 Sisk 中启用 CORS(跨域资源共享) Source: https://docs.sisk-framework.org/zh-cn/docs/features/cors.html Sisk 提供了一个工具,可在公开服务时处理[跨域资源共享 (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)。该功能并非 HTTP 协议的一部分,而是由 W3C 定义的浏览器特性。此安全机制阻止网页向提供该网页的域之外的域发起请求。服务提供者可以允许特定域或单个域访问其资源。 ## Same Origin 要将资源标识为“同源”,请求必须在其请求中包含 [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin) 头部: ```http GET /api/users HTTP/1.1 Host: example.com Origin: http://example.com ... ``` 远程服务器必须在响应中返回一个与请求的 Origin 值相同的 [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/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: ://: ``` ## Enabling 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** 属性是动态的:服务器会检查请求的来源是否在此列表中。如果匹配,则在该来源的响应中发送对应的头部。 当允许的来源取决于请求时(即使用 **AllowOrigins** 或下面描述的 [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) 值),Sisk 还会发送 `Vary: Origin` 头部,以防缓存将响应复用于不同的来源。 ### Wildcards and automatic headers 或者,你可以在响应的来源中使用通配符 (`*`) 来表示允许任何来源访问资源。但该值不适用于带有凭证(授权头部)的请求,此操作会[导致错误](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials)。 可以通过显式列出允许的来源(使用 [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) 属性),或在 [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) 的值中使用 [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) 常量来规避此问题。该魔法属性会将 `Access-Control-Allow-Origin` 头部设置为请求的 `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(); ``` ## Other Ways to Apply 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(); } ``` ## Disabling CORS on Specific Routes `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" }; } } ``` ## Replacing Values in the Response 可以在路由操作中显式替换或移除头部值: ```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" }; } } ``` ## Preflight Requests 预检请求是客户端在实际请求之前发送的[OPTIONS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS) 方法请求。 当 `OPTIONS` 方法的请求匹配某条路由的路径,但该路径没有显式配置 `OPTIONS` 路由时,Sisk 服务器会返回 `200 OK` 并附带相应的 CORS 头部,随后客户端即可继续发送实际请求。 - 如果路由使用 [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 显式映射,例如使用 `MapOptions`,则该路由处理请求,而不是自动响应。 - 如果匹配路径的路由设置了 `UseCors = false`,仍会发送自动响应,但不包含 CORS 头部。 > [!NOTE] > 在 Sisk 1.7 之前,自动预检响应始终携带 CORS 头部,即使路由设置了 `UseCors = false`。 ## Disabling CORS Globally 这在技术上不可行。若不想使用 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)。 `HttpFileServer.CreateServingRoute` 在需要显式创建 `Route` 对象时仍然可用,但 `MapFileSystem` 是应用代码中最直接的选项。 这两种方法都会创建一个 [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md)(Sisk 1.7 之前的正则路由),它是一个 [dynamic route](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/routing.md#matching-precedence)。静态路由始终在其之前进行测试,因此映射到 `/` 的文件服务器可作为其他路由未处理路径的回退。文件服务器路由不受 [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) 的影响。 ## HttpFileServerHandler 若需更细致地控制文件的提供方式,您可以手动实例化并配置 `HttpFileServerHandler`。 ```cs var fileHandler = new HttpFileServerHandler("/var/www/html"); // 启用目录列出(默认禁用) fileHandler.AllowDirectoryListing = true; // 设置自定义路由前缀(此前缀将在请求路径中被去除) fileHandler.RoutePrefix = "/public"; // 在 /public 下注册处理器 mainRouter.MapFileSystem("/public", fileHandler); ``` ### 配置 | 属性 | 描述 | |---|---| | `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)的代理模型提供上下文的应用程序: dotnet add package Sisk.ModelContextProtocol --prerelease 此包提供有用的类和方法,用于构建在 [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) 上运行的 MCP 服务器,支持单请求/响应消息。自 1.7.0-rc1 版本起,它支持协议版本 `2026-07-28` 的无状态请求,并保留对使用 `2025-11-25`、`2025-06-18` 和 `2025-03-26` 版本的客户端的传统初始化流程。它支持工具和技能。 > [!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 Schema [Sisk.ModelContextProtocol] 库使用了 [LightJson](https://github.com/CypherPotato/LightJson) 的分支来处理 JSON 和 JSON Schema。此实现为各种对象提供了流式的 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"]); ``` 生成以下 schema: ```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) => { // 读取操作名称。如果为 null 或不是显式字符串则会抛出异常 string actionName = context.Arguments["action_name"].GetString(); // action_data 被定义为非必需,因此此处可能为 null string? actionData = context.Arguments["action_data"].MaybeNull()?.GetString(); // 根据 actionName 处理浏览器操作 return await Task.FromResult( McpToolResult.CreateText($"Performed browser action: {actionName}")); })); ``` 工具参数会在处理程序运行之前根据 schema 进行验证。如果验证失败,提供程序会向 MCP 客户端返回错误结果,并且不会调用工具处理程序。 ## 函数结果 [McpToolResult](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.md) 对象提供了三种方法来创建工具响应的内容: - [CreateAudio(ReadOnlySpan, string)](...): 为 MCP 客户端创建基于音频的响应。 - [CreateImage(ReadOnlySpan, string)](...): 为 MCP 客户端创建基于图像的响应。 - [CreateText(string)](...): 为 MCP 客户端创建基于文本的响应(默认)。 此外,还可以将多种不同的内容合并为单个 JSON 工具响应: ```csharp mcp.Tools.Add(new McpTool( ... executionHandler: async (McpToolContext context) => { // 模拟真实工作 byte[] browserScreenshot = await browser.ScreenshotAsync(); return McpToolResult.Combine( McpToolResult.CreateText("Heres the screenshot of the browser:"), McpToolResult.CreateImage(browserScreenshot, "image/png") ) })); ``` 提供程序当前支持 `server/discover`、传统的 `initialize` 和 `ping` 方法、`tools/list`、`tools/call`、`skills/list`、`skills/get`、`resources/list`、`resources/read` 以及通知。不支持的 JSON-RPC 方法会返回 JSON-RPC 错误响应。 ## 发布技能 MCP 提供程序还可以通过 `io.modelcontextprotocol/skills` 扩展发布 [Agent Skills](https://github.com/modelcontextprotocol/ext-skills)。在提供程序开始处理请求之前,将 [McpSkill](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpSkill.md) 对象添加到 [McpProvider.Skills](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.Skills.md): ```csharp mcp.Skills.Add(new McpSkill( path: "acme/refunds", frontmatter: new JsonObject { ["name"] = "refunds", ["description"] = "Follow the refund workflow." }, body: "# Refunds\nRead references/policy.md before using the refund tools.", files: new Dictionary { ["references/policy.md"] = File.ReadAllBytes("skills/refunds/references/policy.md") })); ``` `McpSkill` 会根据 frontmatter 和 Markdown 正文生成 `SKILL.md` 文件。`path` 的最后一段必须等于 frontmatter 中的 `name` 字段。技能文件在创建时会被复制,提供程序永不从磁盘读取它们。客户端使用 `skills/list`、`skills/get` 和 `resources/read` 列出和读取技能,使用 `2026-07-28` 协议版本。有关限制和验证规则,请参阅 [package details](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol)。 ## 持续工作 模型上下文协议是一种用于代理模型及向其提供内容的应用程序的通信协议。它是一个新协议,因此其规范经常会因废弃、添加新功能或出现破坏性更改而不断更新。 在开始构建代理应用程序之前,了解 [Model Context Protocol](https://modelcontextprotocol.io/docs/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 ( ); ``` --- # 服务提供者 Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/service-providers.html Service Providers 是一种将你的 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/", // Configuration files also support comments ], "CrossOriginResourceSharingPolicy": { "AllowOrigin": "*", "AllowOrigins": [ "*" ], // new on 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 对象存在且未设置此属性时,值为 0(无限),这会覆盖 HttpServerConfiguration.MaximumContentLength 的 100 MB 默认值。
    Server.IncludeRequestIdHeader 可选 默认值为 false。指定 HTTP 服务器是否应发送 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 = this is an value Another value = "this value has an line break on it" ; 以下代码包含一些颜色 [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 | 服务器最大 content-length 大小(字节)。如果省略,则保持服务器默认值(自 Sisk 1.7 起为 100 MB)。使用 `0` 表示无限制。 | | `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] > This package is published as a prerelease (1.7.0-rc3). Its behavior and API may be subject to change in future updates. 安装来自 NuGet 的 [Sisk.Documenting] 包。若要将文档作为 HTML 页面提供,还需安装 [Sisk.Documenting.Html]: ```sh dotnet add package Sisk.Documenting --prerelease dotnet add package Sisk.Documenting.Html --prerelease ``` 源代码可在[here]获取。 要使用 `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):指示该参数是否为必需。 ### `ApiParameterExample` 为特定情境下的已记录参数添加示例值。该特性可以多次应用于同一方法。 * **ParameterName** (string, required in constructor):示例适用的参数名称。 * **Title** (string, required in constructor):标识示例所示情境的标题。 * **Description** (string):示例适用的时间或原因。 * **Example** (string):示例值。 * **ExampleLanguage** (string):示例值的格式语言(如适用)。 ```csharp [ApiQueryParameter("status", Type = "string", Description = "Filters orders by status.")] [ApiParameterExample("status", "Pending orders", Example = "pending")] [ApiParameterExample("status", "Shipped orders", Example = "shipped", Description = "Orders that left the warehouse.")] ``` ### `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" } ``` ### LlmsApiExporter `LlmsApiExporter` 生成符合 [llms.txt](https://llmstxt.org/) 风格的纯文本文档,旨在供语言模型读取。您可以设置示例中使用的 `Hostname`,以及在文档开头和结尾写入的 `HeaderText` 和 `FooterText`: ```csharp new LlmsApiExporter( hostname: "https://api.example.com", headerText: "Public API of the Example service.", footerText: "Contact support@example.com for an API key.") ``` ### 创建自定义导出器 通过实现 `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/extensions/basic-auth.html > [!CAUTION] > **已停用,请勿使用。** Sisk.BasicAuth 包不再维护,并已在 NuGet 上标记为已弃用,且没有替代包。其 `BasicAuthenticateRequestHandler` 不会解码标准 HTTP 客户端发送的 Base64 凭证,导致已认证的请求被拒绝。请使用您自己的[请求处理程序](https://docs.sisk-framework.org/zh-cn/docs/fundamentals/request-handlers.md)实现 Basic 认证,读取 [HttpHeaderCollection.Authorization](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Authorization.md) 即可。此页面仅作历史参考保留。 Basic Auth 包添加了一个请求处理程序,能够在您的 Sisk 应用中以极少的配置和工作量处理 Basic 认证方案。Basic HTTP 认证是一种通过用户 ID 和密码对请求进行最小化输入的认证方式,会话完全由客户端控制,且不存在认证或访问令牌。 ![基本认证](https://docs.sisk-framework.org/assets/img/basic-auth.svg) 在 [MDN 规范](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Authentication) 中了解更多关于 Basic 认证方案的信息。 ## 安装 要开始使用,请在项目中安装 Sisk.BasicAuth 包: > dotnet add package Sisk.BasicAuth 您可以在 [Nuget 仓库](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0) 中查看更多安装方式。 ## 创建您的认证处理程序 您可以为整个模块或单独路由控制认证方案。为此,让我们先编写第一个 Basic 认证处理程序。 在下面的示例中,会连接数据库,检查用户是否存在以及密码是否有效,随后将用户存入上下文包中。 ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "To enter this page, please, inform your credentials."; } 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("Sorry! No user was found by this email."); } // 验证凭证密码对该用户是否有效。 if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("Invalid credentials."); } // 将已登录的用户添加到 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 $"Hello, {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 $"Hello, {loggedUser.Name}!"; } } ``` ## 备注 Basic 认证的主要职责在客户端完成。存储、缓存控制和加密均在客户端本地处理。服务器仅接收凭证并验证是否允许访问。 请注意,此方法并非最安全的方案,因为它将大量责任放在客户端,难以追踪并维护凭证的安全性。此外,密码必须在安全连接(SSL)环境下传输,因为它们本身没有加密。请求头的短暂拦截就可能泄露用户的访问凭证。 在生产环境的应用中应选择更强大的认证方案,并避免使用过多现成组件,因为它们可能不适配项目需求,进而导致安全风险。 --- # SSL 代理(已停用) Source: https://docs.sisk-framework.org/zh-cn/docs/extensions/ssl-proxy.html > [!CAUTION] > **已停用,请勿使用。** Sisk.SslProxy 包不再维护,在 NuGet 上已被标记为已弃用,且没有替代包。它已无法针对当前的 Cadente API 编译。若要提供 HTTPS,请使用 [Cadente engine](https://docs.sisk-framework.org/zh-cn/docs/cadente.md) 的原生 SSL 支持或在 [Working with SSL](https://docs.sisk-framework.org/zh-cn/docs/ssl.md) 中的选项之一。此页面仅作历史参考保留。 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 连接,而 HttpListener 不支持 SSL。 代理在同一应用程序内部运行,监听 HTTP/1.1 消息,并以相同的协议转发给 Sisk。目前,此功能仍属高度实验性,可能不够稳定,不能在生产环境中使用。 目前,SslProxy 支持几乎所有 HTTP/1.1 特性,如 keep-alive、分块编码、WebSocket 等。要打开到 SSL 代理的连接,会创建到目标服务器的 TCP 连接,并将代理转发到已建立的连接上。 SslProxy 可以通过 `HttpServer.CreateBuilder` 使用,如下所示: ```csharp using var app = HttpServer.CreateBuilder(port: 5555) .UseRouter(r => { r.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); }) // add SSL to the project .UseSsl( sslListeningPort: 5567, new X509Certificate2(@".\ssl.pfx", password: "12345") ) .Build(); app.Start(); ``` 必须为代理提供有效的 SSL 证书。为确保浏览器接受该证书,请记得将其导入操作系统,以便正常工作。 --- # 手动(高级)设置 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 添加到此服务器配置 ``` 常用服务器配置选项: | 属性 | 默认值 | 使用场景 | 备注 | | --- | --- | --- | --- | | [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` seconds | 空闲的 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) | `100 MB` | 请求体或 WebSocket 消息需要不同的大小限制。 | 在 Sisk 1.7 之前默认是 `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 请求的示例为例。 - **Receiving the request:** 每个请求都会在请求本身和将要发送给客户端的响应之间创建一个 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/)。 - External request validation: 对请求进行 [HttpServerConfiguration.RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) 的验证。 - 如果请求是外部的且属性为 `Drop`,则在不向客户端返回响应的情况下关闭连接,并将 `HttpServerExecutionStatus = RemoteRequestDropped`。 - Forwarding Resolver configuration: 如果配置了 [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 matching: 在解析出主机且配置了多个 [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` 状态。 - Router binding: 将对应 ListeningHost 的路由器关联到接收到的 HTTP 服务器。 - 如果路由器已经关联到另一个 HTTP 服务器(这不被允许,因为路由器会主动使用服务器的配置资源),则抛出 `InvalidOperationException`。此异常仅在 HTTP 服务器初始化期间出现,而不会在创建 HTTP 上下文时出现。 - Pre-definition of headers: - 如果已配置,则在响应中预定义 `X-Request-Id` 头。 - 如果已配置,则在响应中预定义 `X-Powered-By` 头。 - Content size validation: 仅当请求内容长度大于零时,验证请求内容是否小于 [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md)(默认 100 MB)。 - 如果请求发送的 `Content-Length` 超过配置的上限,返回 413 Payload Too Large 响应给客户端,并在 HTTP 上下文中返回 `HttpServerExecutionStatus = ContentTooLarge` 状态。 - The `OnHttpRequestOpen` event is invoked for all configured HTTP server handlers. - **Routing the action:** 服务器为接收到的请求调用路由器。 - 如果路由器未找到匹配请求的路由: - 如果已配置 [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) 属性,则调用该操作,并将操作的响应转发给 HTTP 客户端。 - 如果该属性为 null,则返回默认的 404 Not Found 响应给客户端。 - 如果路由器找到了匹配的路由,但路由的 HTTP 方法与请求的方法不匹配: - 如果已配置 [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 响应,`Location` 头指向同一路径加上结尾的 `/`(包括查询字符串),发送给客户端。 - The `OnContextBagCreated` event is invoked for all configured HTTP server handlers. - 所有全局的 [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) 实例(带有 `BeforeResponse` 标记)将被执行。 - 如果任意处理器返回非 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] 实例将被执行。 - 如果任意处理器返回非 null 响应,则该响应被转发给 HTTP 客户端,随后关闭上下文。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions] 为禁用: - 如果已启用 [Router.CallbackErrorHandler] 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义前述属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 调用路由器的操作并将其转换为 HTTP 响应。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions] 为禁用: - 如果已启用 [Router.CallbackErrorHandler] 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义前述属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 所有全局的 [IRequestHandler] 实例(带有 `AfterResponse` 标记)将被执行。 - 如果任意处理器返回非 null 响应,则该处理器的响应替换之前的响应,并立即转发给 HTTP 客户端。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions] 为禁用: - 如果已启用 [Router.CallbackErrorHandler] 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义前述属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - 所有在路由中定义且带有 `AfterResponse` 标记的 [IRequestHandler] 实例将被执行。 - 如果任意处理器返回非 null 响应,则该处理器的响应替换之前的响应,并立即转发给 HTTP 客户端。 - 如果此步骤抛出错误且 [HttpServerConfiguration.ThrowExceptions] 为禁用: - 如果已启用 [Router.CallbackErrorHandler] 属性,则调用它并将产生的响应返回给客户端。 - 如果未定义前述属性,则返回空响应给服务器,服务器会根据抛出的异常类型转发响应,通常为 500 Internal Server Error。 - **Processing the response:** 当响应准备就绪后,服务器会准备将其发送给客户端。 - 根据当前 [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) 的对象。 - The `OnHttpRequestClose` event is invoked for all configured HTTP server handlers. - 如果服务器上抛出了异常,则调用 `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 服务器处理程序 Source: https://docs.sisk-framework.org/zh-cn/docs/advanced/http-server-handlers.html 在 Sisk 0.16 版本中,我们引入了 `HttpServerHandler` 类,旨在扩展 Sisk 的整体行为并为 Sisk 提供额外的事件处理程序,例如处理 Http 请求、路由器、上下文包等。 该类集中处理整个 HTTP 服务器以及单个请求生命周期中发生的事件。Http 协议本身没有会话概念,因此无法在请求之间保留信息。Sisk 目前提供了一种方式,让你实现会话、上下文、数据库连接以及其他有用的提供程序,以帮助你的工作。 请参阅 [this page](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) 了解每个事件何时触发以及其目的。你也可以查看 [lifecycle of an HTTP request](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("User added."); } } ``` 上述代码使用了 `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 })); } } ``` ## 处理程序抛出的异常 自 Sisk 1.7 起,服务器处理程序抛出的异常处理方式如下: - 在 `OnServerStarting`、`OnServerStarted` 和 `OnSetupRouter` 中抛出的异常会阻止服务器启动。它们会被包装在 `InvalidOperationException` 中,原始异常位于 `InnerException`。 - 在其他事件中抛出的异常会写入 [ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md),其余处理程序仍会被调用,除非已启用 [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md)。启用 `ThrowExceptions` 时,这些异常同样会被包装在 `InvalidOperationException` 中。 - 在 `OnHttpRequestClose`、`OnException` 中抛出的异常,或在格式化访问日志和错误日志时抛出的异常,永远不会向 HTTP 引擎泄漏,即使启用了 `ThrowExceptions`。它们会写入 `ErrorsLogsStream`。在早期版本中,这类异常可能导致进程终止或连接保持打开状态。 开发者可以使用此类实现会话、上下文和数据库连接。提供的代码展示了一个使用 `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)。