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

2、gRPC JSON轉碼
.NET7 為 ASP.NET Core gRPC 提供了擴展,以使 gRPC 服務能夠作為 RESTful Web 服務公開。 您現在可以通過 HTTP 調用 gRPC 方法而無需任何重復。
gRPC JSON 轉碼支持:
在此擴展中,HTTP 動詞通過使用 protobuf 注釋的概念映射到 gRPC 服務,擴展在 ASP.NET Core 應用程序中運行,然后將 JSON 反序列化為 protobuf 消息并直接調用 gRPC 服務,而不必編寫自己的 gRPC 客戶端應用程序。
我們將在下一節中研究如何實現它。
3、開放API規范
現在有一個 Open API 規范,用于 .NET 7 中的 gRPC JSON 轉碼,使用以下 Nuget 包:
?https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.Swagger?
4、 Azure 應用服務支持
最后但同樣重要的是,Azure 應用服務現在完全支持 gRPC。 這是在 .NET 中使用 gRPC 構建和部署高性能服務的一大進步。
現在我們已經完成了討論,讓我們實現 gRPC 并看看新功能是什么樣的。

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

確保選擇了 .NET7。


這將分別在 protos 和服務文件夾中的 GreeterService 中創建一個隨時可用的 gRPC 應用程序。
這是一個用作契約的 greeting.proto 文件,定義了客戶端將接收的消息和服務。
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;
}
契約可以被認為是接口,這些接口的實現將由服務定義,在我們的例子中是 GreeterService.cs——這個文件將描述契約的實現。
GreeterService 類是一個標準的 C# 類,它向響應返回 hello。 protobuf 的實際實現是通過代碼生成實現的,并使用 GreeterBase 抽象出來。 如果您想確切地知道引擎下發生了什么,您可以轉到 GreeterBase,您會在那里找到所有底層細節。
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 的一項不錯的功能,它允許您生成服務器端和客戶端 gRPC 代碼。 通過設置代碼生成設置,可以更改 .CS 項目文件中代碼生成過程的行為(例如從服務器到客戶端)。
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
讓我們啟動 Kestral 并在打開應用程序后在瀏覽器中瀏覽 gRPC 端點。

我們無法通過網絡訪問我們的 gRPC 服務,因為它需要使用 gRPC 客戶端。 但是,我們不需要使用 gRPC 客戶端,而是使用流行的測試工具 Postman 對其進行測試。 它最近在其功能中添加了對 gRPC 請求的支持。
第一步是打開 Postman 并創建一個新的 gRPC 請求。

請在下面的框中輸入服務器地址(您的應用程序運行的地址)。 例如,https://localhost:7211。

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

我們可以看到我們的 greet.greeter 服務和它的 SayHello 方法。
可以通過單擊帶有 JSON 正文(將由 Postman 轉換為 protobuf)的 Invoke 按鈕來調用此端點。

在 49 毫秒內得到了服務器響應。
將您的 gRPC 服務轉變為 REST
本節將實現 gRPC JSON 轉碼以通過 HTTP 訪問 gRPC。
- 將以下 nuget 包添加到您的項目中:
Install-Package Microsoft.AspNetCore.Grpc.JsonTranscoding -Version 7.0.0
2. 導航到 Program.cs 并添加 JSONTranscoding 服務:
builder.Services.AddGrpc().AddJsonTranscoding();
下一步,我們將向您的項目添加兩個配置文件。

添加這些文件后,我們需要修改 greet.proto 并添加 import “google/api/annotations.proto” 以便我們可以注解服務方法。
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply)
{
option (google.api.http) =
{
get: "/v1/greeter/{name}"
}
};
基本上,我們向我們的 RPC 方法添加了一個路由,以便它可以作為 REST 方法被調用。 讓我們再次運行應用程序并使用瀏覽器執行端點。

就是這樣! 該 API 現在作為基于 REST 的 API 工作,但它仍然可以作為 gRPC 接口使用。 來自 Postman 的 gRPC 響應如下所示。

