HttpWebRequest 到 HttpClient 迁移指南

本文旨在指导开发人员完成从 HttpWebRequestServicePointServicePointManager迁移到 HttpClient 的过程。 迁移是必要的,因为旧的 API 已经过时,而且 HttpClient 提供了许多好处,包括改进的性能、更好的资源管理以及更现代和灵活的 API 设计。 按照本文档中所述的步骤,开发人员将能够顺利转换其代码库,并充分利用 HttpClient 提供的功能。

警告

HttpWebRequestServicePointServicePointManager 迁移到 HttpClient 不仅仅是一种“锦上添花”的性能改进。 至关重要的是要明白,一旦迁移到 .NET (Core),现有的 WebRequest 逻辑的性能可能会显著下降。 这是因为 WebRequest 维护为最低兼容性层,这意味着它缺少许多优化,例如在许多情况下的连接重用。 因此,过渡到 HttpClient 对于确保应用程序的性能和资源管理达到现代标准至关重要。

HttpWebRequest 迁移到 HttpClient

我们从几个示例开始:

使用 HttpWebRequest 的简单 GET 请求

下面是一个代码的示例:

HttpWebRequest request = WebRequest.CreateHttp(uri);
using WebResponse response = await request.GetResponseAsync();

使用 HttpClient 的简单 GET 请求

下面是一个代码的示例:

HttpClient client = new();
using HttpResponseMessage message = await client.GetAsync(uri);

使用 HttpWebRequest 的简单 POST 请求

下面是一个代码的示例:

HttpWebRequest request = WebRequest.CreateHttp(uri);
request.Method = "POST";
request.ContentType = "text/plain";
await using Stream stream = await request.GetRequestStreamAsync();
await stream.WriteAsync("Hello World!"u8.ToArray());
using WebResponse response = await request.GetResponseAsync();

使用 HttpClient 的简单 POST 请求

下面是一个代码的示例:

HttpClient client = new();
using HttpResponseMessage responseMessage = await client.PostAsync(uri, new StringContent("Hello World!"));

HttpWebRequest 到 HttpClient、SocketsHttpHandler 迁移指南

HttpWebRequest旧 API 新 API 备注
Accept Accept 示例:设置请求标头
Address RequestUri 示例:提取重定向 URI
AllowAutoRedirect AllowAutoRedirect 示例:设置 SocketsHttpHandler 属性
AllowReadStreamBuffering 无直接等效 API 缓冲属性的使用
AllowWriteStreamBuffering 无直接等效 API 缓冲属性的使用
AuthenticationLevel 无直接等效 API 示例:启用相互身份验证
AutomaticDecompression AutomaticDecompression 示例:设置 SocketsHttpHandler 属性
CachePolicy 无直接等效 API 示例:应用 CachePolicy 标头
ClientCertificates SslOptions"ClientCertificates HttpClient 中证书相关属性的用法
Connection Connection 示例:设置请求标头
ConnectionGroupName 无等效 API 无解决方法
ContentLength ContentLength 示例:设置内容标头
ContentType ContentType 示例:设置内容标头
ContinueDelegate 无等效 API 无解决方法。
ContinueTimeout Expect100ContinueTimeout 示例:设置 SocketsHttpHandler 属性
CookieContainer CookieContainer 示例:设置 SocketsHttpHandler 属性
Credentials Credentials 示例:设置 SocketsHttpHandler 属性
Date Date 示例:设置请求标头
DefaultCachePolicy 无直接等效 API 示例:应用 CachePolicy 标头
DefaultMaximumErrorResponseLength 无直接等效 API 示例:在 HttpClient 中设置 MaximumErrorResponseLength
DefaultMaximumResponseHeadersLength 无等效 API 可以改用 MaxResponseHeadersLength
DefaultWebProxy 无等效 API 可以改用 Proxy
Expect Expect 示例:设置请求标头
HaveResponse 无等效 API 通过具有 HttpResponseMessage 实例来暗示。
Headers Headers 示例:设置请求标头
Host Host 示例:设置请求标头
IfModifiedSince IfModifiedSince 示例:设置请求标头
ImpersonationLevel 无直接等效 API 示例:更改 ImpersonationLevel
KeepAlive 无直接等效 API 示例:设置请求标头
MaximumAutomaticRedirections MaxAutomaticRedirections 示例:设置 SocketsHttpHandler 属性
MaximumResponseHeadersLength MaxResponseHeadersLength 示例:设置 SocketsHttpHandler 属性
MediaType 无直接等效 API 示例:设置内容标头
Method Method 示例:HttpRequestMessage 属性的使用
Pipelined 无等效 API HttpClient 不支持管道传送。
PreAuthenticate PreAuthenticate
ProtocolVersion HttpRequestMessage.Version 示例:HttpRequestMessage 属性的使用
Proxy Proxy 示例:设置 SocketsHttpHandler 属性
ReadWriteTimeout 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用
Referer Referrer 示例:设置请求标头
RequestUri RequestUri 示例:HttpRequestMessage 属性的使用
SendChunked TransferEncodingChunked 示例:设置请求标头
ServerCertificateValidationCallback SslOptions"RemoteCertificateValidationCallback 示例:设置 SocketsHttpHandler 属性
ServicePoint 无等效 API ServicePoint 不是 HttpClient 的一部分。
SupportsCookieContainer 无等效 API 对于 HttpClient,这总是 true
Timeout Timeout
TransferEncoding TransferEncoding 示例:设置请求标头
UnsafeAuthenticatedConnectionSharing 无等效 API 无解决方法
UseDefaultCredentials 无直接等效 API 示例:设置 SocketsHttpHandler 属性
UserAgent UserAgent 示例:设置请求标头

