.NET 6 攻略大全(二)

点击上方蓝字

关注我们

(本文阅读时间:15分钟)

接上篇内容,本篇文章将介绍:Arm64、容器、支持 OpenTelemetry 指标、Windows Forms 的相关攻略。 

bcfcb2c04e950d2f3381e8d31eaf356f.png

Arm64

54f7790485fa967d3a88fff6fd3a2bac.png

这些天来,对于笔记本电脑、云硬件和其他设备来说,Arm64 令人兴奋不已。我们对 .NET 团队感到同样兴奋,并正在尽最大努力跟上这一行业趋势。我们直接与 Arm Holdings、Apple 和 Microsoft 的工程师合作,以确保我们的实施是正确和优化的,并且我们的计划保持一致。这些密切的合作伙伴关系对我们帮助很大。

  • 特别感谢 Apple 在 M1 芯片发布之前向我们的团队发送了一蒲式耳 Arm64 开发套件供我们使用,并提供了重要的技术支持。

  • 特别感谢 Arm Holdings,他们的工程师对我们的 Arm64 更改进行了代码审查,并进行了性能改进。

在此之前,我们通过 .NET Core 3.0 和 Arm32 添加了对 Arm64 的初始支持。该团队在最近的几个版本中都对 Arm64 进行了重大投资,并且在可预见的未来这将继续下去。在 .NET 6 中,我们主要关注在 macOS 和 Windows Arm64 操作系统上支持新的 Apple Silicon 芯片和x64 仿真场景。

您可以在 macOS 11+ 和 Windows 11+ Arm64 操作系统上安装 Arm64 和 x64 版本的 .NET。我们必须做出多种设计选择和产品更改以确保其奏效。

我们的策略是“亲原生架构”。我们建议您始终使用与原生架构相匹配的 SDK,即 macOS 和 Windows Arm64 上的 Arm64 SDK。SDK 是大量的软件。在 Arm64 芯片上本地运行的性能将比仿真高得多。我们更新了 CLI 以简化操作。我们永远不会专注于优化模拟 x64。

默认情况下,如果您dotnet run是带有 Arm64 SDK 的 .NET 6 应用程序,它将作为 Arm64 运行。您可以使用参数轻松切换到以 x64 运行,例如-adotnet run -a x64. 相同的论点适用于其他 CLI 动词。有关更多信息,请参阅适用于 macOS 和 Windows Arm64 的 .NET 6 RC2 更新。

我想确保涵盖其中的一个微妙之处。当您使用-a x64时,SDK 仍以 Arm64 方式原生运行。.NET SDK 体系结构中存在进程边界的固定点。在大多数情况下,一个进程必须全是 Arm64 或全是 x64。我正在简化一点,但 .NET CLI 会等待 SDK 架构中的最后一个进程创建,然后将其作为您请求的芯片架构(如 x64)启动。这就是您的代码运行的过程。这样,作为开发人员,您可以获得 Arm64 的好处,但您的代码可以在它需要的过程中运行。这仅在您需要将某些代码作为 x64 运行时才相关。如果你不这样做,那么你可以一直以 Arm64 的方式运行所有东西,这很棒。

Arm64 支持

对于 macOS 和 Windows Arm64,以下是您需要了解的要点:

  • 支持并推荐 .NET 6 Arm64 和 x64 SDK。

  • 支持所有支持的 Arm64 和 x64 运行时。

  • .NET Core 3.1 和 .NET 5 SDK 可以工作,但提供的功能较少,并且在某些情况下不受完全支持。

  • dotnet test 尚未与 x64 仿真一起正常工作。我们正在努力。dotnet test 将作为 6.0.200 版本的一部分进行改进,并且可能更早。

有关更多完整信息,请参阅.NET 对 macOS 和 Windows Arm64的支持。

