
.NET 7 正式推出標(biāo)準(zhǔn)期限支持,支持期限為 18 個(gè)月。 其中包括許多令人興奮的新功能,包括 Web API、gRPC、ASP.NET 和 C#11 的性能升級(jí)。
本文涵蓋以下主題:
- .NET 7 中的性能改進(jìn)。
- gRPC JSON 轉(zhuǎn)碼。
- 在 .NET 7 中創(chuàng)建 gRPC 服務(wù)。
- 在 Postman 使用 gRPC 服務(wù)。
- 使用服務(wù)器反射和 Postman
- 添加 Swagger 規(guī)范。
除了討論 .NET 7 中 gRPC 的新特性,我們還將實(shí)現(xiàn)一個(gè)能夠在一分鐘內(nèi)流式傳輸 500 萬(wàn)條記錄的真實(shí)微服務(wù)。
這是一個(gè)快速回顧:
- gRPC 是由 CNCF 開發(fā)的流行的開源 RPC 框架。
- 作為契約優(yōu)先、獨(dú)立于語(yǔ)言的框架,客戶端和服務(wù)器必須就消息的內(nèi)容和傳遞方式達(dá)成一致,契約在 .proto 文件中定義,然后使用 .NET7 的工具生成代碼。
- 在單個(gè) tcp 連接上,HTTP/2 支持多路復(fù)用,您可以同時(shí)發(fā)送多個(gè)請(qǐng)求。
- 此外,gRPC 支持?jǐn)?shù)據(jù)流,其中服務(wù)器可以同時(shí)向客戶端發(fā)送多個(gè)響應(yīng),反之亦然。
.NET 7 中有哪些新功能?
1、性能改進(jìn)
為了讓 gRPC 支持多路復(fù)用,HTTP/2 是必需的。 但是,Kestrel 的 HTTP/2 實(shí)現(xiàn)存在一個(gè)已知問(wèn)題,該問(wèn)題會(huì)在連接繁忙時(shí)通過(guò) HTTP/2 寫入響應(yīng)時(shí)出現(xiàn)瓶頸。 當(dāng)您在同一個(gè) TCP 連接上同時(shí)運(yùn)行多個(gè)請(qǐng)求,但一次只有一個(gè)線程能夠?qū)懭朐撨B接時(shí),就會(huì)發(fā)生這種情況。 這是通過(guò) .NET 6 中的線程鎖完成的,這會(huì)導(dǎo)致鎖爭(zhēng)用。
NET 7 使用一種巧妙的方法來(lái)解決此瓶頸,即實(shí)現(xiàn)一個(gè)隊(duì)列,該隊(duì)列會(huì)在寫入完成時(shí)通知所有其他線程,讓它們等待寫入完成。 因此,性能大大提升,CPU資源得到更好的利用——不再需要爭(zhēng)鎖。
.NET gRPC 團(tuán)隊(duì)的基準(zhǔn)測(cè)試表明服務(wù)器流式處理提高了 800%。
- .NET 6–0.5M RPS
- .NET 7–4.5M RPS
HTTP/2 上傳速度
通過(guò)增加緩沖區(qū)大小可將延遲減少 600%。 與 .NET 6 相比,.NET 7 將上傳 100MB 文件的時(shí)間從 26.9 秒減少到 4.3 秒。
.NET 7 gRPC 的性能現(xiàn)在超過(guò)了 Rust、Go 和 C++ 等流行框架。

