1. 程式人生 > >淺議Grpc傳輸機制和WCF中的回撥機制的程式碼遷移

淺議Grpc傳輸機制和WCF中的回撥機制的程式碼遷移

淺議Grpc傳輸機制和WCF中的回撥機制的程式碼遷移

一、引子

如您所知,gRPC是目前比較常見的rpc框架,可以方便的作為服務與服務之間的通訊基礎設施,為構建微服務體系提供非常強有力的支援。

而基於.NET Core的gRPC.NET 元件截至2019年11月30日的最新版本為2.25.0,該版本基於.netstrandard2.1進行,能夠在.NET Core3.0上非常方便的實現,而且還能方便的遷移到基於.NET Core的windows桌面端開發體系。

在本文中參考微軟官方文件的示例,實現了一個從WCF 服務回撥機制遷移到gRPC的過程,由於時間倉促,如有疏漏,還望批評指正。第一篇主要從技術層面來分析遷移流程,第二篇打算從業務和程式碼整潔性角度來思考這個問題。

1.1、一些新東西:

1)、使用客戶端工廠元件 Grpc.Net.ClientFactory :

在新版本中,可以使用 Grpc.Net.ClientFactory 支援以依賴注入的形式AddGrpcClient,將grpc客戶端引入中,而無需每一次方法呼叫都使用 New 關鍵詞進行建立。 這對客戶端呼叫來說是極大的方便,畢竟隨著.NET Core的普及,對於許多開發者來說,看到 New 關鍵詞其實是很難受的啊。

示例:

以下程式碼以註冊了 GreetClient ,並在傳送 http 請求前,對請求頭資訊進行修改,新增 jwt 標識,以便傳送帶鑑權標識的請求。

serviceCollection.AddGrpcClient<GreeterClient>(
    o =>
    {
    o.Address = new Uri(configuration["address"]);
    })
    .AddHttpMessageHandler<JwtTokenHeader>();
public class GreetImpl
{

    private readonly GreetClient _greetClient;
    public GreetImpl(GreetClient greetClient)
    {

    }
}

JwtTokenHeader中的程式碼段:

request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "");
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

(以上示例程式碼僅供參考,不支援直接執行,且不支援.NET Framework。。)

所以到此為止,我們在使用gRPC開發時,需要(能)使用的元件包括以下幾種:

  • Grpc.AspNETCore包:這個包用於在asp.net core中提供grpc服務支援,在asp.netcore的服務端專案中以nuget安裝grpc元件時,需要安裝這個包。
    • Google.Protobuf元件:Protobuf協議的實現。
    • Grpc.AspNetCore.Server :gRPC Asp.NET Core服務端核心庫
    • Grpc.Core.Api :gRPC Core API核心庫
  • Grpc.Tools 包:內部封裝了從proto檔案生成gRPC服務端/客戶端方法存根的流程。
  • Grpc.Core:gRPC核心包。
  • Grpc.Net.Client:gRPC 客戶端實現核心庫。
    • Grpc.Core.Api :gRPC Core API核心庫
    • Grpc.Net.Common:gRPC 常用方法。
  • Grpc.Net.ClientFactory: gRPC客戶端工廠方法。僅用於標準庫2.1。
2)、其他特性:
  1. 支援 SerializationContext.GetBufferWriter 。
  2. 效能優化。 Optimize server's gRPC message serialization
  3. 驗證協議降級。 Validate gRPC response protocol is not downgraded
  4. New Grpc.AspNetCore.Server.Reflection package
  5. Log unsupported request content-type and protocol
  6. Major client performance improvement
  7. 修bug等。

( 當然,由於各種原因,未能親測。)

1.2、存在的缺陷

  • 目前的grpc的定位僅僅是一種資料傳輸機制,因此本身不包含負載均衡和服務管理的功能,一般會引入consul/etcd/zk等框架來實現服務治理。

  • 由於最新版本基於標準庫2.1進行構建,因此該最新版本無法在.net fx上使用(因為.netframework最高僅支援到標準庫2.0),不過只是新版本不支援,依然可以使用2.23.2的版本來實現。當然,以後也不會支援.netfx了。。