此讨论中缺少 Linux。它不像 macOS 和 Windows 那样支持 x64 仿真。因此,这些新的 CLI 特性和支持方法并不直接适用于 Linux,Linux 也不需要它们。

视窗 Arm64

我们有一个简单的工具来演示.NET 运行的环境。

C:Usersrich>dotnet tool install -g dotnet-runtimeinfo
You can invoke the tool using the following command: dotnet-runtimeinfo
Tool 'dotnet-runtimeinfo' (version '1.0.5') was successfully installed.


C:Usersrich>dotnet runtimeinfo
         42
         42              ,d                             ,d
         42              42                             42
 ,adPPYb,42  ,adPPYba, MM42MMM 8b,dPPYba,   ,adPPYba, MM42MMM
a8"    `Y42 a8"     "8a  42    42P'   `"8a a8P_____42   42
8b       42 8b       d8  42    42       42 8PP"""""""   42
"8a,   ,d42 "8a,   ,a8"  42,   42       42 "8b,   ,aa   42,
 `"8bbdP"Y8  `"YbbdP"'   "Y428 42       42  `"Ybbd8"'   "Y428

**.NET information

Version: 6.0.0

FrameworkDescription: .NET 6.0.0-rtm.21522.10

Libraries version: 6.0.0-rtm.21522.10

Libraries hash: 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6

**Environment information

ProcessorCount: 8

OSArchitecture: Arm64

OSDescription: Microsoft Windows 10.0.22494

OSVersion: Microsoft Windows NT 10.0.22494.0

如您所见,该工具在 Windows Arm64 上本机运行。我将向您展示 ASP.NET Core 的样子。

9128451493694d815491feeff9ca81cd.png

macOS Arm64

您可以看到在 macOS Arm64 上的体验是相似的,并且还展示了架构目标。

rich@MacBook-Air app % dotnet --version
6.0.100
rich@MacBook-Air app % dotnet --info | grep RID
 RID:         osx-arm64
rich@MacBook-Air app % cat Program.cs 
using System.Runtime.InteropServices;
using static System.Console;
WriteLine($"Hello, {RuntimeInformation.OSArchitecture} from {RuntimeInformation.FrameworkDescription}!");
rich@MacBook-Air app % dotnet run
Hello, Arm64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app % dotnet run -a x64
Hello, X64 from .NET 6.0.0-rtm.21522.10!
rich@MacBook-Air app %

这张图片展示了 Arm64 执行是 Arm64 SDK 的默认设置,以及使用-a参数在目标 Arm64 和 x64 之间切换是多么容易。完全相同的体验适用于 Windows Arm64。

1fc6f7e000d0c8061613a1da432069a0.png

此图像演示了相同的内容,但使用的是 ASP.NET Core。我正在使用与您在上图中看到的相同的 .NET 6 Arm64 SDK。

Arm64 上的 Docker

Docker 支持在本机架构和仿真中运行的容器,本机架构是默认的。这看起来很明显,但当大多数 Docker Hub 目录都是面向 x64 时,这可能会让人感到困惑。您可以使用--platform linux/amd64来请求 x64 图像。

我们仅支持在 Arm64 操作系统上运行 Linux Arm64 .NET 容器映像。这是因为我们从不支持在QEMU中运行 .NET ,这是 Docker 用于架构模拟的。看来这可能是由于 QEMU 的限制。

‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

0be3393a29100620d121ce7d18fb117e.png

此图像演示了我们维护的控制台示例:mcr.microsoft.com/dotnet/samples. 这是一个有趣的示例,因为它包含一些基本逻辑,用于打印您可以使用的 CPU 和内存限制信息。我展示的图像设置了 CPU 和内存限制。

自己试试吧:docker run --rm mcr.microsoft.com/dotnet/samples

Arm64 性能

Apple Silicon 和 x64 仿真支持项目非常重要,但是,我们也普遍提高了 Arm64 性能。

63b769924a4772e3dd841ecee1a5f67a.png