迁移 ServicePoint(Manager) 使用情况

你应该知道,ServicePointManager 是一个静态类,这意味着对其属性所做的任何更改都会对应用程序中所有新创建的 ServicePoint 对象产生全局影响。 例如,修改 ConnectionLimitExpect100Continue 这样的属性时,影响每个新的 ServicePoint 实例的属性。

警告

在新式 .NET 中,HttpClient 不考虑在 ServicePointManager 上设置的任何配置。

ServicePointManager 属性映射

ServicePointManager旧 API 新 API 备注
CheckCertificateRevocationList SslOptions"CertificateRevocationCheckMode 示例:使用 SocketsHttpHandler 启用 CRL 检查
DefaultConnectionLimit MaxConnectionsPerServer 示例:设置 SocketsHttpHandler 属性
DnsRefreshTimeout 无等效 API 示例:启用 Dns 轮循机制
EnableDnsRoundRobin 无等效 API 示例:启用 Dns 轮循机制
EncryptionPolicy SslOptions"EncryptionPolicy 示例:设置 SocketsHttpHandler 属性
Expect100Continue ExpectContinue 示例:设置请求标头
MaxServicePointIdleTime PooledConnectionIdleTimeout 示例:设置 SocketsHttpHandler 属性
MaxServicePoints 无等效 API ServicePoint 不是 HttpClient 的一部分。
ReusePort 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用
SecurityProtocol SslOptions"EnabledSslProtocols 示例:设置 SocketsHttpHandler 属性
ServerCertificateValidationCallback SslOptions"RemoteCertificateValidationCallback 两者都是 RemoteCertificateValidationCallback
UseNagleAlgorithm 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用

警告

在新式 .NET 中,UseNagleAlgorithmExpect100Continue 属性的默认值设置为 false。 这些值在 .NET Framework 中默认为 true

ServicePointManager 方法映射

ServicePointManager旧 API 新 API 备注
FindServicePoint 无等效 API 无解决方法
SetTcpKeepAlive 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用

ServicePoint 属性映射