二、gRPC通訊方式

gRPC提供了以下四種傳輸方式:

檢視

2.1、Simple RPC

簡單RPC 傳輸。一般的rpc方法呼叫,一次請求返回一個物件。適用於類似於以前的webapi請求呼叫的形式。

    rpc Hello (HelloRequest) returns (HelloReply); 

2.1、Server-side streaming RPC

一種單向流,服務端流式RPC,客戶端向服務端請求後,由服務端以流的形式返回多個結果。例如可以用於客戶端需要從服務端獲取流媒體檔案。

rpc Subscribe (SubscribeRequest) returns (stream StockTickerUpdate);

2.3、Client-Side streaming RPC

一種單向流,客戶端單向流,客戶端以流的形式傳輸多個請求,服務端返回一個響應結果。例如可以用於客戶端需要向服務端推流的場景。

rpc Subscribe (stream SubscribeRequest) returns (StockTickerUpdate);

2.4、 Bidirectional streaming RPC

雙向流式rpc。客戶端和服務端均可以傳輸多個請求。例如可以用於遊戲中的雙向傳輸。

rpc Subscribe (stream SubscribeRequest) returns (stream StockTickerUpdate);

總之,看起來gRPC能夠實現目前所能設想的大部分場景,因此也被視為是古老的rpc框架 wcf ( Windows Communication Foundation )的替代者,官方專門編寫了一本電子書,用來給需要從 wcf 轉 gRPC的開發者提供指引。

具體地址為: 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的釋出,他已經不再使用WCF了,而是改用了gRPC。

三、WCF的通訊方式

3.1、簡述

WCF 是.NET框架中非常常用的一種元件,在.NET Framework 3.0時被引入,它整合了一些歷史悠久的技術框架或通訊機制,諸如 soap、remoting等。

由於WCF技術體系龐大,學習路線也比較陡峭,能夠駕馭的往往都是擁有多年工作經驗的資深開發者,開發者們有時需針對各個階段的內涵做深入的瞭解,才能開發對應的應用。

由於本人使用WCF的經驗尚淺(以前的專案用得少,充其量就用過Remoting),所以以下文字均來自網上現有資料的演繹,如有疏漏,敬請批評指正。

WCF中,需要定義合約作為通訊過程中的溝通方式。通訊雙方所遵循的通訊方式,有合約繫結來制定;通訊期間的安全性,有雙方約定的安全性層級來定義。

3.2、合約(Contract)

合約( Contract) 是WCF中最重要的基本概念,合約的使用分成兩個部分,一部分是以介面形式體現的合約,一部分是基於合約派生出的實現類。

合約分成四種類型:

資料合約 (Data Contract) :訂定雙方溝通時的資料格式。

服務合約 (Service Contract) :訂定服務的定義。

操作合約 (Operation Contract) :訂定服務提供的方法。在維基百科中翻譯為營運合約。

訊息合約 (Message Contract) :訂定在通訊期間改寫訊息內容的規範。

在維基百科中,提供了一個如下的程式碼示例。

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、協議繫結

WCF支援HTTP\TCP\命名管道( Named Pipe )、MSMQ( MSMQ )、點對點TCP Peer-To-Peer TCP 等協議。其中對HTTP協議的支援分為:基本HTTP支援\WS-HTTP支援;對TCP的協議也支NetTcpBinding\NetPeerTcpBinding等通訊方式。

從這裡可以看出,能夠駕馭WCF技術的,基本上都是.NET開發領域的大牛,涉及到如此多的技術棧,實在是令人欽佩。

由於WCF支援的協議很多,所以在進行WCF的客戶端和服務端開發時,需要使用統一通訊的協議,並且在編碼以及格式上也要一致。

維基百科提供了一個設定通訊繫結的示例配置檔案,當然,有時候無需通過配置檔案來配置wcf的服務資訊,通過程式碼建立也同樣可行。

