请求
本页由英文自动翻译。 阅读原文
请求是表示 HTTP 请求消息的结构体。HttpRequest 对象包含在整个应用程序中处理 HTTP 消息的实用函数。
HTTP 请求由方法、路径、版本、头部和正文组成。
在本文档中,我们将教您如何获取这些元素。
获取请求方法 #
要获取收到的请求的方法,可以使用 Method 属性:
static HttpResponse Index(HttpRequest request)
{
HttpMethod requestMethod = request.Method;
...
}此属性返回由 HttpMethod 对象表示的请求方法。
注意
与路由方法不同,此属性不提供 RouteMethod.Any 项。相反,它返回真实的请求方法。
获取请求 URL 组件 #
您可以通过请求的某些属性从 URL 中获取各种组件。以下示例使用的 URL 为:
http://localhost:5000/user/[email protected]| 组件名称 | 描述 | 组件值 |
|---|---|---|
| Path | 获取请求路径。 | /user/login |
| FullPath | 获取请求路径和查询字符串。 | /user/[email protected] |
| FullUrl | 获取完整的 URL 请求字符串。 | http://localhost:5000/user/[email protected] |
| Host | 获取请求主机。 | localhost |
| Authority | 获取请求主机和端口。 | localhost:5000 |
| QueryString | 获取请求查询。 | [email protected] |
| Query | 以命名值集合的形式获取请求查询。 | {StringValueCollection object} |
| IsSecure | 确定请求是否使用 SSL(true)或未使用(false)。 | false |
您也可以使用 HttpRequest.Uri 属性,它将上述所有信息包含在一个对象中。
请求元数据和取消 #
Sisk 还会为每个请求附加运行时元数据。这些属性对日志、追踪、本地化、诊断以及长时间运行的操作非常有用:
| 属性或方法 | 用途 |
|---|---|
| RequestId | 请求的唯一标识符。启用 IncludeRequestIdHeader 可将其作为 X-Request-Id 返回。 |
| RequestedAt | Sisk 创建请求对象的时间点。 |
| RemoteAddress | 从连接解析得到的客户端地址,或来自您的 ForwardingResolver。 |
| Culture | 从 Accept-Language 解析得到的最佳语言区域,若未匹配则回退到当前语言区域。 |
| DisconnectToken | 当客户端断开连接时(若配置的 HTTP 引擎支持)发出信号的取消令牌。 |
| Bag | 在请求处理程序和路由操作之间共享的类型化键/值存储。 |
| GetRawHttpRequest | 用于诊断的请求文本表示。 |
获取请求正文 #
某些请求包含正文,例如表单、文件或 API 事务。您可以通过以下属性获取请求的正文:
// 将请求正文获取为字符串,使用请求的编码作为解码器
string body = request.Body;
// 或以字节数组获取
byte[] bodyBytes = request.RawBody;
// 或者,你可以以流的方式获取。
Stream requestStream = request.GetRequestStream();
// 或异步读取正文
Memory<byte> bodyMemory = await request.GetBodyContentsAsync();也可以使用属性 HasContents 判断请求是否包含正文,以及使用 IsContentAvailable 判断 HTTP 服务器是否已完整接收来自远端的内容。
不能多次通过 GetRequestStream 读取请求内容。若使用此方法读取,RawBody 和 Body 的值也将不可用。无需在请求上下文中手动释放请求流,因为它会在创建它的 HTTP 会话结束时被释放。同时,你可以使用 HttpRequest.RequestEncoding 属性获取最佳编码,以手动解码请求。
服务器对读取请求内容有大小限制,适用于 HttpRequest.Body 和 HttpRequest.RawBody。这些属性会将整个输入流复制到与 HttpRequest.ContentLength 相同大小的本地缓冲区。
请求内容受 HttpServerConfiguration.MaximumContentLength 限制,自 Sisk 1.7 起默认 100 MB(早期版本为 0,即无限制)。限制的适用方式如下:
- 如果请求声明的
Content-Length超过限制,服务器将在路由调用前返回413 Content Too Large响应。 - 对于未声明长度的分块或流式正文,在读取时会进行检查。通过 [RawBody]、[Body] 或 [GetRequestStream] 返回的流读取超出限制时会抛出 InsufficientMemoryException,导致返回
500 Internal Server Error,除非你的代码或 Router.CallbackErrorHandler 处理该异常。 - 同一限制也是接收的 WebSocket 消息的最大大小。
将属性设为 0 可移除限制:
using var app = HttpServer.CreateBuilder()
.UseConfiguration(config => config.MaximumContentLength = 0)
.Build();即使未配置限制,Body 和 RawBody 也无法缓冲超过 Array.MaxLength(约 2 GB)的内容,访问时会抛出异常。仍可通过流式处理更大的内容。
注意
虽然 Sisk 允许这样做,但始终建议遵循 HTTP 语义来构建应用程序,避免在不允许的请求方法中获取或提供内容。阅读 RFC 9110 “HTTP Semantics”。
读取 JSON 请求 #
对于 JSON API,建议使用内置的 JSON 辅助方法,而不是读取 Body 并手动反序列化。它们使用 System.Text.Json ,默认使用 HttpRequest.DefaultJsonSerializerOptions。
public record CreateUserRequest(string Name, string Email);
router.MapPost("/users", (HttpRequest request) =>
{
CreateUserRequest? body = request.GetJsonContent<CreateUserRequest>();
if (body is null)
return new HttpResponse(System.Net.HttpStatusCode.BadRequest);
return new HttpResponse(System.Net.HttpStatusCode.Created);
});在已经是异步路由或希望请求取消能够停止反序列化时,请使用异步重载:
router.MapPost("/users", async (HttpRequest request) =>
{
CreateUserRequest? body =
await request.GetJsonContentAsync<CreateUserRequest>(request.DisconnectToken);
if (body is null)
return new HttpResponse(System.Net.HttpStatusCode.BadRequest);
return new HttpResponse(System.Net.HttpStatusCode.Created);
});可以为特定端点传入自定义的 JsonSerializerOptions:
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true
};
UserDto? user = request.GetJsonContent<UserDto>(options);对于 Native AOT 或对裁剪敏感的应用程序,使用由 JsonSerializerContext 生成的 JsonTypeInfo<T> 重载:
[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。该方法返回当前线程中正在处理的请求的上下文。
HttpContext context = HttpContext.GetCurrentContext();日志模式 #
HttpContext.LogMode 属性允许你控制当前请求的日志行为。可以为特定请求启用或禁用日志,覆盖默认的服务器配置。
// Disable logging for this request
context.LogMode = LogOutputMode.None;请求包 #
RequestBag 对象包含在请求处理程序之间传递的存储信息,可在最终目标处使用。该对象也可被路由回调之后运行的请求处理程序使用。
提示
此属性也可以通过 HttpRequest.Bag 属性访问。
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,随后可在最终回调中使用:
public class MyController
{
[RouteGet("/")]
[RequestHandler<AuthenticateUserRequestHandler>]
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 方法,以获得更细粒度的控制。
public class Authenticate : RequestHandler
{
public override HttpResponse? Execute(HttpRequest request, HttpContext context)
{
request.Bag.Set<User>(authUser);
}
}[RouteGet("/")]
[RequestHandler<Authenticate>]
public static HttpResponse GetUser(HttpRequest request)
{
var user = request.Bag.Get<User>();
...
}原子上下文值 #
请求包不是线程安全的。当包中的数值可能在同一请求的并发代码(例如路由启动的并行任务)中被修改时,请使用 HttpContext.Interlocked。它返回一个 HttpContextInterlocked 对象,可对请求包中命名值进行原子操作:
| 方法 | 描述 |
|---|---|
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。
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");
});注意
这些操作仅在相互之间的 Interlocked 调用中保持原子性。直接通过 RequestBag 读取或写入同一键并未与它们同步。
获取表单数据 #
您可以使用下面的示例在 StringKeyStoreCollection 中获取表单数据值:
[RoutePost("/auth")]
public HttpResponse Index(HttpRequest request)
{
var form = request.GetFormContent();
string? username = form["username"];
string? password = form["password"];
if (AttempLogin(username, password))
{
...
}
}当请求正文可能较大或需要取消支持时,异步版本非常有用:
var form = await request.GetFormContentAsync(request.DisconnectToken);获取 multipart 表单数据 #
Sisk 的 HTTP 请求允许获取上传的 multipart 内容,例如文件、表单字段或任何二进制内容。
[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:
var multipartFormDataObjects =
await request.GetMultipartFormContentAsync(request.DisconnectToken);您可以进一步了解 Sisk 的 Multipart 表单对象 及其方法、属性和功能。
检测客户端断开连接 #
自 Sisk v1.15 起,框架通过 HttpRequest.DisconnectToken 提供取消令牌。当配置的 HTTP 引擎支持断开检测时,如果客户端在响应完成前关闭连接,该令牌会被取消。这对于在客户端不再等待结果时停止长时间运行的操作非常有用。
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 引擎 支持 DisconnectToken。如果您的路由依赖于断开感知的取消,请使用 Cadente 或其他明确实现此行为的引擎。即使使用了支持的引擎,取消也是协作式的:将令牌传递给异步 API 并在自己的长时间工作中检查它。
Server-sent events 支持 #
Sisk 支持 Server-sent events,它允许以流的方式发送块并保持服务器与客户端之间的连接存活。
调用 HttpRequest.GetEventSource 方法会将 HttpRequest 置于监听状态。此时,该 HTTP 请求的上下文不再期待 HttpResponse,因为它会与服务器端事件发送的包重叠。
在发送完所有包后,回调必须返回 Close 方法,它会向服务器发送最终响应并指示流已结束。
无法预测将要发送的所有包的总长度,因此无法使用 Content-Length 头部来确定连接的结束。
大多数浏览器默认情况下,服务器端事件不支持发送除 GET 方法之外的 HTTP 头或方法。因此,在使用需要特定请求头的 event-source 请求处理程序时需谨慎,因为它们可能不会包含这些头。
此外,大多数浏览器在客户端收到所有包后若未调用 EventSource.close 方法会重新启动流,导致服务器端无限额外处理。为避免此类问题,通常会发送一个最终包,指示事件源已完成所有包的发送。
下面的示例展示了浏览器如何与支持服务器端事件的服务器通信。
<html>
<body>
<b>Fruits:</b>
<ul></ul>
</body>
<script>
const evtSource = new EventSource('http://localhost:5555/event-source');
const eventList = document.querySelector('ul');
evtSource.onmessage = (e) => {
const newElement = document.createElement("li");
newElement.textContent = `message: ${e.data}`;
eventList.appendChild(newElement);
if (e.data == "Tomato") {
evtSource.close();
}
}
</script>
</html>并逐步向客户端发送消息:
public class MyController
{
[RouteGet("/event-source")]
public async Task<HttpResponse> 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 在 Sisk 中定义自己的解析器。
头部编码 #
头部编码在某些实现中可能会出现问题。在 Windows 上不支持 UTF-8 头部,因此使用 ASCII。Sisk 内置了编码转换器,可用于解码错误编码的头部。
此操作成本较高,默认已禁用,但可以通过 HttpServerConfiguration.NormalizeHeadersEncodings 启用。