ServicePoint旧 API 新 API 备注
Address HttpRequestMessage.RequestUri 这是请求 URI,可在 HttpRequestMessage 下找到此信息。
BindIPEndPointDelegate 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用
Certificate 无直接等效 API 可以从 RemoteCertificateValidationCallback 中提取此信息。 示例:提取证书
ClientCertificate 无等效 API 示例:启用相互身份验证
ConnectionLeaseTimeout SocketsHttpHandler.PooledConnectionLifetime HttpClient 中的等效设置
ConnectionLimit MaxConnectionsPerServer 示例:设置 SocketsHttpHandler 属性
ConnectionName 无等效 API 无解决方法
CurrentConnections 无等效 API 请参阅 .NET 中的网络遥测
Expect100Continue ExpectContinue 示例:设置请求标头
IdleSince 无等效 API 无解决方法
MaxIdleTime PooledConnectionIdleTimeout 示例:设置 SocketsHttpHandler 属性
ProtocolVersion HttpRequestMessage.Version 示例:HttpRequestMessage 属性的使用
ReceiveBufferSize 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用
SupportsPipelining 无等效 API HttpClient 不支持管道传送。
UseNagleAlgorithm 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用

ServicePoint 方法映射

ServicePoint旧 API 新 API 备注
CloseConnectionGroup 无等效项 无解决方法
SetTcpKeepAlive 无直接等效 API SocketsHttpHandler 和 ConnectCallback 的使用

HttpClient 和 HttpRequestMessage 属性的用法

在 .NET 中使用 HttpClient 时,可以访问各种属性,以便配置和自定义 HTTP 请求和响应。 了解这些属性有助于充分利用 HttpClient,并确保应用程序与 Web 服务高效安全地通信。

示例:HttpRequestMessage 属性的使用

下面是如何将 HttpClient 和 HttpRequestMessage 一起使用的示例:

var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://example.com"); // Method and RequestUri usage
var request = new HttpRequestMessage() // Alternative way to set RequestUri and Method
{
    RequestUri = new Uri("https://example.com"),
    Method = HttpMethod.Post
};
request.Headers.Add("Custom-Header", "value");
request.Content = new StringContent("somestring");

using var response = await client.SendAsync(request);
var protocolVersion = response.RequestMessage.Version; // Fetch `ProtocolVersion`.

示例:提取重定向 URI

下面是如何提取重定向 URI 的示例(与 HttpWebRequest.Address 相同):

var client = new HttpClient();
using var response = await client.GetAsync(uri);
var redirectedUri = response.RequestMessage.RequestUri;

SocketsHttpHandler 和 ConnectCallback 的使用

SocketsHttpHandler 中的 ConnectCallback 属性允许开发人员自定义建立 TCP 连接的过程。 这对于需要控制 DNS 解析或在连接上应用特定套接字选项的方案非常有用。 通过使用 ConnectCallback,可以在连接过程被 HttpClient 使用之前对其进行拦截和修改。

示例:将 IP 地址绑定到套接字

在使用 HttpWebRequest 的旧方法中,你可能使用自定义逻辑将特定的 IP 地址绑定到套接字。 下面介绍使用 HttpClientConnectCallback 实现类似功能的方法:

旧代码使用 HttpWebRequest

HttpWebRequest request = WebRequest.CreateHttp(uri);
request.ServicePoint.BindIPEndPointDelegate = (servicePoint, remoteEndPoint, retryCount) =>
{
    // Bind to a specific IP address
    IPAddress localAddress = IPAddress.Parse("192.168.1.100");
    return new IPEndPoint(localAddress, 0);
};
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();

新代码使用 HttpClientConnectCallback