此图像演示了将堆栈帧的内容清零的改进,这是一种常见的操作。绿线是新行为,而橙色线是另一个(不太有益的)实验,两者都相对于基线有所改善,由蓝线表示。对于此测试,越低越好。

fb365ed044d1284b77a6db6df0c5f82d.png

容器

4f4aa6040109abbe17493f62d512b49c.png

.NET 6 更适合容器,主要基于本文中讨论的所有改进,适用于 Arm64 和 x64。我们还进行了有助于各种场景的关键更改。使用 .NET 6 验证容器改进演示了其中一些改进正在一起测试。

Windows 容器改进和新环境变量也包含在 11 月 9 日(明天)发布的11 月 .NET Framework 4.8 容器更新中。

发布说明可在我们的 docker 存储库中找到:

  • .NET 6 容器发行说明

  • .NET Framework 4.8 2021 年 11 月容器发行说明

Windows 容器

.NET 6 增加了对 Windows 进程隔离容器的支持。如果您在 Azure Kubernetes 服务 (AKS) 中使用 Windows 容器,那么您依赖于进程隔离的容器。进程隔离容器可以被认为与 Linux 容器非常相似。Linux 容器使用cgroups,Windows 进程隔离容器使用Job Objects。Windows 还提供 Hyper-V 容器,通过更强大的虚拟化提供更大的隔离。Hyper-V 容器的 .NET 6 没有任何变化。

此更改的主要价值是现在 Environment.ProcessorCount 将使用 Windows 进程隔离容器报告正确的值。如果在 64 核机器上创建 2 核容器,Environment.ProcessorCount 将返回2. 在以前的版本中,此属性将报告机器上的处理器总数,与 Docker CLI、Kubernetes 或其他容器编排器/运行时指定的限制无关。此值被 .NET 的各个部分用于扩展目的,包括 .NET 垃圾收集器(尽管它依赖于相关的较低级别的 API)。社区库也依赖此 API 进行扩展。

我们最近在 AKS 上使用大量 pod 在生产中的 Windows 容器上与客户验证了这一新功能。他们能够以 50% 的内存(与他们的典型配置相比)成功运行,这是以前导致异常的OutOfMemoryException水平StackOverflowException。他们没有花时间找到最低内存配置,但我们猜测它明显低于他们典型内存配置的 50%。由于这一变化,他们将转向更便宜的 Azure 配置,从而节省资金。只需升级即可,这是一个不错的、轻松的胜利。

优化缩放

我们从用户那里听说,某些应用程序在 Environment.ProcessorCount 报告正确的值时无法实现最佳扩展。如果这听起来与您刚刚阅读的有关 Windows 容器的内容相反,那么它有点像。.NET 6 现在提供 DOTNET_PROCESSOR_COUNT 环境变量来手动控制 Environment.ProcessorCount 的值。在典型的用例中,应用程序可能在 64 核机器上配置为 4 核,并且在 8 或 16 核方面扩展得最好。此环境变量可用于启用该缩放。

这个模型可能看起来很奇怪,其中Environment.ProcessorCount和--cpus(通过 Docker CLI)值可能不同。默认情况下,容器运行时面向核心等价物,而不是实际核心。这意味着,当你说你想要 4 个核心时,你得到的 CPU 时间与 4 个核心相当,但你的应用程序可能(理论上)在更多的核心上运行,甚至在短时间内在 64 核机器上运行所有 64 个核心。这可能使您的应用程序能够在超过 4 个线程上更好地扩展(继续示例),并且分配更多可能是有益的。这假定线程分配基于Environment.ProcessorCount 的值。如果您选择设置更高的值,您的应用程序可能会使用更多内存。对于某些工作负载,这是一个简单的权衡。至少,这是一个您可以测试的新选项。

Linux 和 Windows 容器均支持此新功能。

