.NET 8 网络改进

作者:
Máňa - Software Engineer, .NET Natalia
Kondratyeva - Software Engineer, .NET
排版:Alan Wang

随着新的 .NET 版本的发布,发表有关网络空间中新的有趣变化的博客文章已经成为一种传统。今年,我们要介绍 HTTP 部分的变化、新增指标、新的 HttpClientFactoryAPI 等。

HTTP

指标

.NET 8 使用 .NET 6 中引入的 System.Diagnostics.Metrics API 将内置 HTTP 指标添加到 ASP.NET Core 和 HttpClient。Metrics API 和新内置指标的语义都是与 OpenTelemetry 密切合作设计的,确保新指标符合标准,并与 Prometheus 和 Grafana 等流行工具良好配合。

System.Diagnostics.MetricsAPI 引入了许多 EventCounters 所缺少的新功能。新的内置指标广泛利用了这些功能,从而通过更简单、更优雅的工具实现了更广泛的功能。举几个例子:

  • Histograms 允许我们能够报告持续时间,例如请求持续时间( http.client.request.duration)或连接持续时间(http.client.connection.duration)。这些是没有 EventCounter 对应项的新指标。
  • Multi-dimensionality 允许我们将标签(又名属性或标签)附加到测量值上,这意味着我们可以将 server.address (标识 URI 来源)或 error.type(描述请求失败时的错误原因)之类的信息与测量值一起报告。多维还可以实现简化:为了报告打开的 HTTP 连接数,SocketsHttpHandler 使用 3 个 EventCounters:http11-connections-current-total、http20-connections-current-total 和 http30-connections-current-total,而这些计数器的 Metrics 等效项是单个工具 http.client.open_connections,其中使用 network.protocol.version 标记报告 HTTP 版本。
  • 为了帮助内置标签不足以对传出 HTTP 请求进行分类的用例,http.client.request.duration 指标支持注入用户定义的标签。这称为扩充。
  • IMeterFactory 集成可以隔离用于发出 HTTP 指标的 Meter 实例,从而更轻松地编写针对内置测量值运行验证的测试,并启用此类测试的并行执行。
  • 虽然这并不是特定于内置网络指标,但值得一提的是 System.Digangostics.Metrics 中的集合 API 也更高级:它们是强类型且性能更高,并且允许多个同时侦听器和侦听器访问未聚合的测量结果。

这些优势结合在一起带来了更好、更丰富的指标,这些指标可以通过 Prometheus 等第三方工具更有效地收集。由于 PromQL(Prometheus 查询语言)的灵活性,它允许针对从 .NET 网络堆栈收集的多维指标创建复杂的查询,用户现在可以深入了解 HttpClient 和 SocketsHttpHandler 实例的状态和运行状况,这在以前是不可能的。

不足之处在于,在 .NET 8 中,只有 System.Net.Http 和 System.Net.NameResolution 组件是使用 System.Diagnostics.Metrics 进行检测的,这意味着您仍然需要使用 EventCounters 从堆栈的较低层(例如 System.Net.Sockets)提取计数器. 虽然仍然支持以前版本中存在的所有内置 EventCounters,但 .NET 团队预计不会对 EventCounters 进行大量新投资,并且在未来的版本中会使用 System.Diagnostics.Metrics 添加新的内置检测工具。

有关使用内置 HTTP 指标的更多信息,请阅读我们有关 .NET 中的网络指标的教程。它包括有关使用 Prometheus 和 Grafana 进行收集和报告的示例,还演示了如何丰富和测试内置 HTTP 指标。有关内置工具的完整列表,请参阅 System.Net 指标的文档。如果您对服务器端更感兴趣,请阅读有关 ASP.NET Core 指标的文档。

扩展遥测