var handler = new SocketsHttpHandler
{
    ConnectCallback = async (context, cancellationToken) =>
    {
        // Bind to a specific IP address
        IPAddress localAddress = IPAddress.Parse("192.168.1.100");
        var socket = new Socket(localAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        try
        {
            socket.Bind(new IPEndPoint(localAddress, 0));
            await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
            return new NetworkStream(socket, ownsSocket: true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
};
var client = new HttpClient(handler);
using var response = await client.GetAsync(uri);

示例:应用特定套接字选项

如果需要应用特定的套接字选项,例如启用“保持 TCP 连接”,则可以在 HttpClient 使用套接字之前使用 ConnectCallback 配置套接字。 事实上,ConnectCallback 在配置套接字选项方面更灵活。

旧代码使用 HttpWebRequest

ServicePointManager.ReusePort = true;
HttpWebRequest request = WebRequest.CreateHttp(uri);
request.ServicePoint.SetTcpKeepAlive(true, 60000, 1000);
request.ServicePoint.ReceiveBufferSize = 8192;
request.ServicePoint.UseNagleAlgorithm = false;
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();

新代码使用 HttpClientConnectCallback

var handler = new SocketsHttpHandler
{
    ConnectCallback = async (context, cancellationToken) =>
    {
        var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        try
        {
            // Setting TCP Keep Alive
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
            socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 60);
            socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 1);

            // Setting ReceiveBufferSize
            socket.ReceiveBufferSize = 8192;

            // Enabling ReusePort
            socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseUnicastPort, true);

            // Disabling Nagle Algorithm
            socket.NoDelay = true;

            await socket.ConnectAsync(context.DnsEndPoint, cancellationToken);
            return new NetworkStream(socket, ownsSocket: true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    }
};
var client = new HttpClient(handler);
using var response = await client.GetAsync(uri);

示例:启用 DNS 轮循机制

DNS 轮循机制是一种通过在与单个域名相关的 IP 地址列表中轮换来在多个服务器上分配网络流量的技术。 这有助于实现负载均衡并提高服务的可用性。 使用 HttpClient 时,可以通过手动处理 DNS 解析并使用 SocketsHttpHandler 的 ConnectCallback 属性在 IP 地址之间轮换来实现 DNS 轮循机制。

若要使用 HttpClient 启用 DNS 轮循机制,可以使用 ConnectCallback 属性手动解析 DNS 条目并通过 IP 地址轮循。 下面是 HttpWebRequestHttpClient 的示例:

旧代码使用 HttpWebRequest

ServicePointManager.DnsRefreshTimeout = 60000;
ServicePointManager.EnableDnsRoundRobin = true;
HttpWebRequest request = WebRequest.CreateHttp(uri);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();

在较旧的 HttpWebRequest API 中,由于内置了对该功能的支持,因此启用 DNS 轮循机制非常简单。 但是,较新的 HttpClient API 不提供相同的内置功能。 尽管如此,可以通过实现一个手动轮换 DNS 解析返回的 IP 地址的 DnsRoundRobinConnector 来实现类似的行为。

使用 HttpClient 的新代码

// This is available as NuGet Package: https://www.nuget.org/packages/DnsRoundRobin/
// The original source code can be found also here: https://github.com/MihaZupan/DnsRoundRobin
public sealed class DnsRoundRobinConnector : IDisposable

可以在此处找到 DnsRoundRobinConnector 的实现。

DnsRoundRobinConnector 使用情况:

private static readonly DnsRoundRobinConnector s_roundRobinConnector = new(
        dnsRefreshInterval: TimeSpan.FromSeconds(10),
        endpointConnectTimeout: TimeSpan.FromSeconds(5));
static async Task DnsRoundRobinConnectAsync()
{
    var handler = new SocketsHttpHandler
    {
        ConnectCallback = async (context, cancellation) =>
        {
            Socket socket = await DnsRoundRobinConnector.Shared.ConnectAsync(context.DnsEndPoint, cancellation);
            // Or you can create and use your custom DnsRoundRobinConnector instance
            // Socket socket = await s_roundRobinConnector.ConnectAsync(context.DnsEndPoint, cancellation);
            return new NetworkStream(socket, ownsSocket: true);
        }
    };
    var client = new HttpClient(handler);
    HttpResponseMessage response = await client.GetAsync(Uri);
}

示例:设置 SocketsHttpHandler 属性

SocketsHttpHandler 是 .NET 中的一个强大且灵活的处理程序,提供用于管理 HTTP 连接的高级配置选项。 通过设置 SocketsHttpHandler 的各种属性,可以微调 HTTP 客户端的行为以满足特定要求,例如性能优化、安全增强和自定义连接处理。

下面是有关如何使用各种属性配置 SocketsHttpHandler 并将其用于 HttpClient 的示例:

var cookieContainer = new CookieContainer();
cookieContainer.Add(new Cookie("cookieName", "cookieValue"));

var handler = new SocketsHttpHandler
{
    AllowAutoRedirect = true,
    AutomaticDecompression = DecompressionMethods.All,
    Expect100ContinueTimeout = TimeSpan.FromSeconds(1),
    CookieContainer = cookieContainer,
    Credentials = new NetworkCredential("user", "pass"),
    MaxAutomaticRedirections = 10,
    MaxResponseHeadersLength = 1,
    Proxy = new WebProxy("http://proxyserver:8080"), // Don't forget to set UseProxy
    UseProxy = true,
};

var client = new HttpClient(handler);
using var response = await client.GetAsync(uri);

示例:更改 ImpersonationLevel

此功能特定于某些平台,有些过时。 如果需要解决方法,可以参考代码的此部分

使用 HttpClient 时,可能需要出于各种目的处理客户端证书,例如自定义验证服务器证书或获取服务器证书。 HttpClient 提供了几个属性和选项来有效地管理证书。

示例:使用 SocketsHttpHandler 检查证书吊销列表

SocketsHttpHandler.SslOptions 中的 CheckCertificateRevocationList 属性允许开发人员在 SSL/TLS 握手期间启用或禁用对证书吊销列表 (CRL) 的检查。 启用此属性可确保客户端验证服务器证书是否已吊销,从而提高连接的安全性。

旧代码使用 HttpWebRequest

ServicePointManager.CheckCertificateRevocationList = true;
HttpWebRequest request = WebRequest.CreateHttp(uri);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();

使用 HttpClient 的新代码

bool checkCertificateRevocationList = true;
var handler = new SocketsHttpHandler
{
    SslOptions =
    {
        CertificateRevocationCheckMode = checkCertificateRevocationList ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
    }
};
var client = new HttpClient(handler);
using var response = await client.GetAsync(uri);

示例:提取证书

若要从 HttpClient 中的 RemoteCertificateValidationCallback 提取证书,可以使用 HttpClientHandlerSocketsHttpHandler.SslOptionsServerCertificateCustomValidationCallback 属性。 此回调允许在 SSL/TLS 握手期间检查服务器的证书。

旧代码使用 HttpWebRequest

HttpWebRequest request = WebRequest.CreateHttp(uri);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();
X509Certificate? serverCertificate = request.ServicePoint.Certificate;

使用 HttpClient 的新代码

X509Certificate? serverCertificate = null;
var handler = new SocketsHttpHandler
{
    SslOptions = new SslClientAuthenticationOptions
    {
        RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
        {
            serverCertificate = certificate;

            // Leave the validation as-is.
            return sslPolicyErrors == SslPolicyErrors.None;
        }
    }
};
var client = new HttpClient(handler);
using var response = await client.GetAsync("https://example.com");

示例:启用相互身份验证

相互身份验证(也称为双向 SSL 或客户端证书身份验证)是一种客户端和服务器相互进行身份验证的安全过程。 这确保了双方都是他们声称的人,为敏感通信提供了额外的安全层。 在 HttpClient 中,可以通过配置 HttpClientHandlerSocketsHttpHandler 来启用相互身份验证,以包含客户端证书并验证服务器的证书。

要启用相互身份验证,请执行以下步骤:

  • 加载客户端证书。
  • 配置 HttpClientHandler 或 SocketsHttpHandler 以包含客户端证书。
  • 如果需要自定义验证,请设置服务器证书验证回调。

下面是使用 SocketsHttpHandler 的示例:

var handler = new SocketsHttpHandler
{
    SslOptions = new SslClientAuthenticationOptions
    {
        ClientCertificates = new X509CertificateCollection
        {
            // Load the client certificate from a file
            new X509Certificate2("path_to_certificate.pfx", "certificate_password")
        },
        RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
        {
            // Custom validation logic for the server certificate
            return sslPolicyErrors == SslPolicyErrors.None;
        }
    }
};

var client = new HttpClient(handler);
using var response = await client.GetAsync(uri);

标头属性的用法

标头在 HTTP 通信中起着关键作用,提供有关请求和响应的基本元数据。 在 .NET 中使用 HttpClient 时,可以设置和管理各种标头属性,来控制 HTTP 请求和响应的行为。 了解如何有效地使用这些标头属性有助于确保应用程序与 Web 服务高效安全地通信。

设置请求标头

请求标头用于向服务器提供有关所发出请求的其他信息。 常见用例包括指定内容类型、设置身份验证令牌和添加自定义标头。 可以使用 HttpClientDefaultRequestHeaders 属性或 HttpRequestMessage 的 Headers 属性设置请求标头。

示例:设置自定义请求标头

在 HttpClient 中设置默认自定义请求标头

var client = new HttpClient();
client.DefaultRequestHeaders.Add("Custom-Header", "value");

在 HttpRequestMessage 中设置自定义请求标头

var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Add("Custom-Header", "value");

示例:设置通用请求标头

在 .NET 中使用 HttpRequestMessage 时,设置通用请求标头对于向服务器提供有关所发出请求的其他信息至关重要。 这些标头可以包括身份验证令牌等。 正确配置这些标头可确保服务器正确处理 HTTP 请求。 有关 HttpRequestHeaders 中可用的通用属性的完整列表,请参阅属性

若要在 HttpRequestMessage 中设置通用请求标头,可以使用 HttpRequestMessage 对象的 Headers 属性。 此属性提供对 HttpRequestHeaders 集合的访问权限,可在其中根据需要添加或修改标头。

在 HttpClient 中设置通用默认请求标头

using System.Net.Http.Headers;

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "token");

在 HttpRequestMessage 中设置通用请求标头

using System.Net.Http.Headers;

var request = new HttpRequestMessage(HttpMethod.Get, uri);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "token");

示例:设置内容标头

内容标头用于提供有关 HTTP 请求或响应正文的其他信息。 在 .NET 中使用 HttpClient 时,可以设置内容标头,以指定与发送或接收的内容相关的媒体类型、编码和其他元数据。 正确配置内容标头可确保服务器和客户端能够正确解释和处理内容。

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, uri);

