gRPC 基於HTTP/2,相比 HTTP API 有更好的性能,並支持雙向流式傳輸。
HTTP/2在單個 TCP 連接上多路復用多個 HTTP/2 調用。 多路復用可消除隊頭阻塞。
gRPC 支持通過流式傳輸進行實時通信,但不存在將消息廣播到注冊連接的概念。 例如,在聊天室方案中,應將新的聊天消息發送到聊天室中的所有客戶端,這要求每個 gRPC 調用將新的聊天消息單獨流式傳輸到客戶端。 SignalR 是適用於此方案的框架。 SignalR 具有持久性連接的概念,並內置對廣播消息的支持。
在 ASP.NET Core 堆棧中,默認情況下,創建的 gRPC 服務具有范圍內的生存期。
實現 gRPC
gRPC 服務可以有不同類型的方法。 服務發送和接收消息的方式取決於所定義的方法的類型。 gRPC 方法類型如下:
- 一元
- 服務器流式處理
- 客戶端流式處理
- 雙向流式處理
服務器流式處理是由服務端將多個消息流式發給客戶端
public override async Task StreamingFromServer(ExampleRequest request, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { try { while (!context.CancellationToken.IsCancellationRequested) { await responseStream.WriteAsync(new ExampleResponse { Message = DateTime.Now.ToString() }); await Task.Delay(TimeSpan.FromSeconds(1), context.CancellationToken); } } catch (OperationCanceledException) { Console.WriteLine("cancel"); } }
var cts = new CancellationTokenSource(); using var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new Example.ExampleClient(channel); using var call = client.StreamingFromServer(new ExampleRequest(), new CallOptions(cancellationToken: cts.Token)); Task.Delay(5000).ContinueWith(t => cts.Cancel()); try { await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine(response.Message); } } catch (RpcException ex) { Console.WriteLine(ex.Message); }
客戶端流式處理是由客戶端將多個消息發給服務端
public override async Task<ExampleResponse> StreamingFromClient(IAsyncStreamReader<ExampleRequest> requestStream, ServerCallContext context) { int cnt = 0; await foreach (var message in requestStream.ReadAllAsync()) { cnt++; } return new ExampleResponse { Message = cnt.ToString() }; }
var call = client.StreamingFromClient(); for (int i = 0; i < 3; i++) { await call.RequestStream.WriteAsync(new ExampleRequest()); await Task.Delay(1000); } await call.RequestStream.CompleteAsync(); var response = await call; Console.WriteLine($"Count: {response.Message}");
在雙向流式處理方法中,客戶端和服務可在任何時間互相發送消息
public override async Task StreamingBothWays(IAsyncStreamReader<ExampleRequest> requestStream, IServerStreamWriter<ExampleResponse> responseStream, ServerCallContext context) { await foreach (var message in requestStream.ReadAllAsync()) { await responseStream.WriteAsync(new ExampleResponse { Message = message.PageIndex.ToString() }); } }
當服務器已讀取請求流且客戶端已讀取響應流時,雙向調用正常完成。
var call = client.StreamingBothWays(); for (int i = 0; i < 5; i++) { Console.WriteLine("WriteAsync " + i); await call.RequestStream.WriteAsync(new ExampleRequest { PageIndex = i }); await Task.Delay(1000); } await call.RequestStream.CompleteAsync(); await Task.Run(async () => { await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine("ReadAllAsync " + response.Message); } });
配置 TLS
gRPC 客戶端傳輸層安全性 (TLS) 是在創建 gRPC 通道時配置的。 如果在調用服務時通道和服務的連接級別安全性不一致,gRPC 客戶端就會拋出錯誤。
若要將 gRPC 通道配置為使用 TLS,請確保服務器地址以 https
開頭。 例如,GrpcChannel.ForAddress("https://localhost:5001")
使用 HTTPS 協議。
在 .NET Core 3.1 或更高版本中,必須進行其他配置,才能使用 .NET 客戶端調用不安全的 gRPC 服務。
在生產環境中,必須顯式配置 TLS。 以下 appsettings.json 示例中提供了使用 TLS 進行保護的 HTTP/2 終結點:
{ "Kestrel": { "Endpoints": { "HttpsInlineCertFile": { "Url": "https://localhost:5001", "Protocols": "Http2", "Certificate": { "Path": "<path to .pfx file>", "Password": "<certificate password>" } } } } }
或者,可以在 Program.cs 中配置 Kestrel 終結點:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.ConfigureKestrel(options => { options.Listen(IPAddress.Any, 5001, listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; listenOptions.UseHttps("<path to .pfx file>", "<certificate password>"); }); }); webBuilder.UseStartup<Startup>(); });
TLS 的用途不僅限於保護通信。 當終結點支持多個協議時,TLS 應用程序層協議協商 (ALPN) 握手可用於協商客戶端與服務器之間的連接協議。 此協商確定連接是使用 HTTP/1.1 還是 HTTP/2。
客戶端性能
通道及客戶端性能和使用情況:
- 創建通道成本高昂。 重用 gRPC 調用的通道可提高性能。
- gRPC 客戶端是使用通道創建的。 gRPC 客戶端是輕型對象,無需緩存或重用。
- 可從一個通道創建多個 gRPC 客戶端(包括不同類型的客戶端)。
- 通道和從該通道創建的客戶端可由多個線程安全使用。
- 從通道創建的客戶端可同時進行多個調用。
GrpcChannel.ForAddress
不是創建 gRPC 客戶端的唯一選項。 如果要從 ASP.NET Core 應用調用 gRPC 服務,請考慮 gRPC 客戶端工廠集成。 gRPC 與 HttpClientFactory
集成是創建 gRPC 客戶端的集中式操作備選方案。
HTTP/2 連接通常會限制一個連接上同時存在的最大並發流(活動 HTTP 請求)數。 默認情況下,大多數服務器將此限制設置為 100 個並發流。
gRPC 通道使用單個 HTTP/2 連接,並且並發調用在該連接上多路復用。 當活動調用數達到連接流限制時,其他調用會在客戶端中排隊。 排隊調用等待活動調用完成后再發送。 由於此限制,具有高負載或長時間運行的流式處理 gRPC 調用的應用程序可能會因調用排隊而出現性能問題。
.NET Core 3.1 應用有幾種解決方法:
- 為具有高負載的應用的區域創建單獨的 gRPC 通道。 例如,
Logger
gRPC 服務可能具有高負載。 使用單獨的通道在應用中創建LoggerClient
。 - 使用 gRPC 通道池,例如創建 gRPC 通道列表。 每次需要 gRPC 通道時,使用
Random
從列表中選取一個通道。 使用Random
在多個連接上隨機分配調用。
提升服務器上的最大並發流限制是解決此問題的另一種方法。 在 Kestrel 中,這是用 MaxStreamsPerConnection 配置的。
不建議提升最大並發流限制。 單個 HTTP/2 連接上的流過多會帶來新的性能問題:
- 嘗試寫入連接的流之間發生線程爭用。
- 連接數據包丟失導致在 TCP 層阻止所有調用。
由於 L4 負載均衡器是在連接級別運行的,它們不太適用於 gRPC。
有兩種方法可以高效地對 gRPC 進行負載均衡:
- 客戶端負載均衡
- L7(應用程序)代理負載均衡
客戶端負載均衡的缺點是每個客戶端必須跟蹤它應該使用的可用終結點。
Lookaside 客戶端負載均衡是一種將負載均衡狀態存儲在中心位置的技術。 客戶端定期查詢中心位置以獲取在作出負載均衡決策時要使用的信息。
Grpc.Net.Client
當前不支持客戶端負載均衡。 如果 .NET 中需要客戶端負載均衡,則 Grpc.Core 是一個不錯的選擇。
L7(應用程序)代理的工作級別高於 L4(傳輸)代理。 L7 代理了解 HTTP/2,並且能夠在多個終結點之間的一個 HTTP/2 連接上將多路復用的 gRPC 調用分發給代理。 使用代理比客戶端負載均衡更簡單,但會增加 gRPC 調用的額外延遲。
有很多 L7 代理可用。 一些選項包括:
在高性能方案中,可使用 gRPC 雙向流式處理取代一元 gRPC 調用。 雙向流啟動后,來回流式處理消息比使用多個一元 gRPC 調用發送消息更快。 流式處理消息作為現有 HTTP/2 請求上的數據發送,節省了為每個一元調用創建新的 HTTP/2 請求的開銷。
使用流式處理的復雜性和限制:
- 流可能會因服務或連接錯誤而中斷。 需要在出現錯誤時重啟流的邏輯。
- 對於多線程處理,
RequestStream.WriteAsync
並不安全。 一次只能將一條消息寫入流中。 通過單個流從多個線程發送消息需要制造者/使用者隊列(如 Channel<T>)來整理消息。 - gRPC 流式處理方法僅限於接收一種類型的消息並發送一種類型的消息。 例如,
rpc StreamingCall(stream RequestMessage) returns (stream ResponseMessage)
接收RequestMessage
並發送ResponseMessage
。 Protobuf 對使用Any
和oneof
支持未知消息或條件消息,可以解決此限制。
訪問 gRPC 尾部
gRPC 調用可能會返回 gRPC 尾部。 gRPC 尾部用於提供有關調用的名稱/值元數據。 尾部提供與 HTTP 頭相似的功能,但在調用結尾獲得。
gRPC 尾部可通過 GetTrailers()
進行訪問,它會返回元數據的集合。 尾部是在響應完成后返回的,因此你必須等待收到所有響應消息,然后才能訪問尾部。
一元和客戶端流式調用必須等待出現 ResponseAsync
后才能調用 GetTrailers()
:
var client = new Greet.GreeterClient(channel); using var call = client.SayHelloAsync(new HelloRequest { Name = "World" }); var response = await call.ResponseAsync; Console.WriteLine("Greeting: " + response.Message); // Greeting: Hello World var trailers = call.GetTrailers(); var myValue = trailers.GetValue("my-trailer-name");
服務器和雙向流式調用必須等到出現響應流,然后才能調用 GetTrailers()
:
var client = new Greet.GreeterClient(channel); using var call = client.SayHellos(new HelloRequest { Name = "World" }); await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine("Greeting: " + response.Message); // "Greeting: Hello World" is written multiple times } var trailers = call.GetTrailers(); var myValue = trailers.GetValue("my-trailer-name");
gRPC 尾部也可通過 RpcException
進行訪問。 服務可能會同時返回尾部和“異常”gRPC 狀態。 在這種情況下,尾部是從 gRPC 客戶端引起的異常中檢索得到的:
var client = new Greet.GreeterClient(channel); string myValue = null; try { using var call = client.SayHelloAsync(new HelloRequest { Name = "World" }); var response = await call.ResponseAsync; Console.WriteLine("Greeting: " + response.Message); // Greeting: Hello World var trailers = call.GetTrailers(); myValue = trailers.GetValue("my-trailer-name"); } catch (RpcException ex) { var trailers = ex.Trailers; myValue = trailers.GetValue("my-trailer-name"); }
訪問 gRPC 請求標頭
請求消息並不是客戶端將數據發送到 gRPC 服務的唯一方法。 標頭值在使用 ServerCallContext.RequestHeaders
的服務中可用。
public override Task<ExampleResponse> UnaryCall(ExampleRequest request, ServerCallContext context) { var userAgent = context.RequestHeaders.GetValue("user-agent"); // ... return Task.FromResult(new ExampleResponse()); }
解析 gRPC 方法中的 HttpContext
gRPC API 提供對某些 HTTP/2 消息數據(如方法、主機、標頭和尾部)的訪問權限。 訪問是通過傳遞到每個 gRPC 方法的 ServerCallContext
參數進行的:
ServerCallContext
不提供對所有 ASP.NET API 中 HttpContext
的完全訪問權限。 GetHttpContext
擴展方法提供對在 ASP.NET API 中表示基礎 HTTP/2 消息的 HttpContext
的完全訪問權限:
public class GreeterService : Greeter.GreeterBase { public override Task<HelloReply> SayHello( HelloRequest request, ServerCallContext context) { var httpContext = context.GetHttpContext(); var clientCertificate = httpContext.Connection.ClientCertificate; return Task.FromResult(new HelloReply { Message = "Hello " + request.Name + " from " + clientCertificate.Issuer }); } }
配置截止時間
建議配置 gRPC 調用截止時間,因為它提供調用時間的上限。 它能阻止異常運行的服務持續運行並耗盡服務器資源。 截止時間對於構建可靠應用非常有效。
配置 CallOptions.Deadline
以設置 gRPC 調用的截止時間:
var client = new Greet.GreeterClient(channel); try { var response = await client.SayHelloAsync( new HelloRequest { Name = "World" }, deadline: DateTime.UtcNow.AddSeconds(5)); // Greeting: Hello World Console.WriteLine("Greeting: " + response.Message); } catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded) { Console.WriteLine("Greeting timeout."); }
public override async Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { var user = await _databaseContext.GetUserAsync(request.Name, context.CancellationToken); return new HelloReply { Message = "Hello " + user.DisplayName }; }
截止時間隨 gRPC 調用發送到服務,並由客戶端和服務獨立跟蹤。 gRPC 調用可能在一台計算機上完成,但當響應返回給客戶端時,已超過了截止時間。
如果超過了截止時間,客戶端和服務將有不同的行為:
- 客戶端將立即中止基礎的 HTTP 請求並引發
DeadlineExceeded
錯誤。 客戶端應用可以選擇捕獲錯誤並向用戶顯示超時消息。 - 在服務器上,將中止正在執行的 HTTP 請求,並引發 ServerCallContext.CancellationToken。 盡管中止了 HTTP 請求,gRPC 調用仍將繼續在服務器上運行,直到方法完成。 將取消令牌傳遞給異步方法,使其隨調用一同被取消,這非常重要。 例如,向異步數據庫查詢和 HTTP 請求傳遞取消令牌。 傳遞取消令牌讓取消的調用可以在服務器上快速完成,並為其他調用釋放資源。
傳播截止時間
從正在執行的 gRPC 服務進行 gRPC 調用時,應傳播截止時間。
手動傳播截止時間可能會很繁瑣。 截止時間需要傳遞給每個調用,很容易不小心錯過。 gRPC 客戶端工廠提供自動解決方案。 指定 EnableCallContextPropagation
:
- 自動將截止時間和取消令牌傳播到子調用。
- 這是確保復雜的嵌套 gRPC 場景始終傳播截止時間和取消的一種極佳方式。
services .AddGrpcClient<User.UserServiceClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .EnableCallContextPropagation();
配置服務端選項
gRPC 服務在 Startup.cs 中使用 AddGrpc
進行配置。
選項 | 默認值 | 描述 |
---|---|---|
MaxSendMessageSize | null |
設置為 null 時,消息的大小不受限制。 |
MaxReceiveMessageSize | 4 MB | 增大此值可使服務器接收更大的消息,但可能會對內存消耗產生負面影響。 設置為 null 時,消息的大小不受限制。 |
EnableDetailedErrors | false |
如果為 true ,則當服務方法中引發異常時,會將詳細異常消息返回到客戶端。 默認值為 false 。 將 EnableDetailedErrors 設置為 true 可能會泄漏敏感信息。 |
CompressionProviders | gzip | 默認已配置提供程序支持 gzip 壓縮。 |
ResponseCompressionAlgorithm | null |
壓縮算法用於壓縮從服務器發送的消息。 該算法必須與 CompressionProviders 中的壓縮提供程序匹配。 若要使算法可壓縮響應,客戶端必須通過在 grpc-accept-encoding 標頭中進行發送來指示它支持算法。 |
ResponseCompressionLevel | null |
用於壓縮從服務器發送的消息的壓縮級別。 |
攔截器 | None | 隨每個 gRPC 調用一起運行的偵聽器的集合。 偵聽器按注冊順序運行。 全局配置的偵聽器在為單個服務配置的偵聽器之前運行。 有關 gRPC 偵聽器的詳細信息,請參閱 gRPC 偵聽器與中間件。 |
IgnoreUnknownServices | false |
如果為 true ,則對未知服務和方法的調用不會返回 UNIMPLEMENTED 狀態,並且請求會傳遞到 ASP.NET Core 中的下一個注冊中間件。 |
用於單個服務的選項會替代 AddGrpc
中提供的全局選項,可以使用 AddServiceOptions<TService>
進行配置:
public void ConfigureServices(IServiceCollection services) { services.AddGrpc().AddServiceOptions<MyService>(options => { options.MaxReceiveMessageSize = 2 * 1024 * 1024; // 2 MB options.MaxSendMessageSize = 5 * 1024 * 1024; // 5 MB }); }
配置客戶端選項
gRPC 客戶端配置在 GrpcChannelOptions
中進行設置。 下表描述了用於配置 gRPC 通道的選項:
選項 | 默認值 | 描述 |
---|---|---|
HttpHandler | 新實例 | 用於進行 gRPC 調用的 HttpMessageHandler 。 可以將客戶端設置為配置自定義 HttpClientHandler ,或將附加處理程序添加到 gRPC 調用的 HTTP 管道。 如果未指定 HttpMessageHandler ,則會通過自動處置為通道創建新 HttpClientHandler 實例。 |
HttpClient | null |
用於進行 gRPC 調用的 HttpClient 。 此設置是 HttpHandler 的替代項。 |
DisposeHttpClient | false |
如果設置為 true 且指定了 HttpMessageHandler 或 HttpClient ,則在處置 GrpcChannel 時,將分別處置 HttpHandler 或 HttpClient 。 |
LoggerFactory | null |
客戶端用於記錄有關 gRPC 調用的信息的 LoggerFactory 。 可以通過依賴項注入來解析或使用 LoggerFactory.Create 來創建 LoggerFactory 實例。 有關配置日志記錄的示例,請參閱 .NET 上 gRPC 中的日志記錄和診斷。 |
MaxSendMessageSize | null |
設置為 null 時,消息的大小不受限制。 |
MaxReceiveMessageSize | 4 MB | 增大此值可使客戶端接收更大的消息,但可能會對內存消耗產生負面影響。 設置為 null 時,消息的大小不受限制。 |
憑據 | null |
一個 ChannelCredentials 實例。 憑據用於將身份驗證元數據添加到 gRPC 調用。 |
CompressionProviders | gzip | 默認已配置提供程序支持 gzip 壓縮。 |
static async Task Main(string[] args) { var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { MaxReceiveMessageSize = 5 * 1024 * 1024, // 5 MB MaxSendMessageSize = 2 * 1024 * 1024 // 2 MB }); }
對調用 gRPC 服務的用戶進行身份驗證
gRPC 可與 ASP.NET Core 身份驗證配合使用,將用戶與每個調用關聯。
設置身份驗證后,可通過 ServerCallContext
使用 gRPC 服務方法訪問用戶。
public override Task<BuyTicketsResponse> BuyTickets( BuyTicketsRequest request, ServerCallContext context) { var user = context.GetHttpContext().User; // ... access data from ClaimsPrincipal ... }
持有者令牌身份驗證
客戶端可提供用於身份驗證的訪問令牌。 服務器驗證令牌並使用它來標識用戶。
在服務器上,使用 JWT 持有者中間件配置持有者令牌身份驗證。
在通道上配置 ChannelCredentials
是通過 gRPC 調用將令牌發送到服務的備用方法。 憑據在每次進行 gRPC 調用時運行,因而無需在多個位置編寫代碼用於自行傳遞令牌。
private static GrpcChannel CreateAuthenticatedChannel(string address) { var credentials = CallCredentials.FromInterceptor((context, metadata) => { if (!string.IsNullOrEmpty(_token)) { metadata.Add("Authorization", $"Bearer {_token}"); } return Task.CompletedTask; }); // SslCredentials is used here because this channel is using TLS. // CallCredentials can't be used with ChannelCredentials.Insecure on non-TLS channels. var channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions { Credentials = ChannelCredentials.Create(new SslCredentials(), credentials) }); return channel; }
令牌可作為標頭與調用一起發送:
public bool DoAuthenticatedCall( Ticketer.TicketerClient client, string token) { var headers = new Metadata(); headers.Add("Authorization", $"Bearer {token}"); var request = new BuyTicketsRequest { Count = 1 }; var response = await client.BuyTicketsAsync(request, headers); return response.Success; }
客戶端證書身份驗證
客戶端還可以提供用於身份驗證的客戶端證書。 證書身份驗證在 TLS 級別發生,遠在到達 ASP.NET Core 之前。 當請求進入 ASP.NET Core 時,可借助客戶端證書身份驗證包將證書解析為 ClaimsPrincipal
。
public Ticketer.TicketerClient CreateClientWithCert( string baseAddress, X509Certificate2 certificate) { // Add client cert to the handler var handler = new HttpClientHandler(); handler.ClientCertificates.Add(certificate); // Create the gRPC channel var channel = GrpcChannel.ForAddress(baseAddress, new GrpcChannelOptions { HttpHandler = handler }); return new Ticketer.TicketerClient(channel); }
將 gRPC 客戶端配置為使用身份驗證取決於使用的身份驗證機制。 之前的持有者令牌和客戶端證書示例演示可將 gRPC 客戶端配置為通過 gRPC 調用發送身份驗證元數據的幾種方法:
- 強類型 gRPC 客戶端在內部使用
HttpClient
。 可在 HttpClientHandler 上配置身份驗證,也可通過向HttpClient
添加自定義 HttpMessageHandler 實例進行配置。 - 每個 gRPC 調用都有一個可選的
CallOptions
參數。 可使用該選項的標頭集合發送自定義標頭。
建議由客戶端證書保護的 gRPC 服務使用 Microsoft.AspNetCore.Authentication.Certificate 包。 ASP.NET Core 認證身份驗證將對客戶端證書執行其他驗證,包括:
- 驗證證書是否具有有效的增強型密鑰使用 (EKU)
- 驗證是否在其有效期內
- 檢查證書吊銷
授權用戶訪問服務和服務方法
默認情況下,未經身份驗證的用戶可以調用服務中的所有方法。 若要要求進行身份驗證,請將 [Authorize]
特性應用於服務:
[Authorize] public class TicketerService : Ticketer.TicketerBase { }
服務端日志記錄
gRPC 在 Grpc
類別下添加日志。 若要啟用來自 gRPC 的詳細日志,請通過在 Logging
中的 LogLevel
子節中添加以下項目,將 Grpc
前綴配置為 appsettings.json 文件中的 Debug
級別:
{ "Logging": { "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information", "Grpc": "Debug" } } }
客戶端日志記錄
可以在創建客戶端通道時設置 GrpcChannelOptions.LoggerFactory
屬性。
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { LoggerFactory = _loggerFactory });
啟用客戶端日志記錄的另一種方法是使用 gRPC 客戶端工廠創建客戶端。 已向客戶端工廠注冊且解析自 DI 的 gRPC 客戶端將自動使用應用的已配置日志記錄。
如果應用未使用 DI,則可以使用 LoggerFactory.Create 創建新的 ILoggerFactory
實例。 若要訪問此方法,請將 Microsoft.Extensions.Logging 包添加到應用。
var loggerFactory = LoggerFactory.Create(logging => { logging.AddConsole(); logging.SetMinimumLevel(LogLevel.Debug); }); var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions { LoggerFactory = loggerFactory }); var client = Greeter.GreeterClient(channel);