浅议gRPC的数据传输机制和回调机制
本文來自DotNET技術圈
作者:溪源
一、引子
如您所知,gRPC是目前比較常見的rpc框架,可以方便的作為服務與服務之間的通信基礎設施,為構建微服務體系提供非常強有力的支持。
而基于.NET Core的gRPC.NET 組件截至2019年11月30日的最新版本為2.25.0,該版本基于.netstrandard2.1進行,能夠在.NET Core3.0上非常方便的實現(xiàn),而且還能方便的遷移到基于.NET Core的windows桌面端開發(fā)體系。
在本文中參考微軟官方文檔的示例,實現(xiàn)了一個從WCF 服務回調機制遷移到gRPC的過程,由于時間倉促,如有疏漏,還望批評指正。第一篇主要從技術層面來分析遷移流程,第二篇打算從業(yè)務和代碼整潔性角度來思考這個問題。
1.1、一些新東西:
1)、使用客戶端工廠組件 Grpc.Net.ClientFactory :
在新版本中,可以使用 Grpc.Net.ClientFactory 支持以依賴注入的形式AddGrpcClient,將grpc客戶端引入中,而無需每一次方法調用都使用 New 關鍵詞進行創(chuàng)建。這對客戶端調用來說是極大的方便,畢竟隨著.NET Core的普及,對于許多開發(fā)者來說,看到 New 關鍵詞其實是很難受的啊。
示例:
以下代碼以注冊了 GreetClient ,并在發(fā)送 http 請求前,對請求頭信息進行修改,添加 jwt 標識,以便發(fā)送帶鑒權標識的請求。
serviceCollection.AddGrpcClient<GreeterClient>(o =>{o.Address = new Uri(configuration["address"]);}).AddHttpMessageHandler<JwtTokenHeader>();| 1 2 3 4 5 6 7 8 9 | public class GreetImpl {private readonly GreetClient _greetClient;public GreetImpl(GreetClient greetClient){} } |
JwtTokenHeader中的代碼段:
| 1 2 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", ""); HttpResponseMessage response = await base.SendAsync(request, cancellationToken); |
(以上示例代碼僅供參考,不支持直接運行,且不支持.NET Framework。。)
所以到此為止,我們在使用gRPC開發(fā)時,需要(能)使用的組件包括以下幾種:
Grpc.AspNETCore包:這個包用于在asp.net core中提供grpc服務支持,在asp.netcore的服務端項目中以nuget安裝grpc組件時,需要安裝這個包。
Google.Protobuf組件:Protobuf協(xié)議的實現(xiàn)。
Grpc.AspNetCore.Server :gRPC Asp.NET Core服務端核心庫
Grpc.Core.Api :gRPC Core API核心庫
Grpc.Tools 包:內部封裝了從proto文件生成gRPC服務端/客戶端方法存根的流程。
Grpc.Core:gRPC核心包。
Grpc.Net.Client:gRPC 客戶端實現(xiàn)核心庫。
Grpc.Core.Api :gRPC Core API核心庫
Grpc.Net.Common:gRPC 常用方法。
Grpc.Net.ClientFactory:gRPC客戶端工廠方法。僅用于標準庫2.1。
2)、其他特性:
支持 SerializationContext.GetBufferWriter 。
性能優(yōu)化。Optimize server’s gRPC message serialization
驗證協(xié)議降級。Validate gRPC response protocol is not downgraded
New Grpc.AspNetCore.Server.Reflection package
Log unsupported request content-type and protocol
Major client performance improvement
修bug等。
( 當然,由于各種原因,未能親測。)
1.2、存在的缺陷
目前的grpc的定位僅僅是一種數據傳輸機制,因此本身不包含負載均衡和服務管理的功能,一般會引入consul/etcd/zk等框架來實現(xiàn)服務治理。
由于最新版本基于標準庫2.1進行構建,因此該最新版本無法在.net fx上使用(因為.netframework最高僅支持到標準庫2.0),不過只是新版本不支持,依然可以使用2.23.2的版本來實現(xiàn)。當然,以后也不會支持.netfx了。。
二、gRPC通信方式
gRPC提供了以下四種傳輸方式:
2.1、Simple RPC
簡單RPC 傳輸。一般的rpc方法調用,一次請求返回一個對象。適用于類似于以前的webapi請求調用的形式。
| 1 | rpc Hello (HelloRequest) returns (HelloReply); |
2.1、Server-side streaming RPC
一種單向流,服務端流式RPC,客戶端向服務端請求后,由服務端以流的形式返回多個結果。例如可以用于客戶端需要從服務端獲取流媒體文件。
| 1 | rpc Subscribe (SubscribeRequest) returns (stream StockTickerUpdate); |
2.3、Client-Side streaming RPC
一種單向流,客戶端單向流,客戶端以流的形式傳輸多個請求,服務端返回一個響應結果。例如可以用于客戶端需要向服務端推流的場景。
| 1 | rpc Subscribe (stream SubscribeRequest) returns (StockTickerUpdate); |
2.4、 Bidirectional streaming RPC
雙向流式rpc。客戶端和服務端均可以傳輸多個請求。例如可以用于游戲中的雙向傳輸。
| 1 | rpc Subscribe (stream SubscribeRequest) returns (stream StockTickerUpdate); |
總之,看起來gRPC能夠實現(xiàn)目前所能設想的大部分場景,因此也被視為是古老的rpc框架 wcf ( Windows Communication Foundation )的替代者,官方專門編寫了一本電子書,用來給需要從 wcf 轉 gRPC的開發(fā)者提供指引。
具體地址為:?https://docs.microsoft.com/zh-cn/dotnet/architecture/grpc-for-wcf-developers/
除此之外,本人還看到了一些外網作者使用grpc 來移植 wcf的一些博客。
1、?https://www.seeleycoder.com/blog/migrating-wcf-to-grpc-netcore/
2、https://www.seeleycoder.com/blog/using-wcf-with-dotnetcore/
這兩篇博客的作者在.NET Core中使用了WCF,根據作者的說法,在.NET Core2.0中還能使用,但是隨著3.0的發(fā)布,他已經不再使用WCF了,而是改用了gRPC。
三、WCF的通信方式
3.1、簡述
WCF 是.NET框架中非常常用的一種組件,在.NET Framework 3.0時被引入,它整合了一些歷史悠久的技術框架或通信機制,諸如 soap、remoting等。
由于WCF技術體系龐大,學習路線也比較陡峭,能夠駕馭的往往都是擁有多年工作經驗的資深開發(fā)者,開發(fā)者們有時需針對各個階段的內涵做深入的了解,才能開發(fā)對應的應用。
由于本人使用WCF的經驗尚淺(以前的項目用得少,充其量就用過Remoting),所以以下文字均來自網上現(xiàn)有資料的演繹,如有疏漏,敬請批評指正。
WCF中,需要定義合約作為通信過程中的溝通方式。通信雙方所遵循的通信方式,有合約綁定來制定;通信期間的安全性,有雙方約定的安全性層級來定義。
3.2、合約(Contract)
合約( Contract) 是WCF中最重要的基本概念,合約的使用分成兩個部分,一部分是以接口形式體現(xiàn)的合約,一部分是基于合約派生出的實現(xiàn)類。
合約分成四種類型:
數據合約 (Data Contract) :訂定雙方溝通時的數據格式。
服務合約 (Service Contract) :訂定服務的定義。
操作合約 (Operation Contract) :訂定服務提供的方法。在維基百科中翻譯為營運合約。
消息合約 (Message Contract) :訂定在通信期間改寫消息內容的規(guī)范。
在維基百科中,提供了一個如下的代碼示例。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using System.ServiceModel; namespace Microsoft.ServiceModel.Samples {[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")] // 服務合約public interface ICalculator{[OperationContract] // 操作合約double Add(double n1, double n2);[OperationContract] // 操作合約double Subtract(double n1, double n2);[OperationContract] // 操作合約double Multiply(double n1, double n2);[OperationContract] // 操作合約double Divide(double n1, double n2);} } |
3.3、協(xié)議綁定
WCF支持HTTP\TCP\命名管道(?Named Pipe?)、MSMQ(?MSMQ?)、點對點TCP Peer-To-Peer TCP 等協(xié)議。其中對HTTP協(xié)議的支持分為:基本HTTP支持\WS-HTTP支持;對TCP的協(xié)議也支NetTcpBinding\NetPeerTcpBinding等通信方式。
從這里可以看出,能夠駕馭WCF技術的,基本上都是.NET開發(fā)領域的大牛,涉及到如此多的技術棧,實在是令人欽佩。
由于WCF支持的協(xié)議很多,所以在進行WCF的客戶端和服務端開發(fā)時,需要使用統(tǒng)一通信的協(xié)議,并且在編碼以及格式上也要一致。
維基百科提供了一個設置通信綁定的示例配置文件,當然,有時候無需通過配置文件來配置wcf的服務信息,通過代碼創(chuàng)建也同樣可行。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <configuration><system.serviceModel><!-- 接口協(xié)議 --><services><service name=" CalculatorService" ><endpoint address="" binding="wsHttpBinding" bindingConfiguration="Binding1"contract="ICalculator" /></service></services><!-- 通信機制 --><bindings><wsHttpBinding><binding name="Binding1"></binding></wsHttpBinding></bindings></system.serviceModel> </configuration> |
4、代碼遷移
4.1 遷移WCF的單工通信
在WCF中,一般默認的契約形式為點對點的請求-響應方式。即客戶端發(fā)出請求后,一直阻塞方法,指導服務端響應后,才能執(zhí)行后面的代碼。
這種模式類似于gRPC中的簡單傳輸機制,所以如果從WCF服務遷移到gRPC服務時,比較簡單純粹,只需根據對應的數據方法來訂定我們的服務協(xié)議文件 proto 文件。
例如,大概是這樣的:
| 1 2 3 4 5 6 | [ServiceContract] public interface ISimpleStockTickerCallback {[OperationContract]void HelloWorld(string msg); } |
遷移到 gRpc中之后,就是這樣的實現(xiàn):
| 1 2 3 4 5 6 7 | rpc Hello (HelloRequest) returns (google.protobuf.Empty); message HelloReply{string msg=1; } message HelloRequest{string msg=1; } |
然后再在兩端代碼中實現(xiàn)方法即可。(由于代碼過于簡單,此處省略若干字)在引文3中,提供了非常完善的Wcf遷移到gRPC的代碼流程,需要請自取。
4.2 遷移WCF的雙工通信
1、WCF中的雙工通信示例
在WCF中,雙工(Duplex)通信很常用,在通信過程中,雙方都可以向對方發(fā)送消息,使得很容易的就實現(xiàn)了服務端回調客戶端。
在這種模式下,客戶端向服務端調用一個方法,然后在服務端回調客戶端方法,可以理解為雙方的位置發(fā)生了改變,此時的服務端變成了客戶端,而客戶端變成了服務端。
如圖所示。
代碼如下:
服務端:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /// <summary> /// 用于回調的Hello方法 /// </summary> [ServiceContract] public interface HelloCallback {[OperationContract(IsOneWay = true)]void SayHelloworld(string msg); } /// <summary> /// 用戶服務,并回調客戶端到HelloCallback /// </summary> [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(HelloCallback))] public interface UserService {[OperationContract(IsOneWay = true)]void GetUser(string userName); } /// <summary> /// 用戶服務 /// </summary> [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class UserServiceImpl : UserService {HelloCallback callback;public void GetUser(string userName){Console.Write(userName);OperationContext context = OperationContext.Current;callback = context.GetCallbackChannel<HelloCallback>();callback.SayHelloworld($"{userName}:hello");} } |
啟動服務端程序時,需要創(chuàng)建服務端的Host主機信息。
| 1 2 3 4 5 6 7 8 9 10 | private static ServiceHost StartUserService(){var host = new ServiceHost(typeof(UserServiceImpl));var binding = new NetTcpBinding(SecurityMode.None);host.AddServiceEndpoint(typeof(UserService), binding,"net.tcp://localhost:12384/userservice");host.Open();return host; } |
訂定契約HelloCallback,用于處理回調的邏輯。
訂定契約UserService 和 UserServiceImpl,并定義了一個 GetUser 方法。
客戶端:
訂定契約HelloCallback 和客戶端的契約實現(xiàn) HelloCallbackImpl 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /// <summary> /// 回調Hello方法 /// </summary> [ServiceContract] public interface HelloCallback {[OperationContract(IsOneWay = true)]void SayHelloworld(string msg); } public class HelloCallbackImpl : HelloCallback {public void SayHelloworld(string msg){Console.Write(msg);} }
訂定契約UserService,用以保持和服務端的契約保持一致。
1 2 3 4 5 6 7 8 9 /// <summary> /// 用戶服務 /// </summary> [ServiceContract(CallbackContract = typeof(HelloCallback))] public interface UserService {[OperationContract(IsOneWay = true)]void GetUser(string userName); } 客戶端啟動時,連接到服務端。并發(fā)送GetUser方法。
1 2 3 4 5 6 7 8 9 10 11 private static void GetUser(NetTcpBinding binding){var address = new EndpointAddress("net.tcp://localhost:12384/userservice");var factory =new DuplexChannelFactory<UserService>(typeof(HelloCallbackImpl), binding,address);var context = new InstanceContext(new HelloCallbackImpl());var server = factory.CreateChannel(context);server.GetUser("zhangssan");} 實現(xiàn)效果如下:
這是一個典型的WCF雙工通信的示例,在傳統(tǒng)的.NET Framework開發(fā)中可能非常常見,但是該如何才能遷移到gRPC服務中呢?
2、gRPC中的代碼實現(xiàn)
流程說明
gRPC中實現(xiàn)此雙工通信,需要使用來自服務端的單向流來實現(xiàn),但在gRPC中不能直接回調對應的方法,而是在服務端將流返回后,觸發(fā)對應客戶端代碼中的方法來實現(xiàn)這個回調的流程。
如圖所示:
代碼實現(xiàn)流程:
1、定義 proto 協(xié)議文件
請求方法為getUser,并返回流。首先定義服務協(xié)議文件,命名為 userService.proto 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 syntax = "proto3";option csharp_namespace = "DulpexGrpcDemo";package DulpexGrpcDemo;service userService {rpc GetUser (HelloRequest) returns (stream HelloReply);rpc GetTest (HelloRequest) returns (HelloReply); } message HelloReply{string msg=1; } message HelloRequest{string msg=1; } 2、服務端實現(xiàn)
3、客戶端實現(xiàn)(需要被調用的方法)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class UserServiceImpl : userService.userServiceBase{public override async Task GetUser(HelloRequest request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context){await DoSomeThing(request.Msg, (msg) => { responseStream.WriteAsync(new HelloReply { Msg = $"{msg}:hello" }); });}//處理回調邏輯private async Task DoSomeThing(string msg, Action<string> action){Console.WriteLine(msg);action?.Invoke(msg);}public override Task<HelloReply> GetTest(HelloRequest request, ServerCallContext context){Console.WriteLine(request.Msg);return Task.FromResult(new HelloReply { Msg = $"{request.Msg}:hello" });}} 1 2 3 4 5 6 7 8 9 10 11 public interface HelloCallback {void SayHelloworld(string msg); } public class HelloCallbackImpl : HelloCallback {public void SayHelloworld(string msg){Console.Write(msg);} } 4、用戶服務方法的實現(xiàn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class UserServiceImpl{private userService.userServiceClient userServiceClient;private readonly HelloCallback _helloCallback;public UserServiceImpl(userService.userServiceClient serviceClient, HelloCallback helloCallback){userServiceClient = serviceClient;_helloCallback = helloCallback;}public async Task GetUser(){AsyncServerStreamingCall<HelloReply> stream = userServiceClient.GetUser(new HelloRequest { Msg = "張三" });await Helloworld(stream.ResponseStream);}async Task Helloworld(IAsyncStreamReader<HelloReply> stream){await foreach (var update in stream.ReadAllAsync()){_helloCallback.SayHelloworld(update.Msg);}} } 5、客戶端程序的入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Program{static async Task Main(string[] args){IServiceCollection servicesCollection = new ServiceCollection();IConfiguration configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json", true, false).Build();servicesCollection.AddGrpcClient<userService.userServiceClient>(o =>{o.Address = new Uri("https://localhost:5001");});servicesCollection.AddSingleton<UserServiceImpl>();servicesCollection.AddSingleton<HelloCallback, HelloCallbackImpl>();var userServiceImpl = servicesCollection.BuildServiceProvider().GetService<UserServiceImpl>();await userServiceImpl.GetUser();Console.ReadLine();}} 當然,從這個示例中,可能會覺得有點奇怪,明明可以使用請求-響應的簡單RPC模式,為什么要使用服務端的單向流來實現(xiàn)了?
這種單向流中,客戶端無需等待服務端執(zhí)行方法執(zhí)行完,而是由服務端完成后續(xù)流程后,再回調客戶端的方法,使得流程變得簡單清晰。
在微軟的官方文檔(參考文獻1)更適合介紹這個遷移過程的單向流的實現(xiàn),通過實現(xiàn)服務端向客戶端推流的形式來介紹,只是方法相對而言實現(xiàn)的邏輯比較多,而鄙人這個示例則剝離了與讓我們理解服務端單向流流程無關的部分,使得流程看起來更簡單。
參考文獻
[1] 官方文檔:?https://docs.microsoft.com/zh-cn/dotnet/architecture/grpc-for-wcf-developers/migrate-duplex-services
[2] Jon Seeley的官方博客,如何遷移將wcf服務遷移到grpc:https://www.seeleycoder.com/blog/migrating-wcf-to-grpc-netcore/
[3] Jon Seeley的官方博客,如何在.netcore中使用wcf:https://www.seeleycoder.com/blog/using-wcf-with-dotnetcore/
總結
以上是生活随笔為你收集整理的浅议gRPC的数据传输机制和回调机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《RPA、AI、.NET Core 与
- 下一篇: 如何在 Visual Studio 20