.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-L101
    private 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-L107
    private 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-L119
    private 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 DI

services.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=TRUE
  Response <--
  • 或者,不包装处理程序管道(添加到底部,对应于上面旧日志记录概述部分中的第 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 pipeline
  Response <--

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

在向管道添加重试处理程序的情况下(例如 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.mfbz.cn/a/270811.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

nodejs+vue+微信小程序+python+PHP基于Android的共享自习室APP系统-计算机毕业设计推荐

1.用户端&#xff1a; 一、首页&#xff1a; &#xff08;1&#xff09;店面轮播图及位置、营业时间 &#xff08;2&#xff09;预约储物柜功能&#xff1a;选择储物柜号码、确认预约 &#xff08;3&#xff09;会员功能&#xff1a;解锁VIP座位、个人积分信息&#xff08;查看…

STM32 cubeMX 光敏电阻AD转化实验

文章代码使用 HAL 库。 文章目录 前言一、光敏电阻介绍二、光敏电阻原理图解析三、ADC采样介绍1. 工作原理&#xff1a;2. ADC精度&#xff1a; 四、STM32 cubeMX配置ADC采样五、代码编写总结 前言 实验开发板&#xff1a;STM32F051K8。所需软件&#xff1a;keil5 &#xff0c;…

解决ELement-UI三级联动数据不回显

目录 一.处理数据时使用this.$set方法来动态地设置实例中的属性&#xff0c;以确保其响应式。 二.检查数据格式是否正确 三.绑定v-if 确保每次执行 四.完整代码 一.处理数据时使用this.$set方法来动态地设置实例中的属性&#xff0c;以确保其响应式。 二.检查数据格式是否正确…

TypeScript下载安装,编译运行

TypeScript是拥有类型的JavaScript超集&#xff0c;它可以编译成普通、干净、完整的JavaScript代码。 简单理解&#xff1a;TypeScript就是加强版的JavaScript。 TypeScript最终会被编译成JavaScript代码&#xff0c;那么我们必然需要对应的编译环境 环境搭建前提&#xff1a…

简单的喷淋实验(2):(1)根据土壤湿度自动控制喷淋开关;(2)根据光照强度控制风扇以及灯的开关---嵌入式实训

目录 简单的喷淋实验(2)&#xff1a; &#xff08;1&#xff09;根据土壤湿度自动控制喷淋开关&#xff1b; &#xff08;2&#xff09;根据光照强度控制风扇以及灯的开关---嵌入式实训 任务2&#xff1a; 具体过程&#xff1a; 所用的头文件&#xff1a; data_global.h …

人工智能_机器学习073_SVM支持向量机_人脸识别模型建模_预测可视化_网格搜索交叉验证最优化参数对比---人工智能工作笔记0113

接着上一节来说,可以看到我们已经找到了合适的参数,然后 我们可以看一下这里 gc.best_params_ 就可以打印出最合适的参数 然后我们把最合适串按说填入到代码中,然后进行计算,看看得分 可以看到得分,训练数据是1.0 然后测试数据得分是0.7857...对吧

nodejs+vue+微信小程序+python+PHP的热带野生动物园景点预约订票系统的设计与实现-计算机毕业设计推荐

管理员是系统的管理者&#xff0c;拥有系统的所有权限&#xff0c;通过系统设定的账号和密码登录后对系统进行管理&#xff0c;包括密码修改、用户管理。新闻公告的管理、景点管理、订单管理。管理员登录中&#xff0c;通过用户的登录名和密码到热带野生动物园景点预约订票系统…

three.js后处理(发光描边OutlinePass描边样式

效果&#xff1a; <template><div><el-container><el-main><div class"box-card-left"><div id"threejs" style"border: 1px solid red"></div><div class"box-right"></div>&…

WPS复选框里打对号,显示小太阳或粗黑圆圈的问题解决方法

问题描述 WPS是时下最流行的字处理软件之一&#xff0c;是目前唯一可以和微软office办公套件相抗衡的国产软件。然而&#xff0c;在使用WPS的过程中也会出现一些莫名其妙的错误&#xff0c;如利用WPS打开docx文件时&#xff0c;如果文件包含复选框&#xff0c;经常会出…

vue3+ts 代理的使用

简单封装request.ts import axios from "axios";// 1.创建axios对象 const serviceaxios.create();// 2.请求拦截器 service.interceptors.request.use(config>{return config; },error>{Promise.reject(error); })// 3.响应拦截器 service.interceptors…

助力打造清洁环境,基于YOLOv7开发构建公共场景下垃圾堆放垃圾桶溢出检测识别系统

公共社区环境生活垃圾基本上是我们每个人每天几乎都无法避免的一个问题&#xff0c;公共环境下垃圾投放点都会有固定的值班时间&#xff0c;但是考虑到实际扔垃圾的无规律性&#xff0c;往往会出现在无人值守的时段内垃圾堆放垃圾桶溢出等问题&#xff0c;有些容易扩散的垃圾比…

HarmonyOS应用程序包-(上)

应用程序包-(上) 1.应用程序包概述 用户应用程序泛指运行在设备的操作系统之上&#xff0c;为用户提供特定服务的程序&#xff0c;简称“应用”。一个应用所对应的软件包文件&#xff0c;称为“应用程序包”。 HarmonyOS提供了应用程序包开发、安装、查询、更新、卸载的管理…

王道考研计算机网络——数据链路层

码元和信号变化是一一对应的 低通&#xff1a;低于最高频率的可以通过 奈氏准则无噪声&#xff0c;香农定理有噪声 给出db&#xff0c;利用公式求出S/N 放到数字信道上传输就是基带信号&#xff0c;放到模拟信道上传输就是宽带信号 把基带信号调制成宽带信号之后&#xff0c;…

微信使用证书退款时候报”请求被中止: 未能创建 SSL/TLS 安全通道

解决方法&#xff1a;IIS-》应用程序池-》高级设置-》进程模块-》加载用户配置文件&#xff0c;设置为True就可以了。

冒泡排序(C语言)

void BubbleSort(int arr[], int len) {int i, j, temp;for (i 0; i < len; i){for (j len - 1; j > i; j--){if (arr[j] > arr[j 1]){temp arr[j];arr[j] arr[j 1];arr[j 1] temp;}}} } 优化&#xff1a; 设置标志位flag&#xff0c;如果发生了交换flag设置…

VD6283TX环境光传感器(1)----获取光强和色温

VD6283TX环境光传感器.1--获取光强和色温 概述视频教学样品申请完整代码下载主要特点硬件准备技术规格系统框图生成STM32CUBEMX串口配置IIC配置X-CUBE-ALS演示结果光强测试表 概述 为了充分利用VD6283TX传感器的特性和功能&#xff0c;本章节重点介绍了如何捕获光强度和相关色…

SQL server 数据库面试题及答案(实操2)

使用你的名字创建一个数据库 创建表&#xff1a; 数据库中有三张表&#xff0c;分别为student,course,SC&#xff08;即学生表&#xff0c;课程表&#xff0c;选课表&#xff09; 问题&#xff1a; --1.分别查询学生表和学生修课表中的全部数据。--2.查询成绩在70到80分之间…

Stable Diffusion模型原理

AI 图像生成引人注目&#xff0c;它能够根据文字描述生成精美图像&#xff0c;这极大地改变了人们的图像创作方式。Stable Diffusion 作为一款高性能模型&#xff0c;它生成的图像质量更高、运行速度更快、消耗的资源以及内存占用更小&#xff0c;是 AI 图像生成领域的里程碑。…

泛微OA xmlrpcServlet接口任意文件读取漏洞(CNVD-2022-43245)

CNVD-2022-43245 泛微e-cology XmlRpcServlet接口处存在任意文件读取漏洞&#xff0c;攻击者可利用漏洞获取敏感信息。 1.漏洞级别 中危 2.影响范围 e-office < 9.5 202201133.漏洞搜索 fofa 搜索 app"泛微-OA&#xff08;e-cology&#xff09;"4.漏洞复现 …

Android 手机对于Arduino蓝牙控制解决方案

1、Android系统概述 ​ Android 系统是 Google 公司基于 Linux 内核开发的移动端操作系统,适用于智能手机智能手表平板电脑等设备&#xff0c;最新的版本为 7.1。Android系统具有免费开源的优势,任何企业与个人都可以查阅公开的 API 文档&#xff0c;并在自己开发的应用中通过调…
最新文章