2、gRPC JSON轉(zhuǎn)碼
.NET7 為 ASP.NET Core gRPC 提供了擴(kuò)展,以使 gRPC 服務(wù)能夠作為 RESTful Web 服務(wù)公開。 您現(xiàn)在可以通過(guò) HTTP 調(diào)用 gRPC 方法而無(wú)需任何重復(fù)。
gRPC JSON 轉(zhuǎn)碼支持:
- HTTP 動(dòng)詞
- URL參數(shù)綁定
- JSON請(qǐng)求/響應(yīng)
在此擴(kuò)展中,HTTP 動(dòng)詞通過(guò)使用 protobuf 注釋的概念映射到 gRPC 服務(wù),擴(kuò)展在 ASP.NET Core 應(yīng)用程序中運(yùn)行,然后將 JSON 反序列化為 protobuf 消息并直接調(diào)用 gRPC 服務(wù),而不必編寫自己的 gRPC 客戶端應(yīng)用程序。
我們將在下一節(jié)中研究如何實(shí)現(xiàn)它。
3、開放API規(guī)范
現(xiàn)在有一個(gè) Open API 規(guī)范,用于 .NET 7 中的 gRPC JSON 轉(zhuǎn)碼,使用以下 Nuget 包:
?https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.Swagger?
4、 Azure 應(yīng)用服務(wù)支持
最后但同樣重要的是,Azure 應(yīng)用服務(wù)現(xiàn)在完全支持 gRPC。 這是在 .NET 中使用 gRPC 構(gòu)建和部署高性能服務(wù)的一大進(jìn)步。
現(xiàn)在我們已經(jīng)完成了討論,讓我們實(shí)現(xiàn) gRPC 并看看新功能是什么樣的。

先決條件:
- 下載并安裝 .NET 7 SDK
- Visual Studio 2022 17.4+
我們需要做的第一件事是啟動(dòng) Visual Studio 并創(chuàng)建一個(gè)新項(xiàng)目。 選擇“ASP.NET Core gRPC 服務(wù)”,這將創(chuàng)建一個(gè)示例 hello world gRPC 服務(wù)。

確保選擇了 .NET7。


這將分別在 protos 和服務(wù)文件夾中的 GreeterService 中創(chuàng)建一個(gè)隨時(shí)可用的 gRPC 應(yīng)用程序。
這是一個(gè)用作契約的 greeting.proto 文件,定義了客戶端將接收的消息和服務(wù)。
syntax = "proto3";
option csharp_namespace = "gRPCUsingNET7Demo";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}
契約可以被認(rèn)為是接口,這些接口的實(shí)現(xiàn)將由服務(wù)定義,在我們的例子中是 GreeterService.cs——這個(gè)文件將描述契約的實(shí)現(xiàn)。
GreeterService 類是一個(gè)標(biāo)準(zhǔn)的 C# 類,它向響應(yīng)返回 hello。 protobuf 的實(shí)際實(shí)現(xiàn)是通過(guò)代碼生成實(shí)現(xiàn)的,并使用 GreeterBase 抽象出來(lái)。 如果您想確切地知道引擎下發(fā)生了什么,您可以轉(zhuǎn)到 GreeterBase,您會(huì)在那里找到所有底層細(xì)節(jié)。
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger){
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
代碼生成是 .NET 7 的一項(xiàng)不錯(cuò)的功能,它允許您生成服務(wù)器端和客戶端 gRPC 代碼。 通過(guò)設(shè)置代碼生成設(shè)置,可以更改 .CS 項(xiàng)目文件中代碼生成過(guò)程的行為(例如從服務(wù)器到客戶端)。
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
讓我們啟動(dòng) Kestral 并在打開應(yīng)用程序后在瀏覽器中瀏覽 gRPC 端點(diǎn)。

我們無(wú)法通過(guò)網(wǎng)絡(luò)訪問(wèn)我們的 gRPC 服務(wù),因?yàn)樗枰褂?gRPC 客戶端。 但是,我們不需要使用 gRPC 客戶端,而是使用流行的測(cè)試工具 Postman 對(duì)其進(jìn)行測(cè)試。 它最近在其功能中添加了對(duì) gRPC 請(qǐng)求的支持。
第一步是打開 Postman 并創(chuàng)建一個(gè)新的 gRPC 請(qǐng)求。

請(qǐng)?jiān)谙旅娴目蛑休斎敕?wù)器地址(您的應(yīng)用程序運(yùn)行的地址)。 例如,https://localhost:7211。