// Create the content and set the content headers
var jsonData = "{\"key\":\"value\"}";
var content = new StringContent(jsonData, Encoding.UTF8, "application/json");

// The following headers are set automatically by `StringContent`. If you wish to override their values, you can do it like so:
// content.Headers.ContentType = new MediaTypeHeaderValue("application/json; charset=utf-8");
// content.Headers.ContentLength = Encoding.UTF8.GetByteCount(jsonData);

// Assign the content to the request
request.Content = content;

using var response = await client.SendAsync(request);

示例:在 HttpClient 中设置 MaximumErrorResponseLength

利用 MaximumErrorResponseLength 用法,开发人员可以指定处理程序将缓冲的错误响应内容的最大长度。 这对于控制从服务器收到错误响应时读取和存储到内存中的数据量非常有用。 通过使用此技术,可以防止过度使用内存,并在处理大型错误响应时提高应用程序的性能。

有几种方法可以做到这一点,我们将在此示例中研究 TruncatedReadStream 技术:

internal sealed class TruncatedReadStream(Stream innerStream, long maxSize) : Stream
{
    private long _maxRemainingLength = maxSize;
    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;

    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }

    public override void Flush() => throw new NotSupportedException();

    public override int Read(byte[] buffer, int offset, int count)
    {
        return Read(new Span<byte>(buffer, offset, count));
    }

    public override int Read(Span<byte> buffer)
    {
        int readBytes = innerStream.Read(buffer.Slice(0, (int)Math.Min(buffer.Length, _maxRemainingLength)));
        _maxRemainingLength -= readBytes;
        return readBytes;
    }

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        return ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
    }

    public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
    {
        int readBytes = await innerStream.ReadAsync(buffer.Slice(0, (int)Math.Min(buffer.Length, _maxRemainingLength)), cancellationToken)
            .ConfigureAwait(false);
        _maxRemainingLength -= readBytes;
        return readBytes;
    }

    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();

    public override ValueTask DisposeAsync() => innerStream.DisposeAsync();

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            innerStream.Dispose();
        }
    }
}