Docker 还提供了一个 CPU 组功能,您的应用程序可以关联到特定的内核。在这种情况下不建议使用此功能,因为应用程序可以访问的内核数量是具体定义的。我们还看到了将它与 Hyper-V 容器一起使用时的一些问题,并且它并不是真正适用于那种隔离模式。

Debian 11“bullseye”

我们密切关注 Linux 发行版的生命周期和发布计划,并尝试代表您做出最佳选择。Debian 是我们用于默认 Linux 映像的 Linux 发行版。如果您6.0从我们的一个容器存储库中提取标签,您将提取一个 Debian 映像(假设您使用的是 Linux 容器)。对于每个新的 .NET 版本,我们都会考虑是否应该采用新的 Debian 版本。

作为一项政策,我们不会为了方便标签而更改 Debian 版本,例如6.0, mid-release。如果我们这样做了,某些应用程序肯定会崩溃。这意味着,在发布开始时选择 Debian 版本非常重要。此外,这些图像得到了很多使用,主要是因为它们是“好标签”的引用。

Debian 和 .NET 版本自然不会一起计划。当我们开始 .NET 6 时,我们看到 Debian “bullseye” 可能会在 2021 年发布。我们决定从发布开始就押注于 Bullseye。我们开始使用.NET 6 Preview 1发布基于靶心的容器映像,并决定不再回头。赌注是 .NET 6 版本会输掉与靶心版本的竞争。到 8 月 8 日,我们仍然不知道 Bullseye 什么时候发货,距离我们自己的版本发布还有三个月,即 11 月 8 日。我们不想在预览版 Linux 上发布生产 .NET 6,但我们坚持我们会输掉这场竞赛的计划很晚。

当 Debian 11 “bullseye”于 8 月 14 日发布时,我们感到非常惊喜。我们输掉了比赛,但赢得了赌注。这意味着默认情况下,.NET 6 用户从第一天开始就可以获得最佳和最新的 Debian。我们相信 Debian 11 和 .NET 6 将是许多用户的绝佳组合。抱歉,克星,我们中了靶心。

较新的发行版在其软件包提要中包含各种软件包的较新主要版本,并且通常可以更快地获得CVE 修复。这是对较新内核的补充。新发行版可以更好地为用户服务。

再往前看,我们很快就会开始计划对Ubuntu 22.04的支持。Ubuntu是另一个 Debian 系列发行版,深受 .NET 开发人员的欢迎。我们希望为新的 Ubuntu LTS 版本提供当日支持。

向 Tianon Gravi 致敬,感谢他们为社区维护 Debian 映像并在我们有问题时帮助我们。

Dotnet Monitor

dotnet monitor 是容器的重要诊断工具。它作为 sidecar 容器镜像已经有一段时间了,但处于不受支持的“实验”状态。作为 .NET 6 的一部分,我们正在发布一个基于 .NET 6 的 dotnet monitor映像,该映像在生产中得到完全支持。

dotnet monitor 已被 Azure App Service 用作其 ASP.NET Core Linux 诊断体验的实现细节。这是预期的场景之一,建立在 dotnet monitor 之上,以提供更高级别和更高价值的体验。

您现在可以拉取新图像:

docker pull mcr.microsoft.com/dotnet/monitor:6.0

dotnet monitor 使从 .NET 进程访问诊断信息(日志、跟踪、进程转储)变得更加容易。在台式机上访问所需的所有诊断信息很容易,但是,这些熟悉的技术在使用容器的生产环境中可能不起作用。dotnet monitor 提供了一种统一的方式来收集这些诊断工件,无 论是在您的桌面计算机上还是在 Kubernetes 集群中运行。收集这些诊断工件有两种不同的机制:

  • 用于临时收集工件的 HTTP API 。当您已经知道您的应用程序遇到问题并且您有兴趣收集更多信息时,您可以调用这些 API 端点。

  • 基于规则的配置触发器,用于始终在线收集工件。您可以配置规则以在满足所需条件时收集诊断数据,例如,当您持续高 CPU 时收集进程转储。

