我需要发布 HTTP API 请求,其正文为
List<ArraySegment<byte>>
(通常的固定标头 + 可变中间 + 固定页脚内容)。 .NET Socket 自古以来就可以发送 List<ArraySegment<byte>>
,但我在 .NET HttpClient 的上下文中找不到类似的功能。
为了发送正文,
HttpClient
需要一个HttpRequestMessage,其中要发送的正文在Content属性(类型为HttpContent)中给出。
HttpContent
及其任何后代都没有带有 List<ArraySegment<byte>>
或类似内容的构造函数;它们只处理单个数组切片。这同样适用于 MemoryStream,否则可以通过 HttpContent
后代之一使用。
此时前景似乎黯淡:我可以将正文片段合并到单个字节缓冲区中进行发送,或者在
HttpContent
之上实现 List<ArraySegment<byte>>
。
这两种选择似乎都不是特别有吸引力。消息正文可能有数十兆字节,因此不必要的复制会给内存子系统带来不必要的负载并降低性能(服务器位于同一物理主机上,并且通过环回设备或通过虚拟网卡到达)。
HttpContent
和 MemoryStream
拥有不必要的庞大且复杂的 API,实施起来似乎具有挑战性。
有没有更简单的方法来发布
List<ArraySegment<byte>>
?如果没有,有人根据经验知道哪条路线是最不痛苦的路线 - 实施HttpContent
或实施MemoryStream
? (后者的 API 稍大,但看起来更简单、更容易理解。)
P.S.:把事情放在上下文中:我正在重新设计一个我们用来测量 API 服务器性能以及功能和压力测试的工具;它目前基于 .NET
Socket
API,但使用普通 TCP 套接字实际上将其限制为 HTTP 1.1。这就是 HttpClient
发挥作用的地方。
好消息是,实现HttpContent以在HttpRequestMessage中使用非常简单明了;只需要实现 TryComputeLength() 以及将内容序列化到 (TCP) 流的三个函数。
围绕
LoadIntoBuffer()
、CreateReadStream()
和朋友的令人费解的 - 而且大多没有记录 - 功能系统并没有发挥作用,HttpContent
文档中的半官方评论也说明了这一点。
这是一个最小的工作示例实现:
class ArraySegmentsContent : HttpContent
{
private readonly List<ArraySegment<byte>> m_segments;
public ArraySegmentsContent (List<ArraySegment<byte>> segments)
{
m_segments = segments;
}
protected override Task SerializeToStreamAsync (Stream stream, TransportContext? context)
{
return SerializeToStreamAsync(stream, context, default);
}
protected override async Task SerializeToStreamAsync (Stream stream, TransportContext? context, CancellationToken cancellationToken)
{
foreach (var segment in m_segments)
{
await stream.WriteAsync(segment, cancellationToken);
}
}
protected override void SerializeToStream (Stream stream, TransportContext? context, CancellationToken cancellationToken)
{
foreach (var segment in m_segments)
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
stream.Write(segment);
}
}
protected override bool TryComputeLength (out long length)
{
length = m_segments.Sum(segment => (long) segment.Count);
return true;
}
}
我提到的奇怪之处在于,采用取消令牌的异步函数是由
HttpContent
实现的,而对于不采用此类令牌的函数来说;取消令牌掉到地板上。通常情况下,情况会相反:基本实现函数将是需要取消标记的函数,而没有标记的函数将使用 CancellationToken.None
来调用它。
缺少的花里胡哨的是覆盖剩余的虚拟来抛出
NotImplementedException
以及在没有任何同步/序列化逻辑的情况下围绕 List<>
的可变性的棘手问题。
我使用
List<ArraySegment<byte>>
是因为它是 Socked.SendAsync() 所采用的,但更现实的是它应该是 IReadOnlyCollection<ArraySegment<byte>>
或类似的东西,结合防止更改底层字节缓冲区的方法,直到发送 HTTP 请求.
通过这个小类,我可以通过引用单个处方 300 次,加上用于获取的微小间隙片段,来表示包含 300 个电子处方(相关 API 允许的最大数量)的 30 MB 请求,以进行性能/负载/压力测试。照顾框架并为每个处方提供唯一的 ID。总共约为 200 KB,而不是 30 MB,并且在这 200 KB 中,100 KB(规定)可以由所有并发发送者共享。这意味着即使模拟 100 个并发客户端,所有数据也能整齐地放入所涉及 CPU 的 L2 缓存中。