Postman目前不了解我們的服務(wù)如何運(yùn)作,因此我們有幾個(gè)選擇。 一種是導(dǎo)入 .proto 文件或使用稱為“服務(wù)器反射”的東西。 它可以被認(rèn)為是 gRPC 調(diào)用的 OpenAPI 規(guī)范。
在 gRPC 服務(wù)中啟用服務(wù)器反射。
按照以下步驟啟用服務(wù)器反射非常簡(jiǎn)單。
下載并安裝以下 nuget 包:
Install-Package Grpc.AspNetCore.Server.Reflection -Version 2.49.0
2、在Program.cs文件中,需要注冊(cè)如下服務(wù),并將該服務(wù)映射到我們的http管道中,如下:
builder.Services.AddGrpcReflection();
app.MapGrpcReflectionService();
現(xiàn)在我們已經(jīng)完成了所有這些前置需求,讓我們回到 Postman,再次運(yùn)行應(yīng)用程序。

我們可以看到我們的 greet.greeter 服務(wù)和它的 SayHello 方法。
可以通過(guò)單擊帶有 JSON 正文(將由 Postman 轉(zhuǎn)換為 protobuf)的 Invoke 按鈕來(lái)調(diào)用此端點(diǎn)。

在 49 毫秒內(nèi)得到了服務(wù)器響應(yīng)。
將您的 gRPC 服務(wù)轉(zhuǎn)變?yōu)?REST
本節(jié)將實(shí)現(xiàn) gRPC JSON 轉(zhuǎn)碼以通過(guò) HTTP 訪問(wèn) gRPC。
- 將以下 nuget 包添加到您的項(xiàng)目中:
Install-Package Microsoft.AspNetCore.Grpc.JsonTranscoding -Version 7.0.0
2. 導(dǎo)航到 Program.cs 并添加 JSONTranscoding 服務(wù):
builder.Services.AddGrpc().AddJsonTranscoding();
下一步,我們將向您的項(xiàng)目添加兩個(gè)配置文件。

添加這些文件后,我們需要修改 greet.proto 并添加 import “google/api/annotations.proto” 以便我們可以注解服務(wù)方法。
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply)
{
option (google.api.http) =
{
get: "/v1/greeter/{name}"
}
};
基本上,我們向我們的 RPC 方法添加了一個(gè)路由,以便它可以作為 REST 方法被調(diào)用。 讓我們?cè)俅芜\(yùn)行應(yīng)用程序并使用瀏覽器執(zhí)行端點(diǎn)。

就是這樣! 該 API 現(xiàn)在作為基于 REST 的 API 工作,但它仍然可以作為 gRPC 接口使用。 來(lái)自 Postman 的 gRPC 響應(yīng)如下所示。

添加開放 API 規(guī)范
本節(jié)的目的是解釋我們?nèi)绾问褂?gRPC.Swagger 向我們的應(yīng)用程序添加開放 API 規(guī)范。
- 安裝以下 nuget 包:
Install-Package Microsoft.AspNetCore.Grpc.Swagger -Version 0.3.0
2.注冊(cè)Swagger服務(wù)和中間件,如下
builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen( c=>
{
c.SwaggerDoc("v1",
new Microsoft.OpenApi.Models.OpenApiInfo { Title = "gRPC using .NET 7 Demo", Version = "v1" } );
});
最后,您的 program.cs 應(yīng)該如下所示:
using gRPCUsingNET7Demo.Services;
namespace gRPCUsingNET7Demo
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
// Add services to the container.
builder.Services.AddGrpc().AddJsonTranscoding();
builder.Services.AddGrpcReflection();
builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen( c=>
{
c.SwaggerDoc("v1",
new Microsoft.OpenApi.Models.OpenApiInfo { Title = "gRPC using .NET 7 Demo", Version = "v1" }
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "gRPC using .NET7 Demo");
}
);
// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcReflectionService();
app.MapGet("/", () "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
app.Run();
}
}
}
啟動(dòng)應(yīng)用程序后調(diào)用 Swagger 端點(diǎn):
https://localhost:7211/swagger/index.html。