dotnet monitor 为 .NET 应用程序提供了一个通用的诊断 API,可以使用任何工具在任何地方工作。“通用 API”不是 .NET API,而是您可以调用和查询的 Web API。dotnet monitor 包括一个 ASP.NET Web 服务器,它直接与 .NET 运行时中的诊断服务器交互并公开来自诊断服务器的数据。的设计 dotnet monitor 可实现生产中的高性能监控和安全使用,以控制对特权信息的访问。dotnet monitor 通过非 Internet 可寻址的 unix domain socket 与运行时交互——跨越容器边界。该模型通信模型非常适合此用例。

结构化 JSON 日志

JSON 格式化程序现在是 aspnet.NET 6 容器映像中的默认控制台记录器。.NET 5 中的默认设置为简单的控制台格式化程序。进行此更改是为了使默认配置与依赖机器可读格式(如 JSON)的自动化工具一起使用。

图像的输出现在如下所示 aspnet:

$ docker run --rm -it -p 8000:80 mcr.microsoft.com/dotnet/samples:aspnetapp
{"EventId":60,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository","Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","State":{"Message":"Storing keys in a directory u0027/root/.aspnet/DataProtection-Keysu0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.","path":"/root/.aspnet/DataProtection-Keys","{OriginalFormat}":"Storing keys in a directory u0027{path}u0027 that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed."}}
{"EventId":35,"LogLevel":"Warning","Category":"Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager","Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","State":{"Message":"No XML encryptor configured. Key {86cafacf-ab57-434a-b09c-66a929ae4fd7} may be persisted to storage in unencrypted form.","KeyId":"86cafacf-ab57-434a-b09c-66a929ae4fd7","{OriginalFormat}":"No XML encryptor configured. Key {KeyId:B} may be persisted to storage in unencrypted form."}}
{"EventId":14,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Now listening on: http://[::]:80","State":{"Message":"Now listening on: http://[::]:80","address":"http://[::]:80","{OriginalFormat}":"Now listening on: {address}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Application started. Press Ctrlu002BC to shut down.","State":{"Message":"Application started. Press Ctrlu002BC to shut down.","{OriginalFormat}":"Application started. Press Ctrlu002BC to shut down."}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Hosting environment: Production","State":{"Message":"Hosting environment: Production","envName":"Production","{OriginalFormat}":"Hosting environment: {envName}"}}
{"EventId":0,"LogLevel":"Information","Category":"Microsoft.Hosting.Lifetime","Message":"Content root path: /app","State":{"Message":"Content root path: /app","contentRoot":"/app","{OriginalFormat}":"Content root path: {contentRoot}"}}

Logging__Console__FormatterName 可以通过设置或取消设置环境变量或通过代码更改来更改记录器格式类型(有关更多详细信息,请参阅控制台日志格式)。

更改后,您将看到如下输出(就像 .NET 5 一样):

$ docker run --rm -it -p 8000:80 -e Logging__Console__FormatterName="" mcr.microsoft.com/dotnet/samples:aspnetapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {8d4ddd1d-ccfc-4898-9fe1-3e7403bf23a0} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

注意:此更改不会影响开发人员计算机上的 .NET SDK,例如 dotnet run. 此更改特定于 aspnet 容器映像。

b39bee094cbaad9b45baf766f422cc0b.png

支持 OpenTelemetry 指标

f2dd5ce2ff8ce75bb61dcf38d6784e03.png

作为我们关注可观察性的一部分,我们一直在为最后几个 .NET 版本添加对 OpenTelemetry 的支持。在 .NET 6 中,我们添加了对 OpenTelemetry Metrics API的支持。通过添加对 OpenTelemetry 的支持,您的应用程序可以与其他 OpenTelemetry 系统无缝互操作。

System.Diagnostics.Metrics 是 OpenTelemetry Metrics API 规范的 .NET 实现。Metrics API 是专门为处理原始测量而设计的,目的是高效、同时地生成这些测量的连续摘要。

API 包括 Meter 可用于创建仪器对象的类。API 公开了四个工具类:Counter、Histogram、ObservableCounter 和 ObservableGauge 以支持不同的度量方案。此外,API 公开 MeterListener 该类以允许收听仪器记录的测量值,以用于聚合和分组目的。

OpenTelemetry .NET 实现将被扩展以使用这些新的 API,这些 API 添加了对 Metrics 可观察性场景的支持。

▌图书馆测量记录示例

Meter meter = new Meter("io.opentelemetry.contrib.mongodb", "v1.0");
    Counter<int> counter = meter.CreateCounter<int>("Requests");
    counter.Add(1);
    counter.Add(1, KeyValuePair.Create<string, object>("request", "read"));

▌听力示例

MeterListener listener = new MeterListener();
    listener.InstrumentPublished = (instrument, meterListener) =>
    {
        if (instrument.Name == "Requests" && instrument.Meter.Name == "io.opentelemetry.contrib.mongodb")
        {
            meterListener.EnableMeasurementEvents(instrument, null);
        }
    };
    listener.SetMeasurementEventCallback<int>((instrument, measurement, tags, state) =>
    {
        Console.WriteLine($"Instrument: {instrument.Name} has recorded the measurement {measurement}");
    });
    listener.Start();

1f8ac03a1fe36d26e5aed22ad4c8055b.png

Windows Forms

43e830158b1164cb9818ea9ddef6d301.png

我们继续在 Windows 窗体中进行重要改进。.NET 6 包括更好的控件可访问性、设置应用程序范围的默认字体、模板更新等的能力。

▌可访问性改进

在此版本中,我们添加了用于CheckedListBox、LinkLabel、Panel、ScrollBar和TabControlTrackBar的UIA 提供程序,它们使讲述人等工具和测试自动化能够与应用程序的元素进行交互。

▌默认字体

您现在可以使用.Application.SetDefaultFont

void Application.SetDefaultFont(Font font)

▌最小的应用程序

以下是带有 .NET 6 的最小 Windows 窗体应用程序:

class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();
        Application.Run(new Form1());
    }
}