TruncatedReadStream 的用法示例:

int maxErrorResponseLength = 1 * 1024; // 1 KB

HttpClient client = new HttpClient();
using HttpResponseMessage response = await client.GetAsync(Uri);

if (response.Content is not null)
{
    Stream responseReadStream = await response.Content.ReadAsStreamAsync();
    // If MaxErrorResponseLength is set and the response status code is an error code, then wrap the response stream in a TruncatedReadStream
    if (maxErrorResponseLength >= 0 && !response.IsSuccessStatusCode)
    {
        responseReadStream = new TruncatedReadStream(responseReadStream, maxErrorResponseLength);
    }
    // Read the response stream
    Memory<byte> buffer = new byte[1024];
    int readValue = await responseReadStream.ReadAsync(buffer);
}

示例:应用 CachePolicy 标头

警告

HttpClient 没有内置的逻辑来缓存响应。 除了自行实现所有缓存之外,没有解决方法。 仅仅设置标头并不能实现缓存。

HttpWebRequest 迁移到 HttpClient 时,必须正确处理与缓存相关的标头,例如 pragmacache-control。 这些标头控制如何缓存和检索响应,确保应用程序在性能和数据新鲜度方面按预期方式运行。

HttpWebRequest 中,你可能已使用 CachePolicy 属性来设置这些标头。 但是,在 HttpClient 中,需要在请求中手动设置这些标头。