您可以像調(diào)用任何 Restful API 一樣嘗試調(diào)用端點(diǎn)。

在本節(jié)之后,我們將演示如何使用 gRPC 服務(wù)器流將 5M 記錄(大約 600MB 數(shù)據(jù))使用流式傳輸?shù)娇蛻舳恕?/span>
gRPC 服務(wù)器流
在服務(wù)器流中,gRPC 客戶端發(fā)送請(qǐng)求并獲取響應(yīng)流。 客戶端讀取這些響應(yīng),直到所有消息都已傳遞。 gRPC 確保消息排序。
使用此示例 CSV 文件作為示例。
該 CSV 文件包含大約 500 萬(wàn)條銷售記錄,因此不可能在一個(gè)調(diào)用中將它們?nèi)堪l(fā)送出去。
此外,傳統(tǒng)的基于 REST 的分頁(yè)涉及多個(gè)客戶端請(qǐng)求,并且需要在客戶端和服務(wù)器之間來(lái)回通信。
gRPC Server streaming 是解決這個(gè)問(wèn)題的絕佳方案。
- 客戶端將簡(jiǎn)單地調(diào)用服務(wù)方法。
- CSV 文件將逐行讀取,轉(zhuǎn)換為原型模型,然后使用 StreamReader 發(fā)送回客戶端。
- 響應(yīng)流將被發(fā)送到客戶端。
我們將從定義一個(gè)原型文件開始:

Protos-> sales.proto
syntax = "proto3";
import "google/protobuf/timestamp.proto";
csharp_namespace = "gRPCUsingNET7Demo";
package sales;
service SalesService {
rpc GetSalesData(Request) returns (stream SalesDataModel) {}
}
message Request{
string filters=1;
}
message SalesDataModel {
int32 OrderID = 1;
string Region = 2;
string Country = 3;
string ItemType=4;
google.protobuf.Timestamp OrderDate=5;
google.protobuf.Timestamp ShipDate=6;
int32 UnitsSold=7;
float UnitCost=8;
float UnitPrice=9;
int32 TotalRevenue=10;
int32 TotalCost=11;
int32 TotalProfit=12;
}
使用 stream 關(guān)鍵字,我們可以指定 SalesDataModel 將作為流傳遞。
我們的下一步是通過(guò)以下方式添加一個(gè)新服務(wù)——SalesDataService.cs:
using Grpc.Core;
using gRPCUsingNET7Demo;
namespace gRPCUsingNET7Demo.Services
{
public class SalesDataService : SalesService.SalesServiceBase
{
public override async Task
GetSalesData(Request request,
IServerStreamWriter<SalesDataModel> responseStream, ServerCallContext context){
using (var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data", "sales_records.csv")))
{
string line; bool isFirstLine = true;
while ((line = reader.ReadLine()) != null)
{
var pieces = line.Split(',');
var _model = new SalesDataModel();
try
{
if (isFirstLine)
{
isFirstLine = false;
continue;
}
_model.Region = pieces[0];
_model.Country = pieces[1];
_model.OrderID = int.TryParse(pieces[6], out int _orderID) ? _orderID : 0;
_model.UnitPrice = float.TryParse(pieces[9], out float _unitPrice) ? _unitPrice : 0;
_model.ShipDate = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime
((DateTime.TryParse(pieces[7], out DateTime _dateShip) ? _dateShip : DateTime.MinValue).ToUniversalTime());
_model.UnitsSold = int.TryParse(pieces[8], out int _unitsSold) ? _unitsSold : 0;
_model.UnitCost = float.TryParse(pieces[10], out float _unitCost) ? _unitCost : 0;
_model.TotalRevenue = int.TryParse(pieces[11], out int _totalRevenue) ? _totalRevenue : 0;
_model.TotalCost = int.TryParse(pieces[13], out int _totalCost) ? _totalCost : 0;
await responseStream.WriteAsync(_model);
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.Internal, ex.ToString()));
}
}
}
}
}
}
此服務(wù)實(shí)現(xiàn) SalesServiceBase 類,該類由 .NET7 工具使用 proto 文件自動(dòng)生成。
它只是重寫 GetSalesData 以逐行從文件中讀取數(shù)據(jù)并將其作為流返回。
await responseStream.WriteAsync(_model);
讓我們構(gòu)建項(xiàng)目并運(yùn)行應(yīng)用程序。