除了新指标之外,.NET 5 中引入的现有基于 EventSource 的遥测事件还增加了有关 HTTP 连接的更多信息(dotnet/runtime#88853):

- ConnectionEstablished(byte versionMajor, byte versionMinor)
+ ConnectionEstablished(byte versionMajor, byte versionMinor, long connectionId, string scheme, string host, int port, string? remoteAddress)- ConnectionClosed(byte versionMajor, byte versionMinor)
+ ConnectionClosed(byte versionMajor, byte versionMinor, long connectionId)- RequestHeadersStart()
+ RequestHeadersStart(long connectionId)

现在,当建立新连接时,该事件会记录 connectionId 及其方案、端口和对等 IP 地址。这样就能通过 RequestHeadersStart 事件将请求和响应与连接关联起来(当请求与池连接关联并开始处理时发生该事件),该事件还记录关联的 ConnectionId。这在用户希望查看为其 HTTP 请求提供服务的服务器的 IP 地址的诊断场景中尤其有价值,这也是添加此功能的主要动机(dotnet/runtime#63159)。

事件可以通过多种方式使用,请参阅 .NET 中的网络遥测 – 事件。但为了在进程内增强日志记录,可以使用自定义 EventListener 将请求/响应对与连接数据相关联:

using IPLoggingListener ipLoggingListener = new();
using HttpClient client = new();// Send requests in parallel.
await Parallel.ForAsync(0, 1000, async (i, ct) =>
{// Initialize the async local so that it can be populated by "RequestHeadersStart" event handler.RequestInfo info = RequestInfo.Current;using var response = await client.GetAsync("https://testserver");Console.WriteLine($"Response {response.StatusCode} handled by connection {info.ConnectionId}. Remote IP: {info.RemoteAddress}");// Process response...
});internal sealed class RequestInfo
{private static readonly AsyncLocal<RequestInfo> _asyncLocal = new();public static RequestInfo Current => _asyncLocal.Value ??= new();public string? RemoteAddress;public long ConnectionId;
}internal sealed class IPLoggingListener : EventListener
{private static readonly ConcurrentDictionary<long, string> s_connection2Endpoint = new ConcurrentDictionary<long, string>();// EventId corresponds to [Event(eventId)] attribute argument and the payload indices correspond to the event method argument order.// See: https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs#L100-L101private const int ConnectionEstablished_EventId = 4;private const int ConnectionEstablished_ConnectionIdIndex = 2;private const int ConnectionEstablished_RemoteAddressIndex = 6;// See: https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs#L106-L107private const int ConnectionClosed_EventId = 5;private const int ConnectionClosed_ConnectionIdIndex = 2;// See: https://github.com/dotnet/runtime/blob/a6e4834d53ac591a4b3d4a213a8928ad685f7ad8/src/libraries/System.Net.Http/src/System/Net/Http/HttpTelemetry.cs#L118-L119private const int RequestHeadersStart_EventId = 7;private const int RequestHeadersStart_ConnectionIdIndex = 0;protected override void OnEventSourceCreated(EventSource eventSource)
{if (eventSource.Name == "System.Net.Http"){EnableEvents(eventSource, EventLevel.LogAlways);}}protected override void OnEventWritten(EventWrittenEventArgs eventData)
{ReadOnlyCollection<object?>? payload = eventData.Payload;if (payload == null) return;switch (eventData.EventId){case ConnectionEstablished_EventId:// Remember the connection data.long connectionId = (long)payload[ConnectionEstablished_ConnectionIdIndex]!;string? remoteAddress = (string?)payload[ConnectionEstablished_RemoteAddressIndex];if (remoteAddress != null){Console.WriteLine($"Connection {connectionId} established to {remoteAddress}");s_connection2Endpoint.TryAdd(connectionId, remoteAddress);}break;case ConnectionClosed_EventId:connectionId = (long)payload[ConnectionClosed_ConnectionIdIndex]!;s_connection2Endpoint.TryRemove(connectionId, out _);break;case RequestHeadersStart_EventId:// Populate the async local RequestInfo with data from "ConnectionEstablished" event.connectionId = (long)payload[RequestHeadersStart_ConnectionIdIndex]!;if (s_connection2Endpoint.TryGetValue(connectionId, out remoteAddress)){RequestInfo.Current.RemoteAddress = remoteAddress;RequestInfo.Current.ConnectionId = connectionId;}break;}}
}

此外,Redirect 事件已扩展为包含重定向 URI:

-void Redirect();
+void Redirect(string redirectUri);

HTTP 错误代码

HttpClient 在诊断方面的问题之一是,当发生异常时,很难以编程方式区分错误的确切根本原因。区分它们的唯一方法是解析来自 HttpRequestException 的异常消息。此外,其他 HTTP 实现(如带有 ERROR_WINHTTP_* 错误码的 WinHTTP)以数字代码或枚举的形式提供了此类功能。所以 .NET 8引入了一个类似的枚举,并在 HTTP 处理抛出的异常中提供了它,它们是:

HttpRequestException 用于接收响应头之前的请求处理。

读取响应内容时抛出 HttpIOException。

在 dotnet/runtime#76644 API 提案中描述了 HttpRequestError 枚举的设计以及如何将其插入 HTTP 异常。

现在,HttpClient 方法的使用者可以更容易、更可靠地处理特定的内部错误:

using HttpClient httpClient = new();// Handling problems with the server:
try
{using HttpResponseMessage response = await httpClient.GetAsync("https://testserver", HttpCompletionOption.ResponseHeadersRead);using Stream responseStream = await response.Content.ReadAsStreamAsync();// Process responseStream ...
}
catch (HttpRequestException e) when (e.HttpRequestError == HttpRequestError.NameResolutionError)
{Console.WriteLine($"Unknown host: {e}");// --> Try different hostname.
}
catch (HttpRequestException e) when (e.HttpRequestError == HttpRequestError.ConnectionError)
{Console.WriteLine($"Server unreachable: {e}");// --> Try different server.
}
catch (HttpIOException e) when (e.HttpRequestError == HttpRequestError.InvalidResponse)
{Console.WriteLine($"Mangled responses: {e}");// --> Block list server.
}// Handling problems with HTTP version selection:
try
{using HttpResponseMessage response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://testserver"){Version = HttpVersion.Version20,VersionPolicy = HttpVersionPolicy.RequestVersionExact}, HttpCompletionOption.ResponseHeadersRead);using Stream responseStream = await response.Content.ReadAsStreamAsync();// Process responseStream ...
}
catch (HttpRequestException e) when (e.HttpRequestError == HttpRequestError.VersionNegotiationError)
{Console.WriteLine($"HTTP version is not supported: {e}");// Try with different HTTP version.
}

HTTPS 代理支持

这个版本中实现的最受欢迎的功能之一是支持 HTTPS 代理(dotnet/runtime#31113)。现在可以使用代理处理通过 HTTPS发送的请求,这意味着与代理的连接是安全的。这并没有涉及来自代理本身的请求,它仍然可以是 HTTP 或 HTTPS。对于纯文本 HTTP 请求,与 HTTPS 代理的连接是安全的(通过 HTTPS),然后是从代理到目标的纯文本请求。如果是 HTTPS 请求(代理隧道),打开隧道的初始 CONNECT 请求将通过安全通道 (HTTPS) 发送到代理,然后是从代理通过隧道到目的地的 HTTPS 请求。

如果要利用该功能,只需在设置代理时使用 HTTPS 方案即可:


using HttpClient client = new HttpClient(new SocketsHttpHandler()
{Proxy = new WebProxy("https://proxy.address:12345")
});using HttpResponseMessage response = await client.GetAsync("https://httpbin.org/");

HttpClientFactory

.NET 8 扩展了配置 HttpClientFactory 的方式,包括客户端默认设置、自定义日志记录和简化的 SocketsHttpHandler 配置。这些 API 在 Microsoft.Extensions.Http 包中实现,该包可在 NuGet 上获取,并包含对 .NET Standard 2.0 的支持。因此,此功能不仅适用于 .NET 8 上的客户端,而且适用于所有版本的 .NET,包括 .NET Framework(唯一的例外是 SocketsHttpHandler 相关 API,仅在 .NET 5+ 中可用)。

为所有客户端设置默认值

.NET 8 添加了设置默认配置的功能,该配置将用于 HttpClientFactory(dotnet/runtime#87914)创建的所有 HttpClient。当所有或大多数注册客户端包含相同的配置子集时,这非常有用。

考虑一个定义了两个命名客户端的示例,它们都需要在其消息处理程序链中使用 MyAuthHandler。

services.AddHttpClient("consoto", c => c.BaseAddress = new Uri("https://consoto.com/")).AddHttpMessageHandler<MyAuthHandler>();services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/")).AddHttpMessageHandler<MyAuthHandler>();

您现在可以使用以下 ConfigureHttpClientDefaults 方法提取公共部分:

services.ConfigureHttpClientDefaults(b => b.AddHttpMessageHandler<MyAuthHandler>());// both clients will have MyAuthHandler added by default
services.AddHttpClient("consoto", c => c.BaseAddress = new Uri("https://consoto.com/"));
services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://github.com/"));

所有与 AddHttpClient 一起使用的 IHttpClientBuilder 扩展方法也可以在 ConfigureHttpClientDefaults 中使用。

默认配置 (ConfigureHttpClientDefaults) 在客户端特定 (AddHttpClient) 配置之前应用于所有客户端;它们在注册中的相对位置并不重要。ConfigureHttpClientDefaults 可以注册多次,在这种情况下,配置将按照注册的顺序一一应用。配置的任何部分都可以在特定于客户端的配置中被重写或修改,例如,您可以为 HttpClient 对象或主处理程序设置额外的设置,删除以前添加的额外处理程序等。

请注意,从 8.0 开始,ConfigureHttpMessageHandlerBuilder 方法已被弃用。您应该改用 ConfigurePrimaryHttpMessageHandler(Action<httpmessagehandler,iserviceprovider< span=“”>>))) 或 ConfigureAdditionalHttpMessageHandlers 方法,需要分别修改先前配置的主处理程序或附加处理程序列表。

// by default, adds User-Agent header, uses HttpClientHandler with UseCookies=false
// as a primary handler, and adds MyAuthHandler to all clients
services.ConfigureHttpClientDefaults(b =>b.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0")).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }).AddHttpMessageHandler<MyAuthHandler>());// HttpClient will have both User-Agent (from defaults) and BaseAddress set
// + client will have UseCookies=false and MyAuthHandler from defaults
services.AddHttpClient("modify-http-client", c => c.BaseAddress = new Uri("https://httpbin.org/"))// primary handler will have both UseCookies=false (from defaults) and MaxConnectionsPerServer set
// + client will have User-Agent and MyAuthHandler from defaults
services.AddHttpClient("modify-primary-handler").ConfigurePrimaryHandler((h, _) => ((HttpClientHandler)h).MaxConnectionsPerServer = 1);// MyWrappingHandler will be inserted at the top of the handlers chain
// + client will have User-Agent, UseCookies=false and MyAuthHandler from defaults
services.AddHttpClient("insert-handler-into-chain")).ConfigureAdditionalHttpMessageHandlers((handlers, _) =>handlers.Insert(0, new MyWrappingHandler());// MyAuthHandler (initially from defaults) will be removed from the handler chain
// + client will still have User-Agent and UseCookies=false from defaults
services.AddHttpClient("remove-handler-from-chain")).ConfigureAdditionalHttpMessageHandlers((handlers, _) =>handlers.Remove(handlers.Single(h => h is MyAuthHandler)));

修改 HttpClient 日志记录

自定义(或者干脆关闭)HttpClientFactory 日志记录是长期请求的功能之一(dotnet/runtime#77312)。

旧日志记录概述

HttpClientFactory 添加的默认(“旧”)日志记录非常冗长,每个请求发出 8 条日志消息:

  1. 使用请求 URI 启动通知 —— 在通过委托处理程序管道传播之前;
  2. 请求标头 —— 在处理程序管道之前;
  3. 使用请求 URI 启动通知 —— 在处理程序管道之后;
  4. 请求标头 —— 处理程序管道之后;
  5. 随着时间的流逝停止通知 —— 在通过委托处理程序管道传播回响应之前;
  6. 响应标头 —— 在传播回响应之前;
  7. 随着时间的流逝停止通知 —— 在传播回响应之后;
  8. 响应标头 —— 将响应传播回来之后。

这可以用下面的图来说明。在下图中,* 和 […] 表示日志记录事件(在默认实现中,日志消息被写入 ILogger),–> 表示通过应用程序层和传输层的数据流。

Request -->
*   [Start notification]    // "Start processing HTTP request ..." (1)
*   [Request headers]       // "Request Headers: ..." (2)--> Additional Handler #1 -->--> .... -->--> Additional Handler #N -->
*           [Start notification]    // "Sending HTTP request ..." (3)
*           [Request headers]       // "Request Headers: ..." (4)--> Primary Handler -->--------Transport--layer------->// Server sends response<-------Transport--layer--------<-- Primary Handler <--
*           [Stop notification]    // "Received HTTP response ..." (5)
*           [Response headers]     // "Response Headers: ..." (6)<-- Additional Handler #N <--<-- .... <--<-- Additional Handler #1 <--
*   [Stop notification]    // "End processing HTTP request ..." (7)
*   [Response headers]     // "Response Headers: ..." (8)Response <--

默认 HttpClientFactory 日志记录的控制台输出如下所示:

var client = _httpClientFactory.CreateClient();
await client.GetAsync("https://httpbin.org/get");
info: System.Net.Http.HttpClient.test.LogicalHandler[100]Start processing HTTP request GET https://httpbin.org/get
trce: System.Net.Http.HttpClient.test.LogicalHandler[102]Request Headers:....
info: System.Net.Http.HttpClient.test.ClientHandler[100]Sending HTTP request GET https://httpbin.org/get
trce: System.Net.Http.HttpClient.test.ClientHandler[102]Request Headers:....
info: System.Net.Http.HttpClient.test.ClientHandler[101]Received HTTP response headers after 581.2898ms - 200
trce: System.Net.Http.HttpClient.test.ClientHandler[103]Response Headers:....
info: System.Net.Http.HttpClient.test.LogicalHandler[101]End processing HTTP request after 618.9736ms - 200
trce: System.Net.Http.HttpClient.test.LogicalHandler[103]Response Headers:....

请注意,为了查看跟踪级别消息,您需要在全局日志记录配置文件中选择此选项或通过 SetMinimumLevel(LogLevel.Trace)进行设置 。但即使只考虑信息级别的消息,“旧”日志记录每个请求仍然有 4 条消息。

要删除默认(或之前添加的)日志记录,您可以使用新的 RemoveAllLoggers() 扩展方法。它与上面“为所有客户端设置默认值”部分中描述的 ConfigureHttpClientDefaults API 结合起来特别强大。这样,您就可以在一行中删除所有客户端的“旧”日志记录:

services.ConfigureHttpClientDefaults(b => b.RemoveAllLoggers()); // remove HttpClientFactory default logging for all clients

如果您需要恢复“旧”日志记录,例如 针对特定客户端,您可以使用 AddDefaultLogger() 来执行此操作。

添加自定义日志记录

除了能够删除“旧”日志记录之外,新的 HttpClientFactory API 还允许您完全自定义日志记录。您可以指定当 HttpClient 启动请求、接收响应或引发异常时记录的内容和方式。

您可以同时添加多个自定义记录器 - 例如,控制台和 ETW 记录器,或“包装”和“不包装”记录器。由于其附加性质,您可能需要事先显式删除默认的“旧”日志记录。

如果要添加自定义日志记录,您需要实现 IHttpClientLogger 接口,然后使用 AddLogger 将自定义记录器添加到客户端。请注意,日志记录实现不应引发任何异常,否则可能会中断请求执行。

注册:

services.AddSingleton<SimpleConsoleLogger>(); // register the logger in DIservices.AddHttpClient("foo") // add a client.RemoveAllLoggers() // remove previous logging.AddLogger<SimpleConsoleLogger>(); // add the custom logger

示例记录器实现:

// outputs one line per request to console
public class SimpleConsoleLogger : IHttpClientLogger
{public object? LogRequestStart(HttpRequestMessage request) => null;public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
=> Console.WriteLine($"{request.Method} {request.RequestUri?.AbsoluteUri} - {(int)response.StatusCode} {response.StatusCode} in {elapsed.TotalMilliseconds}ms");public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)
=> Console.WriteLine($"{request.Method} {request.RequestUri?.AbsoluteUri} - Exception {e.GetType().FullName}: {e.Message}");
}

示例输出:

var client = _httpClientFactory.CreateClient("foo");
await client.GetAsync("https://httpbin.org/get");
await client.PostAsync("https://httpbin.org/post", new ByteArrayContent(new byte[] { 42 }));
await client.GetAsync("http://httpbin.org/status/500");
await client.GetAsync("http://localhost:1234");
GET https://httpbin.org/get - 200 OK in 393.2039ms
POST https://httpbin.org/post - 200 OK in 95.524ms
GET https://httpbin.org/status/500 - 500 InternalServerError in 99.5025ms
GET http://localhost:1234/ - Exception System.Net.Http.HttpRequestException: No connection could be made because the target machine actively refused it. (localhost:1234)
请求上下文对象

您可以使用上下文对象来匹配 LogRequestStart 调用和相应的 LogRequestStop 调用,从而将数据从一个调用传递到另一个调用。上下文对象由 LogRequestStart 产生,然后传递回 LogRequestStop。这可以是一个属性包或任何其他保存必要数据的对象。

如果不需要上下文对象,实现可以从 LogRequestStart 返回 null。

以下示例显示了如何使用上下文对象来传递自定义请求标识符。

public class RequestIdLogger : IHttpClientLogger
{private readonly ILogger _log;public RequestIdLogger(ILogger<RequestIdLogger> log)
{_log = log;}private static readonly Action<ILogger, Guid, string?, Exception?> _requestStart =LoggerMessage.Define<Guid, string?>(LogLevel.Information,EventIds.RequestStart,"Request Id={RequestId} ({Host}) started");private static readonly Action<ILogger, Guid, double, Exception?> _requestStop =LoggerMessage.Define<Guid, double>(LogLevel.Information,EventIds.RequestStop,"Request Id={RequestId} succeeded in {elapsed}ms");private static readonly Action<ILogger, Guid, Exception?> _requestFailed =LoggerMessage.Define<Guid>(LogLevel.Error,EventIds.RequestFailed,"Request Id={RequestId} FAILED");public object? LogRequestStart(HttpRequestMessage request){var ctx = new Context(Guid.NewGuid());_requestStart(_log, ctx.RequestId, request.RequestUri?.Host, null);return ctx;}public void LogRequestStop(object? ctx, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
=> _requestStop(_log, ((Context)ctx!).RequestId, elapsed.TotalMilliseconds, null);public void LogRequestFailed(object? ctx, HttpRequestMessage request, HttpResponseMessage? response, Exception e, TimeSpan elapsed)
=> _requestFailed(_log, ((Context)ctx!).RequestId, null);public static class EventIds{public static readonly EventId RequestStart = new(1, "RequestStart");public static readonly EventId RequestStop = new(2, "RequestStop");public static readonly EventId RequestFailed = new(3, "RequestFailed");}record Context(Guid RequestId);
}

info: RequestIdLogger[1]Request Id=d0d63b84-cd67-4d21-ae9a-b63d26dfde50 (httpbin.org) started
info: RequestIdLogger[2]Request Id=d0d63b84-cd67-4d21-ae9a-b63d26dfde50 succeeded in 530.1664ms
info: RequestIdLogger[1]Request Id=09403213-dd3a-4101-88e8-db8ab19e1eeb (httpbin.org) started
info: RequestIdLogger[2]Request Id=09403213-dd3a-4101-88e8-db8ab19e1eeb succeeded in 83.2484ms
info: RequestIdLogger[1]Request Id=254e49bd-f640-4c56-b62f-5de678eca129 (httpbin.org) started
info: RequestIdLogger[2]Request Id=254e49bd-f640-4c56-b62f-5de678eca129 succeeded in 162.7776ms
info: RequestIdLogger[1]Request Id=e25ccb08-b97e-400d-b42b-b09d6c42adec (localhost) started
fail: RequestIdLogger[3]Куйгуые Шв=у25сси08-и97у-400в-и42и-и09в6с42фвус АФШДУВ
避免从内容流中读取

如果您打算读取和记录(例如:请求和响应内容),请注意,它可能会对最终用户体验产生不利的副作用并导致错误。例如,请求内容可能在发送之前被消耗,或者巨大的响应内容可能最终被缓冲在内存中。此外,在 .NET 7 之前,访问标头不是线程安全的,可能会导致错误和意外行为。

谨慎使用异步日志记录

我们预计同步 IHttpClientLogger 接口适用于绝大多数自定义日志记录用例。出于性能原因,建议不要在日志记录中使用异步。但是,如果严格要求日志记录中的异步访问,您可以实现异步版本 IHttpClientAsyncLogger。它派生自 IHttpClientLogger,因此可以使用相同的 AddLogger API 进行注册。

请注意,在这种情况下,还应该实现日志记录方法的同步对应项,特别是如果该实现是面向 .NET Standard 或 .NET 5+ 的库的一部分。同步对应项是从同步 HttpClient.Send 方法调用的;即使 .NET Standard 表面不包含它们,.NET Standard 库也可以在 .NET 5+ 应用程序中使用,因此最终用户可以访问同步 HttpClient.Send 方法。

包装和不包装记录器

当您添加记录器时,您可以显式设置 wrapHandlersPipeline 参数来指定记录器是否将被

  • 包装处理程序管道(添加到管道的顶部,对应于上面旧日志记录概述部分中的 1、2、7 和 8 号消息)
  Request -->
*   [LogRequestStart()]                // wrapHandlersPipeline=TRUE--> Additional Handlers #1..N -->    // handlers pipeline--> Primary Handler -->--------Transport--layer--------<-- Primary Handler <--<-- Additional Handlers #N..1 <--    // handlers pipeline
*   [LogRequestStop()]                 // wrapHandlersPipeline=TRUEResponse <--
  • 或者,不包装处理程序管道(添加到底部,对应于上面旧日志记录概述部分中的第 3、4、5 和 6 号消息)。
  Request -->--> Additional Handlers #1..N --> // handlers pipeline
*     [LogRequestStart()]             // wrapHandlersPipeline=FALSE--> Primary Handler -->--------Transport--layer--------<-- Primary Handler <--
*     [LogRequestStop()]              // wrapHandlersPipeline=FALSE<-- Additional Handlers #N..1 <-- // handlers pipelineResponse <--

默认情况下,记录器被添加为不包装。

在向管道添加重试处理程序的情况下(例如 Polly 或某些自定义重试实现),包装和不包装管道之间的区别最为显着。在这种情况下,包装记录器(位于顶部)将记录有关单个成功请求的消息,记录的经过时间将是从用户发起请求到收到响应的总时间。非包装记录器(位于底部)将记录每次重试迭代,最初的迭代可能记录异常或不成功的状态代码,最后一个记录成功。每种情况消耗的时间纯粹是在主处理程序中花费的时间(实际在网络上发送请求的处理程序,例如 HttpClientHandler)。

这可以用下图来说明:

  • 包装案例(wrapHandlersPipeline=TRUE)
Request -->
*   [LogRequestStart()]--> Additional Handlers #1..(N-1) -->--> Retry Handler -->--> //1--> Primary Handler --><-- "503 Service Unavailable" <----> //2--> Primary Handler -><-- "503 Service Unavailable" <----> //3--> Primary Handler --><-- "200 OK" <--<-- Retry Handler <--<-- Additional Handlers #(N-1)..1 <--
*   [LogRequestStop()]Response <--info: Example.CustomLogger.Wrapping[1]GET https://consoto.com/
info: Example.CustomLogger.Wrapping[2]200 OK - 809.2135ms
  • 不包装案例(wrapHandlersPipeline=FALSE)
Request -->--> Additional Handlers #1..(N-1) -->--> Retry Handler -->--> //1
*           [LogRequestStart()]--> Primary Handler --><-- "503 Service Unavailable" <--
*           [LogRequestStop()]--> //2
*           [LogRequestStart()]--> Primary Handler --><-- "503 Service Unavailable" <--
*           [LogRequestStop()]--> //3
*           [LogRequestStart()]--> Primary Handler --><-- "200 OK" <--
*           [LogRequestStop()]<-- Retry Handler <--<-- Additional Handlers #(N-1)..1 <--Response <--
info: Example.CustomLogger.NotWrapping[1]GET https://consoto.com/
info: Example.CustomLogger.NotWrapping[2]503 Service Unavailable - 98.613ms
info: Example.CustomLogger.NotWrapping[1]GET https://consoto.com/
info: Example.CustomLogger.NotWrapping[2]503 Service Unavailable - 96.1932ms
info: Example.CustomLogger.NotWrapping[1]GET https://consoto.com/
info: Example.CustomLogger.NotWrapping[2]200 OK - 579.2133ms

简化的 SocketsHttpHandler 配置

.NET 8 添加了更方便、更流畅的方式来使用 SocketsHttpHandler 作为 HttpClientFactory 中的主处理程序(dotnet/runtime#84075)。

您可以使用 UseSocketsHttpHandler 方法设置和配置 SocketsHttpHandler。您可以使用 IConfiguration 从配置文件设置 SocketsHttpHandler 属性,也可以从代码中配置它,或者可以结合使用这两种方法。

请注意,将 IConfiguration 应用于 SocketsHttpHandler 时,仅解析 bool、int、Enum 或 TimeSpan 类型的 SocketsHttpHandler 属性。IConfiguration 中所有不匹配的属性都将被忽略。配置仅在注册时解析一次并且不会重新加载,因此在应用程序重新启动之前,处理程序不会反映任何配置文件更改。

// sets up properties on the handler directly
services.AddHttpClient("foo").UseSocketsHttpHandler((h, _) => h.UseCookies = false);// uses a builder to combine approaches
services.AddHttpClient("bar").UseSocketsHttpHandler(b =>b.Configure(config.GetSection($"HttpClient:bar")) // loads simple properties from config.Configure((h, _) => // sets up SslOptions in code{h.SslOptions.RemoteCertificateValidationCallback = delegate { return true; };}););{"HttpClient": {"bar": {"AllowAutoRedirect": true,"UseCookies": false,"ConnectTimeout": "00:00:05"}}
}

QUIC

OpenSSL 3 支持

当前大多数 Linux 发行版在其最新版本中都采用了 OpenSSL 3:

  • Debian 12+:Bookworm OpenSSL
  • Ubuntu 22+:Jammy OpenSSL
  • Fedora 37+:Fedora OpenSSL
  • OpenSUSE:Tumbleweed OpenSSL
  • AlmaLinux 9+:AlmaLinux 9 软件包存储库

.NET 8 的 QUIC 支持已准备就绪(dotnet/runtime#81801)。

实现这一目标的第一步是确保 System.Net.Quic 下使用的 QUIC 实现 MsQuic 可以与 OpenSSL 3+ 一起使用。这项工作在 MsQuic 存储库 microsoft/msquic#2039 中进行。下一步是确保构建并发布的 libmsquic 包相应的依赖于特定发行版和版本的默认 OpenSSL 版本。例如 Debian 发行版:

  • Debian 11 libmsquic 依赖于 OpenSSL 1.1
  • Debian 12 libmsquic 依赖于 OpenSSL 3

最后一步是确保正在测试的MsQuic 和 OpenSSL版本正确,并且测试覆盖了所有 .NET 支持的发行版。

异常

在 .NET 7 中发布 QUIC API(作为预览功能)后,我们收到了几个有关异常的问题:

  • dotnet/runtime#78751:
    找不到主机时 QuicConnection.ConnectAsync 引发 SocketException

  • dotnet/runtime#78096:
    QuicListener AcceptConnectionAsync 和 OperationCanceledException

  • dotnet/runtime#75115:QuicListener.AcceptConnectionAsync 重新抛出异常

在 .NET 8 中,System.Net.Quic 异常行为在 dotnet/runtime#82262 中进行了彻底修改,并且解决了上述问题。

修订的主要目标之一是确保 System.Net.Quic 中的异常行为在整个命名空间中尽可能一致。总的来说,当前的行为可以总结如下:

  • QuicException:特定于 QUIC 协议或与其处理相关的所有错误。

    • 连接由本地或由对等方关闭。
    • 连接因不活动而超时。
    • 流被本地或由对等方中止。
    • QuicError 中描述的其他错误
  • SocketException:针对网络问题,例如网络状况、名称解析或用户错误。

    • 地址已被使用。
    • 无法访问目标主机。
    • 指定的地址无效。
    • 无法解析主机名。
  • AuthenticationException:所有与 TLS 相关的问题。目标是具有与 SslStream 类似的行为。

    • 证书相关错误。
    • ALPN 协商错误。
    • 握手期间用户取消。
  • ArgumentException:当提供 QuicConnectionOptions 或 QuicListenerOptions 无效时。

    • 提供的流限制不在 0-65535 范围内。
    • 省略强制属性,例如:DefaultCloseErrorCode 或 DefaultStreamErrorCode。
    • 未指定 ClientAuthenticationOptions 或 ServerAuthenticationOptions。
  • OperationCanceledException:每当 CancellationToken 被触发时取消。

  • ObjectDisposedException:每当在已释放的对象上调用方法时。

请注意,上述示例并不详尽。

除了改变行为之外,QuicException 也发生了改变。其中一项变化是调整 QuicError 枚举值。现在 SocketException 涵盖的项目已被删除,并为用户回调错误添加了一个新值(dotnet/runtime#87259)。新添加的 CallbackError 用于区分

QuicListenerOptions.ConnectionOptionsCallback 引发的异常与 System.Net.Quic 引发的异常(dotnet/runtime#88614)。因此,如果用户代码抛出 ArgumentException,QuicListener.AcceptConnectionAsync 会将其包装在 QuicException 中,并将 QuicError 设置为 CallbackError,并且内部异常将包含原始用户抛出的异常。它可以这样使用:

await using var listener = await QuicListener.ListenAsync(new QuicListenerOptions
{// ...ConnectionOptionsCallback = (con, hello, token) =>{if (blockedServers.Contains(hello.ServerName)){throw new ArgumentException($"Connection attempt from forbidden server: '{hello.ServerName}'.", nameof(hello));}return ValueTask.FromResult(new QuicServerConnectionOptions{// ...});},
});
// ...
try
{await listener.AcceptConnectionAsync();
}
catch (QuicException ex) when (ex.QuicError == QuicError.CallbackError && ex.InnerException is ArgumentException)
{Console.WriteLine($"Blocked connection attempt from forbidden server: {ex.InnerException.Message}");
}

异常部分的最后一个更改是将传输错误代码添加到 QuicException 中(dotnet/runtime#88550)。传输错误代码由 RFC 9000 传输错误代码定义,并且 MsQuic 的 System.Net.Quic 已经可以使用它们,只是没有公开。因此,QuicException 中添加了一个新的可为 null 的属性:TransportErrorCode。我们要感谢社区贡献者 AlexRach,他在 dotnet/runtime#88614 中实现了这一更改。

Socket

Socket 空间中影响最大的更改是显着减少无连接(UDP) Socket 的分配(dotnet/runtime#30797)。使用 UDP Socket 时,分配的最大贡献者之一是在每次调用 Socket.ReceiveFrom 时分配一个新的 EndPoint 对象(并支持 IPAddress 等分配)。为了缓解这个问题,引入了一组使用 SocketAddress 的新 API(dotnet/runtime#87397)。SocketAddress 在内部将 IP 地址保存为平台相关形式的字节数组,以便可以将其直接传递给操作系统调用。因此,在调用本机 Socket 函数之前不需要复制 IP 地址数据。

此外,新添加的 ReceiveFrom-system-net-sockets-socketflags-system-net-socketaddress)) 和 ReceiveFromAsync-system-net-sockets-socketflags-system-net-socketaddress-system-threading-cancellationtoken)) 重载不会在每次调用时实例化新的 IPEndPoint,而是在适当的位置改变提供的 receiveAddress 参数。所有这些一起可以用来提高 UDP Socket 代码的效率:


// Same initialization code as before, no change here.
Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
byte[] message = Encoding.UTF8.GetBytes("Hello world!");
byte[] buffer = new byte[1024];
IPEndPoint endpoint = new IPEndPoint(IPAddress.Loopback, 12345);
server.Bind(endpoint);// --------
// Original code that would allocate IPEndPoint for each ReceiveFromAsync:
Task<SocketReceiveFromResult> receiveTaskOrig = server.ReceiveFromAsync(buffer, SocketFlags.None, endpoint);
await client.SendToAsync(message, SocketFlags.None, endpoint);
SocketReceiveFromResult resultOrig = await receiveTaskOrig;Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, result.ReceivedBytes) + " from " + result.RemoteEndPoint);
// Prints:
// Hello world! from 127.0.0.1:59769// --------
// New variables that can be re-used for subsequent calls:
SocketAddress receivedAddress = endpoint.Serialize();
SocketAddress targetAddress = endpoint.Serialize();// New code that will mutate provided SocketAddress for each ReceiveFromAsync:
ValueTask<int> receiveTaskNew = server.ReceiveFromAsync(buffer, SocketFlags.None, receivedAddress);
await client.SendToAsync(message, SocketFlags.None, targetAddress);
var length = await receiveTaskNew;Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, length) + " from " + receivedAddress);
// Prints:
// Hello world! from InterNetwork:16:{233,121,127,0,0,1,0,0,0,0,0,0,0,0}

最重要的是,在 dotnet/runtime#86872 中改进了 SocketAddress 的使用。SocketAddress 现在有几个额外的成员,使其本身更有用:

  • getter Buffer:访问整个底层地址缓冲区。
  • setter Size:能够调整上述缓冲区大小(只能调整到较小的尺寸)。
  • static GetMaximumAddressSize:根据地址类型获取所需的缓冲区大小。
  • 接口 IEquatable<socketaddress< span=“”>>:SocketAddress 可用于区分 Socket 与之通信的对等点,例如作为字典中的键(这不是新功能,它只是使其可通过接口调用)。

最后,删除了一些内部生成的 IP 地址数据副本,以提高性能。

Networking Primitives

MIME 类型

添加缺失的 MIME 类型是网络空间中投票最多的问题之一(dotnet/runtime#1489)。这是一个主要由社区驱动的更改,最终形成了 dotnet/runtime#85807 API 提案。由于此添加需要经过 API 审核流程,因此有必要确保添加的类型是相关的并遵循规范(IANA 媒体类型)。对于这项准备工作,我们要感谢社区贡献者 Bilal-io 和 mmarinchenko。

IPNetwork

.NET 8 中添加的另一个新 API 是新类型 IPNetwork(dotnet/runtime#79946)。该结构允许指定 RFC 4632 中定义的无类 IP 子网。例如:

  • 127.0.0.0/8 用于与 A 类子网对应的无类定义。
  • 42.42.128.0/17 用于 215 个地址的无类子网。
  • 2a01:110:8012::/100 用于 228 个地址的 IPv6 子网。

新的 API 可以使用构造函数从 IPAddress 和前缀长度进行构造,也可以通过 TryParse 或 Parse 从字符串进行解析。最重要的是,它允许使用 Contains 方法检查 IPAddress 是否属于子网。示例用法如下:


// IPv4 with manual construction.
IPNetwork ipNet = new IPNetwork(new IPAddress(new byte[] { 127, 0, 0, 0 }), 8);
IPAddress ip1 = new IPAddress(new byte[] { 255, 0, 0, 1 });
IPAddress ip2 = new IPAddress(new byte[] { 127, 0, 0, 10 });
Console.WriteLine($"{ip1} {(ipNet.Contains(ip1) ? "belongs" : "doesn't belong")} to {ipNet}");
Console.WriteLine($"{ip2} {(ipNet.Contains(ip2) ? "belongs" : "doesn't belong")} to {ipNet}");
// Prints:
// 255.0.0.1 doesn't belong to 127.0.0.0/8
// 127.0.0.10 belongs to 127.0.0.0/8// IPv6 with parsing.
IPNetwork ipNet = IPNetwork.Parse("2a01:110:8012::/96");
IPAddress ip1 = IPAddress.Parse("2a01:110:8012::1742:4244");
IPAddress ip2 = IPAddress.Parse("2a01:110:8012:1010:914e:2451:16ff:ffff");
Console.WriteLine($"{ip1} {(ipNet.Contains(ip1) ? "belongs" : "doesn't belong")} to {ipNet}");
Console.WriteLine($"{ip2} {(ipNet.Contains(ip2) ? "belongs" : "doesn't belong")} to {ipNet}");
// Prints:
// 2a01:110:8012::1742:4244 belongs to 2a01:110:8012::/96
// 2a01:110:8012:1010:914e:2451:16ff:ffff doesn't belong to 2a01:110:8012::/96

请注意,不要将此类型与自 1.0 以来 ASP.NET Core 中存在的

Microsoft.AspNetCore.HttpOverrides.IPNetwork 类混淆。我们预计 ASP.NET API 最终将迁移到新的 System.Net.IPNetwork 类型(dotnet/aspnetcore#46157)。

最后说明

本文选择的主题并不是 .NET 8 中所有更改的详尽列表,只是我们认为最有趣的内容。如果您对性能改进更感兴趣,您可以查看 Stephen 的大型性能博客文章中的网络部分。如果您有任何疑问或发现任何错误,可以在 dotnet/runtime 存储库中与我们联系。

最后,我要感谢我的合著者:

  • Metrics 的作者:@antonfirsov。
  • HttpClientFactory 的作者:@CarnaViire。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/578178.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python RCL 调试劳德巴赫

Python RCL 调试劳德巴赫 比较旧的调试方式是TRACE32 Remote API, 看起来比较麻烦 新的是RCL trace32的安装路径下有官方的python教程, 用浏览器可以打开 <path>/demo/api/python/rcl/doc/html/index.html 需要先安装python库, 也在trace32安装路径下 <path>/de…

谷歌公布 2023 年最受欢迎的 Chrome 扩展

2023年&#xff0c;谷歌公布了最受欢迎的Chrome扩展&#xff0c;共有12款涵盖了多个领域&#xff0c;从提升工作效率到游戏娱乐。这些扩展旨在增强用户的浏览体验和生产力。 Scribe 功能&#xff1a;使用AI记录工作流程并创建逐步指南。 特点&#xff1a;自动记录和生成详细…

【完整项目】基于Python+Tkinter+FFD(free-form deformations)的2D彩色图像实时网格自由变形软件的设计与实现

文章目录 一、效果展示二、前言介绍三、软件使用说明3.1 环境配置3.2 文件结构3.3 准备工作 四、快速开始五、主要思路算法思路网格变形和实时操作思路 六、总结与反思七、代码链接八、其他完整项目 一、效果展示 校正比萨斜塔&#xff1a; 人脸变形&#xff1a; 图像拼接结果…

【运维】大日志文件按日期划分方法的改进及思考

之前由于运维需求&#xff0c;需要对一个大日志文件按照日期进行划分&#xff0c;将每天的日志写入一个单独的文件中。 刚开始接到这个需求后&#xff0c;我浏览了一遍日志文件&#xff0c;发现里面只有11月17号到11月22号的日志&#xff0c;天数不多&#xff0c;可以尝试手动…

conan 入门(三十七):conan 2.x通过定义环境变量(environment)执行make编译只有Makefile的项目(erpcgen)

之前一篇博客《conan 入门(三十四):conan 2.x实现对只有Makefile的项目(erpcgen)的封装示例》介绍如何用AutotoolsToolchain,Autotools实现来编译只有Makefile的项目&#xff0c;因为Autotools.make,Autotools.install方法就是执行make来编译项目。 我原本是想用self.run来执行…

C# 读取Word表格到DataSet

目录 功能需求 Office 数据源的一些映射关系 范例运行环境 配置Office DCOM 关键代码 组件库引入 ​核心代码 杀掉进程 总结 功能需求 在应用项目里&#xff0c;多数情况下我们会遇到导入 Excel 文件数据到数据库的功能需求&#xff0c;但某些情况下&#xff0c;也存…

基于VUE3+Layui从头搭建通用后台管理系统(前端篇)十七:演示功能模块相关功能实现

一、本章内容 本章实现常见业务功能,包括文章管理、商品管理、订单管理、会员管理等功能。 1. 详细课程地址: https://edu.csdn.net/course/detail/38183 2. 源码下载地址: 点击下载 二、界面预览 三、开发视频 3.1 B站视频地址:

Jenkins 自动设置镜像版本号

使用Jenkins环境变量当作镜像版本号 这样version变量就是版本号,在镜像构建的过程中可以使用 docker build 之后&#xff0c;如果有自己的镜像库&#xff0c;肯定要docker push 一下 至于部署的步骤&#xff0c;一般需要stop并删除原有的容器.我这里用的是docker-compose。同样…

如何使用 pnpm 实现前端 Monorepo项目管理

前言 随着软件开发项目变得越来越庞大和复杂&#xff0c;如何有效管理和维护代码库成为了一个重要的问题。一种流行的解决方案是 Monorepo&#xff0c;也就是在一个版本控制系统中管理所有的项目代码。 什么是 Monorepo Monorepo 是一种项目代码管理方式&#xff0c;指单个仓…

PHP-8.1.0-dev 后门命令执行漏洞复现_zerodiumvar_dump

0x00漏洞描述 PHP 8.1.0-dev 版本在2021年3月28日被植入后门&#xff0c;但是后门很快被发现并清除。当服务器存在该后门时&#xff0c;攻击者可以通过发送User-Agentt头来执行任意代码。 0x01影响范围 PHP 8.1.0-dev 0x02环境搭建 1、本次环境搭建使用vulhub中的docker环…

加速 SQL 查询的 9 种方法

SQL 是开发和查询数据库的主要语言&#xff0c;但它有一些怪癖。在我的上一篇文章中&#xff0c;我分享了 7 个需要避免的 SQL 错误。现在&#xff0c;让我们来看看编写更快的 SQL 查询的 9 个最佳实践。 更快 SQL 查询的 9 个最佳实践 仅检索您需要的列 使用 CASE 而不是 UP…

PPT中加入页码

PPT中加入页码 文章目录 简单版本样式更改 简单版本 PPT中插入页码&#xff0c;基础的就是在“插入”选项卡中单机“幻灯片编号”即可 样式更改 然而&#xff0c;就像我们做幻灯片不满足于白底黑字一样&#xff0c;页码也总不能是默认的样式。 比如&#xff0c;在页码下面…

2023年全国省市区县行政区划矢量数据(含10段线)

2023年&#xff0c;中国地图面貌发生了重大变化&#xff0c;领土面积由960万平方公里扩大到1045万平方公里&#xff0c;九段线改为了十段线。 因此在使用地图的时候&#xff0c;特别是做全国的地图的时候&#xff0c;一定需要最新的行政界限&#xff0c;今天就将最新的省市县行…

http请求超时 ,用PHP如何解决的?

当进行HTTP请求时&#xff0c;有时候可能会遇到请求超时的情况。为了解决这个问题&#xff0c;你可以考虑以下几个方面&#xff1a; 设置脚本的最大执行时间&#xff1a; 在PHP中&#xff0c;可以使用set_time_limit函数来设置脚本的最大执行时间。该函数接受一个以秒为单位的整…

串口发送控制命令,实现一些外设LED 风扇 马达

main.c #include "uart4.h"int main(){char a;char buf[128];uart4_config();while (1){/* //接收一个字符数据agetchar();//发送接收的字符1putchar(a1);putchar(\r);putchar(\n); */gets(buf); // 读取字符串//puts(buf); // 输出字符串if(strcmp(buf,"l…

linux无法访问共享目录,ls hgfs失败

刚在新买的华为电脑上安装ubuntu20&#xff0c;共享文件出现各种问题&#xff1a; dlubuntu:/mnt$ ls ls: cannot access hgfs: Permission denied hgfs 解决方法&#xff1a; 1.首先输入vmware-hgfsclient&#xff0c;看看是否共享文件夹已经建立&#xff0c;没有的话去参考…

在Vue3中使用qrcode库实现二维码生成

本文主要介绍在Vue3中使用qrcode库实现二维码生成的方法。 目录 一、基础用法实现二、toDataURL()方法三、toCanvas()方法四、create()方法五、QRCodeRenderersOptions()方法 在Vue3中实现二维码生成需要使用第三方库来处理生成二维码的逻辑。常用的库有 qrcode和 vue-qrcode…

支付平台在选择服务器租用时要注意什么?

如果要建设一个支付平台的话要进行服务器租用&#xff0c;一旦涉及到钱的方面就必须要顾虑到多方面&#xff0c;这样才能保证安全性&#xff0c;今天小编就给大家讲一讲要注意什么呢&#xff1f; 1、带宽:带宽是业务稳定性的直接因素&#xff0c;只有带宽充足&#xff0c;这样…

kafka发送大消息

1 kafka消息压缩 kafka关于消息压缩的定义&#xff08;来源于官网&#xff09;&#xff1a; 此为 Kafka 中端到端的块压缩功能。如果启用&#xff0c;数据将由 producer 压缩&#xff0c;以压缩格式写入服务器&#xff0c;并由 consumer 解压缩。压缩将提高 consumer 的吞吐量…

使用 pytest.ini 文件控制输出 log 日志

一、前置说明 pytest.ini 文件中可以配置参数来控制 pytest 的运行行为,其存放路径要求与 conftest.py 一样。 项目根目录project_root/ ├── pytest.ini ├── tests/ │ └── test_demo.py以test开头的测试子目录project_root/ ├── tests/ │ ├── pytest.in…