旧代码使用 HttpWebRequest

HttpWebRequest request = WebRequest.CreateHttp(uri);
request.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore);
using HttpWebResponse response = (HttpWebResponse)request.GetResponse();

在较旧的 HttpWebRequest API 中,由于内置了对该功能的支持,因此应用 CachePolicy 非常简单。 但是,较新的 HttpClient API 不提供相同的内置功能。 尽管如此,可以通过实现手动添加缓存相关标头的 AddCacheControlHeaders 来实现类似的行为。

使用 HttpClient 的新代码

public static class CachePolicy
{
    public static void AddCacheControlHeaders(HttpRequestMessage request, RequestCachePolicy policy)

可以在此处找到 AddCacheControlHeaders 的实现。

AddCacheControlHeaders 使用情况:

static async Task AddCacheControlHeaders()
{
    HttpClient client = new HttpClient();
    HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, Uri);
    CachePolicy.AddCacheControlHeaders(requestMessage, new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore));
    HttpResponseMessage response = await client.SendAsync(requestMessage);
}

缓冲属性的使用

从 HttpWebRequest 迁移到 HttpClient时,请务必了解这两个 API 处理缓冲的方式的差异。

旧代码使用 HttpWebRequest

HttpWebRequest 中,可以通过 AllowWriteStreamBufferingAllowReadStreamBuffering 属性直接控制缓冲属性。 这些属性启用或禁用发送到服务器和从服务器接收的数据的缓冲。

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
request.AllowReadStreamBuffering = true; // Default is `false`.
request.AllowWriteStreamBuffering = false; // Default is `true`.

使用 HttpClient 的新代码

HttpClient 中,没有与 AllowWriteStreamBufferingAllowReadStreamBuffering 属性直接等效的属性。

HttpClient 本身不缓冲请求正文,而是将责任委派给 HttpContent 所使用的机构。 像 StringContentByteArrayContent 这样的内容在逻辑上已经缓冲在内存中,而使用 StreamContent 默认情况下不会产生任何缓冲。 若要强制缓冲内容,可以在发送请求之前调用 HttpContent.LoadIntoBufferAsync。 下面是一个示例:

HttpClient client = new HttpClient();

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
request.Content = new StreamContent(yourStream);
await request.Content.LoadIntoBufferAsync();

HttpResponseMessage response = await client.SendAsync(request);

HttpClient 中,默认情况下启用读取缓冲。 若要避免此问题,可以指定 HttpCompletionOption.ResponseHeadersRead 标志或使用 GetStreamAsync 帮助程序。

HttpClient client = new HttpClient();

using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
await using Stream responseStream = await response.Content.ReadAsStreamAsync();

// Or simply
await using Stream responseStream = await client.GetStreamAsync(uri);