<configuration>
  <system.serviceModel>
    <!-- 介面協議 -->
    <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中,一般預設的契約形式為點對點的請求-響應方式。即客戶端發出請求後,一直阻塞方法,指導服務端響應後,才能執行後面的程式碼。

這種模式類似於gRPC中的簡單傳輸機制,所以如果從WCF服務遷移到gRPC服務時,比較簡單純粹,只需根據對應的資料方法來訂定我們的服務協議檔案 proto 檔案。

例如,大概是這樣的:

[ServiceContract]
public interface ISimpleStockTickerCallback
{
    [OperationContract]
    void HelloWorld(string msg);
}

遷移到 gRpc中之後,就是這樣的實現:

rpc Hello (HelloRequest) returns (google.protobuf.Empty);
message HelloReply{
    string msg=1;
}
message HelloRequest{
    string msg=1; 
}

然後再在兩端程式碼中實現方法即可。(由於程式碼過於簡單,此處省略若干字)在引文3中,提供了非常完善的Wcf遷移到gRPC的程式碼流程,需要請自取。

4.2 遷移WCF的雙工通訊

1、WCF中的雙工通訊示例

在WCF中,雙工(Duplex)通訊很常用,在通訊過程中,雙方都可以向對方傳送訊息,使得很容易的就實現了服務端回撥客戶端。

在這種模式下,客戶端向服務端呼叫一個方法,然後在服務端回撥客戶端方法,可以理解為雙方的位置發生了改變,此時的服務端變成了客戶端,而客戶端變成了服務端。

如圖所示。

程式碼如下:

  1. 服務端:

    • 訂定契約HelloCallback,用於處理回撥的邏輯。
    • 訂定契約UserService 和 UserServiceImpl,並定義了一個 GetUser 方法。
    /// <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");
        }
    }

    啟動服務端程式時,需要建立服務端的Host主機資訊。

     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;
    }
  2. 客戶端:

    • 訂定契約HelloCallback 和客戶端的契約實現 HelloCallbackImpl 。

      /// <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,用以保持和服務端的契約保持一致。

      /// <summary>
      /// 使用者服務
      /// </summary>
      [ServiceContract(CallbackContract = typeof(HelloCallback))]
      public interface UserService
      {
          [OperationContract(IsOneWay = true)]
          void GetUser(string userName);
      }

      客戶端啟動時,連線到服務端。併發送GetUser方法。

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");
        }

實現效果如下:

這是一個典型的WCF雙工通訊的示例,在傳統的.NET Framework開發中可能非常常見,但是該如何才能遷移到gRPC服務中呢?

2、gRPC中的程式碼實現

  • 流程說明

gRPC中實現此雙工通訊,需要使用來自服務端的單向流來實現,但在gRPC中不能直接回調對應的方法,而是在服務端將流返回後,觸發對應客戶端程式碼中的方法來實現這個回撥的流程。

如圖所示:

  • 程式碼實現流程:

    1、定義 proto 協議檔案

    請求方法為getUser,並返回流。首先定義服務協議檔案,命名為 userService.proto 檔案。

    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、服務端實現

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" });
        }
    }
3、客戶端實現(需要被呼叫的方法)
public interface HelloCallback
{
    void SayHelloworld(string msg);
}
public class HelloCallbackImpl : HelloCallback
{
   public void SayHelloworld(string msg)
   {
      Console.Write(msg);
   }
}

4、使用者服務方法的實現

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、客戶端程式的入口

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模式,為什麼要使用服務端的單向流來實現了?

這種單向流中,客戶端無需等待服務端執行方法執行完,而是由服務端完成後續流程後,再回調客戶端的方法,使得流程變得簡單清晰。

在微軟的官方文件(參考文獻1)更適合介紹這個遷移過程的單向流的實現,通過實現服務端向客戶端推流的形式來介紹,只是方法相對而言實現的邏輯比較多,而鄙人這個示例則剝離了與讓我們理解服務端單向流流程無關的部分,使得流程看起來更簡單。

參考文獻

[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/