應(yīng)用程序按預(yù)期運(yùn)行。 要從服務(wù)器獲取訂單流,我們需要?jiǎng)?chuàng)建一個(gè)單獨(dú)的 RPC 客戶端,這將在下一節(jié)中介紹。
使用 .NET7 創(chuàng)建 gRPC 客戶端
讓我們?cè)谀慕鉀Q方案中創(chuàng)建一個(gè)新的控制臺(tái)應(yīng)用程序,并向其中添加以下包
<PackageReference Include="Google.Protobuf" Version="3.21.9" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.49.0" />
<PackageReference Include="Grpc.Tools" Version="2.40.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- 確保添加了 Protos 文件夾并將 sales.proto 文件復(fù)制到那里。
- 為了為客戶端生成 gRPC 類,您需要修改 .csproj 文件。
<ItemGroup>
<Protobuf Include="Protos\sales.proto" GrpcServices="Client" />
</ItemGroup>
3. 保存并構(gòu)建項(xiàng)目(以便生成客戶端代碼)
4. 第一步是打開 Program.cs 并為您的 gRPC 服務(wù)創(chuàng)建一個(gè)通道。
var channel = GrpcChannel.ForAddress("https://localhost:7211");
5. 創(chuàng)建一個(gè)新的SalesService對(duì)象(使用gRPC工具創(chuàng)建)如下:
var client = new SalesService.SalesServiceClient(channel);
6.服務(wù)方法應(yīng)按如下方式調(diào)用:
using var call = client.GetSalesData(new Request { Filters = "" });
7. 我們的代碼只是調(diào)用服務(wù)器上的 ReadAllAsync 來(lái)檢索流,然后在收到流后立即在控制臺(tái)上打印輸出。
await foreach (var each in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice,each.ShipDate));
Count++;
}
這就是完整實(shí)現(xiàn):
using Grpc.Core;
using Grpc.Net.Client;
using gRPCUsingNET7Demo;
namespace gRPCClient
{
internal class Program
{
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:7211");
int Count = 0;
var watch = System.Diagnostics.Stopwatch.StartNew();
try
{
var client = new SalesService.SalesServiceClient(channel);
using var call = client.GetSalesData(new Request { Filters = "" }
, deadline: DateTime.UtcNow.AddMinutes(10)
);
await foreach (var each in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice, each.ShipDate));
Count++;
}
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("Service timeout.");
}
watch.Stop();
Console.WriteLine($"Stream ended: Total Records:{Count.ToString()} in {watch.Elapsed.TotalMinutes} minutes and {watch.Elapsed.TotalSeconds});
Console.Read();
}
}
}
正如您在上面的示例中看到的,服務(wù)方法調(diào)用是在deadline的幫助下完成的。 您可以使用deadline指定通話的持續(xù)時(shí)間,這樣您就可以指定通話應(yīng)該持續(xù)多長(zhǎng)時(shí)間。
using var call = client.GetSalesData(new Request { Filters = "" }
, deadline: DateTime.UtcNow.AddMinutes(10)
);
客戶端現(xiàn)在允許您查看來(lái)自 gRPC 服務(wù)的傳入消息。

結(jié)論:
本文的目的是提供有關(guān)已添加到 gRPC .NET 7 框架的性能增強(qiáng)的信息,包括 gRPC JSON 轉(zhuǎn)碼功能、OpenAPI 規(guī)范和服務(wù)器反射功能,以及新的性能改進(jìn)。 本文還解釋了如何使用 gRPC 服務(wù)器流式處理來(lái)創(chuàng)建能夠立即處理和交付數(shù)百萬(wàn)條記錄的高性能服務(wù)。