添加開放 API 規范
本節的目的是解釋我們如何使用 gRPC.Swagger 向我們的應用程序添加開放 API 規范。
- 安裝以下 nuget 包:
Install-Package Microsoft.AspNetCore.Grpc.Swagger -Version 0.3.0
2.注冊Swagger服務和中間件,如下
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 應該如下所示:
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();
}
}
}
啟動應用程序后調用 Swagger 端點:
https://localhost:7211/swagger/index.html。

您可以像調用任何 Restful API 一樣嘗試調用端點。

在本節之后,我們將演示如何使用 gRPC 服務器流將 5M 記錄(大約 600MB 數據)使用流式傳輸到客戶端。
gRPC 服務器流
在服務器流中,gRPC 客戶端發送請求并獲取響應流。 客戶端讀取這些響應,直到所有消息都已傳遞。 gRPC 確保消息排序。
使用此示例 CSV 文件作為示例。
該 CSV 文件包含大約 500 萬條銷售記錄,因此不可能在一個調用中將它們全部發送出去。
此外,傳統的基于 REST 的分頁涉及多個客戶端請求,并且需要在客戶端和服務器之間來回通信。
gRPC Server streaming 是解決這個問題的絕佳方案。
- 客戶端將簡單地調用服務方法。
- CSV 文件將逐行讀取,轉換為原型模型,然后使用 StreamReader 發送回客戶端。
- 響應流將被發送到客戶端。
我們將從定義一個原型文件開始:

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 關鍵字,我們可以指定 SalesDataModel 將作為流傳遞。
我們的下一步是通過以下方式添加一個新服務——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()));
}
}
}
}
}
}
此服務實現 SalesServiceBase 類,該類由 .NET7 工具使用 proto 文件自動生成。
它只是重寫 GetSalesData 以逐行從文件中讀取數據并將其作為流返回。
await responseStream.WriteAsync(_model);
讓我們構建項目并運行應用程序。

應用程序按預期運行。 要從服務器獲取訂單流,我們需要創建一個單獨的 RPC 客戶端,這將在下一節中介紹。
使用 .NET7 創建 gRPC 客戶端
讓我們在您的解決方案中創建一個新的控制臺應用程序,并向其中添加以下包
<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 文件復制到那里。
- 為了為客戶端生成 gRPC 類,您需要修改 .csproj 文件。
<ItemGroup>
<Protobuf Include="Protos\sales.proto" GrpcServices="Client" />
</ItemGroup>
3. 保存并構建項目(以便生成客戶端代碼)
4. 第一步是打開 Program.cs 并為您的 gRPC 服務創建一個通道。
var channel = GrpcChannel.ForAddress("https://localhost:7211");
5. 創建一個新的SalesService對象(使用gRPC工具創建)如下:
var client = new SalesService.SalesServiceClient(channel);
6.服務方法應按如下方式調用:
using var call = client.GetSalesData(new Request { Filters = "" });
7. 我們的代碼只是調用服務器上的 ReadAllAsync 來檢索流,然后在收到流后立即在控制臺上打印輸出。
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++;
}
這就是完整實現:
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();
}
}
}
正如您在上面的示例中看到的,服務方法調用是在deadline的幫助下完成的。 您可以使用deadline指定通話的持續時間,這樣您就可以指定通話應該持續多長時間。
using var call = client.GetSalesData(new Request { Filters = "" }
, deadline: DateTime.UtcNow.AddMinutes(10)
);
客戶端現在允許您查看來自 gRPC 服務的傳入消息。

結論:
本文的目的是提供有關已添加到 gRPC .NET 7 框架的性能增強的信息,包括 gRPC JSON 轉碼功能、OpenAPI 規范和服務器反射功能,以及新的性能改進。 本文還解釋了如何使用 gRPC 服務器流式處理來創建能夠立即處理和交付數百萬條記錄的高性能服務。