作为 .NET 6 版本的一部分,我们一直在更新大多数模板,使其更加现代和简约,包括 Windows 窗体。我们决定让 Windows 窗体模板更传统一些,部分原因是需要将[STAThread]属性应用于应用程序入口点。然而,还有更多的戏剧而不是立即出现在眼前。

ApplicationConfiguration.Initialize()是一个源生成 API,它在后台发出以下调用:

Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.SetDefaultFont(new Font(...));
Application.SetHighDpiMode(HighDpiMode.SystemAware);

这些调用的参数可通过 csproj 或 props 文件中的 MSBuild 属性进行配置。

Visual Studio 2022 中的 Windows 窗体设计器也知道这些属性(目前它只读取默认字体),并且可以向您显示您的应用程序,就像它在运行时一样:

8dffbaec42252d2db0c3aeb6e4aa4cb0.png

▌模板更新

C# 的 Windows 窗体模板已更新,以支持新的应用程序引导、global using 指令、文件范围的命名空间和可为空的引用类型。

▌更多运行时 designers

现在您可以构建通用设计器(例如,报表设计器),因为 .NET 6 具有设计器和与设计器相关的基础架构所缺少的所有部分。有关详细信息,请参阅此博客文章。

  • 博客文章

    https://devblogs.microsoft.com/dotnet/whats-new-in-windows-forms-in-net-6-0-preview-5/#more-runtime-designers

未完待续

更多内容请继续关注明日(周日)文章

2b8d82020a1a7f7ba4d0996f79ad6955.gif

 了解更多.NET 6 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值