乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 淺析ASP.NET Core遠程過程調用,HttpClientFactory和gRPC最佳實踐

什麼是遠程調用

在分佈式計算中,遠程過程調用(Remote Procedure Call, RPC)是指計算機程序導致一個過程(子程序)在不同的地址空間(通常是在共享網絡上的另一臺計算機上)執行,其編碼就像普通的(本地)過程調用一樣,而程序員沒有明確編碼遠程交互的細節。在分佈式計算中,遠程過程調用(Remote Procedure Call, RPC)是指計算機程序導致一個過程(子程序)在不同的地址空間(通常是在共享網絡上的另一臺計算機上)執行,其編碼就像普通的(本地)過程調用一樣,而程序員沒有明確編碼遠程交互的細節。

image

也就是說,無論子程序是在執行程序的本地,還是在遠程,程序員寫的代碼基本上都是一樣的。這是一種客戶-服務器交互的形式(調用者是客戶,執行者是服務器),通常通過請求-響應的消息傳遞系統實現。在面向對象的編程範式中,RPC由遠程方法調用(RMI)表示。RPC模型意味着一定程度的位置透明,即無論本地還是遠程,調用程序基本相同,但通常,它們並不完全相同,所以本地調用可以與遠程調用區分開來。遠程調用通常比本地調用慢幾個數量級,而且不那麼可靠,所以區分它們很重要。

RPC是進程間通信(Inter-Process Communication, IPC)的一種形式,即不同的進程有不同的地址空間:如果在同一主機上,它們有不同的虛擬地址空間,即使物理地址空間是相同的;而如果它們在不同的主機上,物理地址空間是不同的。許多不同的(通常是不兼容的)技術被用來實現這一概念。

函數接口調用方式分爲:

  • 本地調用(Local Procedure Call, LPC),通常,在我們的代碼中調用一個函數,這個函數要麼是系統API,要麼是我們自己實現的本地代碼,一起編譯,一起發佈,也在同一個進程中一起執行,這就是本地調用!
  • 遠程調用(Remote Procedure Call, RPC) 被調用方法的具體實現不在同一個進程,而是在別進程,甚至別的電腦上。RPC一個重要思想就是,使遠程調用看起來像本地調用一樣,調用者無需知道被調用接口具體在哪臺機器上執行。

Socket是RPC經常採用的通信手段之一,除了Socket,RPC還有其他的通信方法,比如:http、操作系統自帶的管道等

歷史和淵源

請求-響應協議可以追溯到20世紀60年代末的早期分佈式計算,將遠程過程調用作爲網絡操作模型的理論建議可以追溯到20世紀70年代,而實際實現可以追溯到20世紀80年代初。布魯斯-傑-尼爾森一般被認爲是在1981年創造了"遠程過程調用"這個術語。

現代操作系統中使用的遠程過程調用可以追溯到RC4000多編程系統,它使用請求-響應通信協議進行進程同步。將網絡操作視爲遠程過程調用的想法至少可以追溯到1970年代的早期ARPANET文件中。1978年,Per Brinch Hansen提出了分佈式進程,一種基於"外部請求"的分佈式計算語言,由進程間的過程調用構成

最早的實際實現之一是在1982年由BrianRandell及其同事爲他們的UNIX機器之間的Newcastle Connection所做的。隨後不久,Andrew Birrell和Bruce Nelson在XeroxPARC的Cedar環境中提出了"Lupine"。Lupine自動生成存根,提供類型安全的綁定,並使用高效協議進行通信。RPC的首批商業用途之一是由Xerox在1981年以"Courier"的名義進行的。RPC在Unix上的第一個流行實現是Sun的RPC(現在稱爲ONC RPC),被用作網絡文件系統(NFS)的基礎

在20世紀90年代,隨着面向對象編程的普及,遠程方法調用(RMI)的另一種模式被廣泛實現,例如在通用對象請求代理架構(CORBA,1991)和Java遠程方法調用中。而RMI又隨着互聯網的興起而衰落,特別是在2000年代。

消息傳遞

RPC是一個請求-迴應協議。RPC由客戶端發起,它向已知的遠程服務器發送請求消息,以執行一個指定的程序和提供的參數。遠程服務器向客戶端發送一個響應,然後應用程序繼續其進程。當服務器處理調用時,客戶端被阻塞(它等待服務器完成處理後再恢復執行),除非客戶端向服務器發送異步請求,如XMLHttpRequest。在各種實現中存在許多變化和微妙之處,導致各種不同的(不兼容的)RPC協議。

遠程過程調用和本地調用的一個重要區別是,遠程調用可能因爲不可預測的網絡問題而失敗。而且,調用者通常必須在不知道遠程過程是否真的被調用的情況下處理這種失敗。無效程序(那些如果被多次調用就不會有額外效果的程序)很容易處理,但仍有足夠的困難,所以調用遠程程序的代碼往往被限制在精心編寫的低級子系統中。

事件的順序

  1. 客戶端調用客戶端存根。該調用是一個本地過程調用,參數以正常方式推送到堆棧中。
  2. 客戶端存根將參數打包成一個消息,並進行系統調用來發送該消息。包裝參數被稱爲marshalling。
  3. 客戶機的本地操作系統將消息從客戶機發送至服務器機。
  4. 服務器機器上的本地操作系統將傳入的數據包傳遞給服務器存根。
  5. 服務器存根從消息中解包參數。解除參數的包裝被稱爲解包。
  6. 最後,服務器存根調用服務器程序。回覆以相反的方向追蹤同樣的步驟。

管理向外請求的最佳實踐(HttpClientFactory)

https://github.com/TaylorShi/HelloRemoteCall

前世今生

HttpClient類型是在2012年發佈的.NET Framework4.5中引入的。換句話說,它已經存在一段時間了。HttpClient用於從由Uri標識的網絡資源發出HTTP請求和處理HTTP響應。

HTTP協議佔所有Internet流量的絕大部分。

根據推動最佳做法的新式應用程序開發原則,IHttpClientFactory充當工廠抽象,可以使用自定義配置創建HttpClient實例。.NET Core 2.1中引入了IHttpClientFactory。常見的基於HTTP的.NET工作負載可以輕鬆利用可復原和瞬態故障處理第三方中間件。

image

組件包

https://www.nuget.org/packages/Microsoft.Extensions.Http

https://www.nuget.org/packages/Microsoft.Extensions.Http.Polly

  • Microsoft.Extensions.Http
  • Microsoft.Extensions.Http.Polly

在Net Core中微軟爲我們提供了HttpClientFactory這個類,基於這個類我們可以更好的HttpClient管理。

HttpClientFactory存在於Microsoft.Extensions.Http包中。

核心能力

  • 管理內部HttpMessgaeHandler的生命週期,靈活應對資源問題和DNS刷新問題
  • 支持命名化、類型化配置,集中管理配置,避免衝突
  • 靈活的出站請求管道配置,輕鬆管理請求生命週期
  • 內置管道最外層和最內層的日誌記錄器,有Information和Trace輸出

核心對象

  • HttpClient
  • HttpMessageHandler
  • SocketsHttpHandler
  • DelegatingHandler
  • IHttpClientFactory
  • IHttpClientBuilder

管道模型

image

它和我們之前講到的中間件的管道模型很像,由HttpClient負責去調用DelegatingHandler,最後端是由SocketsHttpHandler來處理真正的HTTP請求。

中間的DelegatingHandler就是我們的管道處理器,也就是中間件部分,內置了最外層的LoggingScoped HttpMessageHandler用來記錄管道最外層的日誌。

還有一個Logging HttpMessageHandler這個用來記錄最內層的HTTP請求日誌,它可以記錄SocketsHttpHandler發起請求前的日誌和響應的日誌。

我們的擴展點就是在Custom MessageHandler,我們可以把自己的Handler註冊到管道模型裏面,就類似我們註冊中間件一樣,它會按照我們的註冊順序去執行。

它的整個請求過程:HttpClient發起請求 -> 最外層的日誌記錄器記錄日誌 -> 自定義的Handler去處理一些自定義的邏輯 -> 最內層的SockerHttpHandler(我們真正去發起遠程調用的處理程序,它會去向遠程站點發起HTTP請求並接受響應) -> Http最內層日誌記錄器會記錄我們的響應信息 -> 把響應結果交還給自定義的MessageHandler(接收響應後再處理接收響應後的邏輯) -> 最外層日誌記錄器會輸出響應日誌 -> 最終HttpClient拿到響應結果輸出給應用程序。

創建模式

HttpClientFactory提供了三種創建HttpClient的模式

  • 工程模式
  • 命名客戶端模式
  • 類型化客戶端模式

準備響應項目

將項目模板中的WeatherForecast改成Order

[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<OrderController> _logger;

    public OrderController(ILogger<OrderController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

將啓動配置文件launchSettings.json中的applicationUrl的端口號錯開下,改成50035002

{
    "demoForRemoteSite31": {
        "commandName": "Project",
        "launchBrowser": true,
        "launchUrl": "order",
        "environmentVariables": {
            "ASPNETCORE_ENVIRONMENT": "Development"
        },
        "applicationUrl": "https://localhost:5003;http://localhost:5002"
    }
}

在項目上右鍵,查看-在瀏覽器中查看,啓動它

image

得到可供調用的接口:https://localhost:5003/order

image

通過工廠模式請求接口

在另外一個請求示例項目中,我們準備基於工廠模式的訂單請求代碼OrderServiceClient

/// <summary>
/// 訂單服務請求(工廠構造模式)
/// </summary>
public class OrderServiceClient
{
    readonly IHttpClientFactory _httpClientFactory;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="httpClientFactory"></param>
    public OrderServiceClient(IHttpClientFactory httpClientFactory)
    {
        this._httpClientFactory = httpClientFactory;
    }

    public async Task<string> GetAsync()
    {
        var client = _httpClientFactory.CreateClient();

        // 使用Client發起Http請求
        return await client.GetStringAsync("https://localhost:5003/order");
    }
}

Startup.csConfigureServices添加對HttpClient的支持,並且添加OrderServiceClient

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddHttpClient();
    services.AddScoped<OrderServiceClient>();
}

接着我們改造下Controller,讓它提供一個Action以便我們可以觸發調用。

/// <summary>
/// 訂單服務
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;
    private readonly OrderServiceClient _orderServiceClient;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="logger"></param>
    /// <param name="orderServiceClient"></param>
    public OrderController(ILogger<OrderController> logger, OrderServiceClient orderServiceClient)
    {
        _logger = logger;
        _orderServiceClient = orderServiceClient;
    }

    /// <summary>
    /// 獲取接口
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    public async Task<string> Get()
    {
        return await _orderServiceClient.GetAsync();
    }
}

啓動項目運行下

image

image

順利完成請求。

這裏我們使用了IHttpClientFactoryCreateClient方式來獲取請求客戶端。

使用命名客戶端方式發起請求

我們可以通過AddHttpClient的參數來命名一個新的客戶端,並且給它設置一些策略

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddHttpClient("TeslaOrderHttpClient", configureClient =>
    {
        configureClient.DefaultRequestHeaders.Add("tesla-header", "TeslaOrder");
        configureClient.BaseAddress = new Uri("https://localhost:5003");
    });
    services.AddScoped<TeslaOrderHttpClient>();

這裏我們就把它取名爲TeslaOrderHttpClient,並且攜帶了定製Header以及指定它的BaseAddress。

接下來我們看看關於TeslaOrderHttpClient類的定義,基於之前的,我們稍作改造。

/// <summary>
/// 特斯拉訂單網絡請求
/// </summary>
public class TeslaOrderHttpClient
{
    readonly IHttpClientFactory _httpClientFactory;

    /// <summary>
    /// 客戶端名稱
    /// </summary>
    readonly string _clientName = "TeslaOrderHttpClient";

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="httpClientFactory"></param>
    public TeslaOrderHttpClient(IHttpClientFactory httpClientFactory)
    {
        this._httpClientFactory = httpClientFactory;
    }

    public async Task<string> GetAsync()
    {
        // 根據客戶端的名稱來獲取客戶端
        var client = _httpClientFactory.CreateClient(_clientName);

        // 使用Client發起Http請求
        return await client.GetStringAsync("/order");
    }
}

可以看從我們這次根據名稱來獲取HttpClient,並且這個名稱就是前面約定的。

在Controller中添加新的Action來支持對它的調用

/// <summary>
/// 訂單服務
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;
    private readonly TeslaOrderHttpClient _teslaOrderHttpClient;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="logger"></param>
    /// <param name="orderServiceClient"></param>
    public OrderController(ILogger<OrderController> logger, TeslaOrderHttpClient teslaOrderHttpClient)
    {
        _logger = logger;
        _teslaOrderHttpClient = teslaOrderHttpClient;
    }

    /// <summary>
    /// 獲取接口
    /// </summary>
    /// <returns></returns>
    [HttpGet("GetTeslaOrder")]
    public async Task<string> GetTeslaOrder()
    {
        return await _teslaOrderHttpClient.GetAsync();
    }
}

運行試試

image

image

從斷點可以看出,這裏已經正確獲取到我們配置的HttpClient了

另外還可以給這個HttpClient設置生命週期SetHandlerLifetime

services.AddHttpClient("TeslaOrderHttpClient", configureClient =>
{
    configureClient.DefaultRequestHeaders.Add("tesla-header", "TeslaOrder");
    configureClient.BaseAddress = new Uri("https://localhost:5003");
}).SetHandlerLifetime(TimeSpan.FromMinutes(4));

通過管道模式來設置客戶端

繼承自DelegatingHandlerTeslaOrderDelegatingHandler

/// <summary>
/// 特斯拉訂單管道處理程序
/// </summary>
public class TeslaOrderDelegatingHandler : DelegatingHandler
{
    /// <summary>
    /// 發送
    /// </summary>
    /// <param name="request"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("t-Guid", Guid.NewGuid().ToString());
        var result = await base.SendAsync(request, cancellationToken);
        return result;
    }
}

我們重寫其SendAsync方法,並且和前面一樣,添加我們自定義的Header進來。

然後我們把它註冊進去AddHttpMessageHandler

services.AddScoped<TeslaOrderDelegatingHandler>();
services.AddHttpClient("TeslaOrderHttpClient", configureClient =>
{
    configureClient.DefaultRequestHeaders.Add("tesla-header", "TeslaOrder");
    configureClient.BaseAddress = new Uri("https://localhost:5003");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(4))
.AddHttpMessageHandler(serviceProvider => serviceProvider.GetService<TeslaOrderDelegatingHandler>());

運行下

image
image
image

看到,它會作用於真正請求之前。

使用類型客戶端方式發起請求

類型客戶端的本質也是命名客戶端,只是它的本質是使用客戶端的類型名稱來作爲HttpClient的配置名稱的。

/// <summary>
/// 類型訂單客戶端
/// </summary>
public class TypedOrderHttpClient
{
    readonly HttpClient _httpClient;

    /// <summary>
    /// 構造函數
    /// </summary>
    /// <param name="httpClient"></param>
    public TypedOrderHttpClient(HttpClient httpClient)
    {
        this._httpClient = httpClient;
    }

    public async Task<string> GetAsync()
    {
        // 使用Client發起Http請求
        return await _httpClient.GetStringAsync("/order");
    }
}

這裏可以不使用HttpClientFactory

在註冊的時候直接services.AddHttpClient<TypedOrderHttpClient>即可

services.AddHttpClient<TypedOrderHttpClient>(configureClient =>
{
    configureClient.DefaultRequestHeaders.Add("tesla-header", "TeslaOrder");
    configureClient.BaseAddress = new Uri("https://localhost:5003");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(4))
.AddHttpMessageHandler(serviceProvider => serviceProvider.GetService<TeslaOrderDelegatingHandler>());

運行

image

啓用日誌輸出

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "System.Net.Http.HttpClient.TypedOrderHttpClient": "Trace"
    }
  },
  "AllowedHosts": "*"
}

注意這裏TypedOrderHttpClient和你要輸出的HttpClient配置名稱保持一致。

info: System.Net.Http.HttpClient.TypedOrderHttpClient.LogicalHandler[100]
      Start processing HTTP request GET https://localhost:5003/order
trce: System.Net.Http.HttpClient.TypedOrderHttpClient.LogicalHandler[102]
      Request Headers:
      tesla-header: TeslaOrder

info: System.Net.Http.HttpClient.TypedOrderHttpClient.ClientHandler[100]
      Sending HTTP request GET https://localhost:5003/order
trce: System.Net.Http.HttpClient.TypedOrderHttpClient.ClientHandler[102]
      Request Headers:
      tesla-header: TeslaOrder
      t-Guid: 92f85542-3db7-4b95-966d-71c8dc15f022

info: System.Net.Http.HttpClient.TypedOrderHttpClient.ClientHandler[101]
      Received HTTP response after 192.8728ms - OK
trce: System.Net.Http.HttpClient.TypedOrderHttpClient.ClientHandler[103]
      Response Headers:
      Date: Wed, 19 Oct 2022 16:28:20 GMT
      Server: Kestrel
      Transfer-Encoding: chunked
      Content-Type: application/json; charset=utf-8

info: System.Net.Http.HttpClient.TypedOrderHttpClient.LogicalHandler[101]
      End processing HTTP request after 213.5573ms - OK
trce: System.Net.Http.HttpClient.TypedOrderHttpClient.LogicalHandler[103]
      Response Headers:
      Date: Wed, 19 Oct 2022 16:28:20 GMT
      Server: Kestrel
      Transfer-Encoding: chunked
      Content-Type: application/json; charset=utf-8

image

可以看到整個管道的日誌。

總結

實戰中,建議優先採用類型客戶端方式,可以爲不同服務客戶端設置不同生命週期和管道模型。

內部服務間通訊利器(gRPC)

什麼是gRPC

https://grpc.io

image

gRPC是一個現代開源的高性能遠程過程調用(RPC)框架,可以在任何環境下運行。它可以通過對負載平衡、跟蹤、健康檢查和認證的可插拔支持,有效地連接數據中心內和跨數據中心的服務。它也適用於分佈式計算的最後一英里,將設備、移動應用和瀏覽器連接到後端服務。

gRPC是一個現代的、高性能的框架,發展了古老的遠程過程調用(RPC)協議。在應用層面,gRPC簡化了客戶端和後端服務之間的信息傳遞。gRPC起源於谷歌,是開源的,是雲原生計算基金會(CNCF)雲原生產品生態系統的一部分。CNCF認爲gRPC是一個孵化項目。孵化意味着終端用戶正在生產應用中使用該技術,而且該項目有大量的貢獻者。

一個典型的gRPC客戶端應用程序將暴露一個本地的、進程中的函數,實現一個業務操作。掩蓋之下,該本地函數調用了遠程機器上的另一個函數。看上去是一個本地調用,實際上是對遠程服務的一個透明的進程外調用。RPC管道抽象了計算機之間的點對點網絡通信、序列化和執行

在雲原生應用中,開發人員經常跨編程語言、框架和技術工作。這種互操作性使消息契約和跨平臺通信所需的管道變得複雜。gRPC提供了一個"統一的水平層",抽象了這些問題。開發人員在他們的原生平臺上編碼,專注於業務功能,而gRPC處理通信管道。

gRPC爲大多數流行的開發堆棧提供全面支持,包括Java、JavaScript、C#、Go、Swift和NodeJS。

在gRPC中,客戶端應用程序可以直接調用不同機器上的服務器應用程序的方法,就像它是一個本地對象一樣,使你更容易創建分佈式應用程序和服務。就像許多RPC系統一樣,gRPC是圍繞着定義服務的想法,指定可以遠程調用的方法及其參數和返回類型。在服務器端,服務器實現這個接口並運行gRPC服務器來處理客戶端的調用。在客戶端,客戶端有一個存根(在某些語言中被稱爲只是一個客戶端),提供與服務器相同的方法。

gRPC客戶端和服務器可以在各種環境中運行並相互交談--從谷歌內部的服務器到你自己的桌面--並且可以用任何gRPC支持的語言編寫。因此,舉例來說,你可以很容易地用Java創建一個gRPC服務器,客戶端用Go、Python或Ruby。此外,最新的谷歌API將有gRPC版本的接口,讓你輕鬆地在你的應用程序中建立谷歌功能。

image

gRPC的好處

gRPC使用HTTP/2作爲其傳輸協議。雖然與HTTP 1.1兼容,但HTTP/2具有許多高級功能。

  • 用於數據傳輸的二進制框架協議--與HTTP 1.1不同,它是基於文本的。
  • 支持多路複用,可在同一連接上發送多個並行請求--HTTP 1.1限制每次處理一個請求/響應信息。
  • 雙向全雙工通信,可同時發送客戶端請求和服務器響應。
  • 內置的流媒體,使請求和響應能夠異步地流傳大數據集。
  • 頭部壓縮,減少網絡使用。

gRPC是輕量級和高性能的。它可以比JSON序列化快8倍,信息量小60-80%。用微軟Windows通信基金會(WCF)的話說,gRPC的性能超過了高度優化的NetTCP綁定的速度和效率。與NetTCP不同的是,gRPC是跨平臺的,因爲NetTCP偏向於微軟的協議棧

協議緩衝區(Protocol Buffers)

https://developers.google.com/protocol-buffers/docs/overview

gRPC採用了一種叫做協議緩衝區(Protocol Buffers)的開源技術。它們提供了一種高效的、平臺中立的序列化格式,用於序列化服務之間相互發送的結構化消息。使用跨平臺的接口定義語言(IDL),開發者爲每個微服務定義一個服務合同。該合同以基於文本的.proto文件實現,描述了每個服務的方法、輸入和輸出。同樣的合同文件可以用於gRPC客戶端和建立在不同開發平臺上的服務。

使用proto文件,Protobuf編譯器protoc可以爲你的目標平臺生成客戶端和服務代碼。這些代碼包括以下部分。

  • 強類型的對象,由客戶和服務共享,表示服務操作和消息的數據元素。
  • 一個強類型的基類,具有所需的網絡管道,遠程gRPC服務可以繼承和擴展。
  • 一個客戶端存根,包含調用遠程gRPC服務所需的管道。

在運行時,每個消息都被序列化爲標準的Protobuf表示,並在客戶端和遠程服務之間進行交換。與JSON或XML不同,Protobuf消息被序列化爲編譯的二進制字節

這本書《gRPC for WCF Developers》(可從微軟架構網站獲得)對gRPC和協議緩衝區進行了深入介紹。

image

容器上的eShop中的gRPC

gRPC的特點

  • 提供幾乎所有主流語言的實現,打破語言隔閡
  • 基於HTTP/2,開放協議,受到廣泛的支持,易於實現和集成
  • 默認使用Protocol Buffers序列化,性能相較於Restful Json好很多
  • 工具鏈成熟,代碼生成便捷,開箱即用
  • 支持雙向流式的請求和響應,對批量處理、低延時場景友好

.Net生態對gRPC的支持情況

  • 提供基於HttpClient的原生框架實現
  • 提供原生的ASP.NET Core集成庫
  • 提供完整的代碼生成工具
  • Visual Studio和Visual Studio Code提供proto文件的智能提示

客戶端核心包

  • Google.Protobuf
  • Grpc.Net.Client
  • Grpc.Net.ClientFactory
  • Grpc.Tools

.proto文件

  • 定義包、庫名
  • 定義服務Service
  • 定義輸入輸出模型Message

proto文件主要是負責定義包名和庫名,定義我們的服務,定義服務的輸入輸出類型,基於這些定義可以通過Grpc.Tools生成我們的服務端代碼和客戶端的代碼。

gRPC異常處理

  • 使用Grpc.Core.RpcException
  • 使用Grpc.Core.Interceptors.Interceptor

gRPC發生異常時,我們會使用到Grpc的RpcException這個異常,我們捕獲到這個異常就可以捕捉到gRPC相關的異常,它也提供了攔截器的機制,可以通過注入攔截器來處理我們的異常。

gRPC與HTTP證書

  • 使用自制證書
  • 使用非加密的HTTP2

gRPC基於HTTP2,默認情況下HTTP2使用了HTTP的加密協議,在發佈我們的gRPC服務時會需要用到證書,同時它也提供了不使用證書的解決方案。

引入Grpc並且定義proto

依賴包

https://www.nuget.org/packages/Grpc.AspNetCore

dotnet add package Grpc.AspNetCore

image

這裏我們新建一個Proto文件夾來存放約定的.proto文件

image

這裏新建一個order.proto文件

syntax = "proto3";

option csharp_namespace = "GrpcServices";

service OrderGrpc
{
	rpc CreateOrder(CreateOrderCommand) returns (CreateOrderResult);
}

message CreateOrderCommand
{
	string buyerId = 1;
	int32 productId = 2;
	double unitPrice = 3;
	double discount = 4;
	int32 units = 5;
}

message CreateOrderResult
{
	int32 orderId = 1;
}

第一行表示了使用proto3協議,第二行、第三行表示了命名空間是GrpcServices

接下來定義了一個服務叫OrderGrpc,這個服務有一個方法叫CreateOrder,入參是CreateOrderCommand,響應是CreateOrderResult

在gRPC的proto文件裏面定義輸入輸出響應的時候,都是message,這裏定義了兩個messageCreateOrderCommandCreateOrderResult

需要爲每一個字段定義它的順序,這個順序決定了序列化時的順序,它不像Json,我們需要定義Key、Value,在序列化的時候,它實際上是根據數據類型和順序來識別我們的字段的值,所以這也是Protocol Buffers比HTTP Json快的原因。

基於proto生成編譯前代碼

同時,我們需要修改下order.proto的屬性,將它的Build Action設置爲Protobuf compiler類型。

image

我們還可以根據它實際作用設定它的工作場景:Client and ServerClient OnlyServer Only

image

這時候,它在項目文件中描述是同步變更的

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>410ca9c3-222f-4093-8d2e-1b9457d86656</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="Proto\order.proto" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="Proto\order.proto" GrpcServices="Server" />
  </ItemGroup>

image

這時候重新生成一次項目,我們就可以發現,在obj目錄下,會生成對應的cs文件。

image

image

看下Order.cs的定義

// <auto-generated>
//     Generated by the protocol buffer compiler.  DO NOT EDIT!
//     source: Proto/order.proto
// </auto-generated>
#pragma warning disable 1591, 0612, 3021, 8981
#region Designer generated code

using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace GrpcServices {

  /// <summary>Holder for reflection information generated from Proto/order.proto</summary>
  public static partial class OrderReflection {

    #region Descriptor
    /// <summary>File descriptor for Proto/order.proto</summary>
    public static pbr::FileDescriptor Descriptor {
      get { return descriptor; }
    }
    private static pbr::FileDescriptor descriptor;

    static OrderReflection() {
      byte[] descriptorData = global::System.Convert.FromBase64String(
          string.Concat(
            "ChFQcm90by9vcmRlci5wcm90byJsChJDcmVhdGVPcmRlckNvbW1hbmQSDwoH",
            "YnV5ZXJJZBgBIAEoCRIRCglwcm9kdWN0SWQYAiABKAUSEQoJdW5pdFByaWNl",
            "GAMgASgBEhAKCGRpc2NvdW50GAQgASgBEg0KBXVuaXRzGAUgASgFIiQKEUNy",
            "ZWF0ZU9yZGVyUmVzdWx0Eg8KB29yZGVySWQYASABKAUyQwoJT3JkZXJHcnBj",
            "EjYKC0NyZWF0ZU9yZGVyEhMuQ3JlYXRlT3JkZXJDb21tYW5kGhIuQ3JlYXRl",
            "T3JkZXJSZXN1bHRCD6oCDEdycGNTZXJ2aWNlc2IGcHJvdG8z"));
      descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
          new pbr::FileDescriptor[] { },
          new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
            new pbr::GeneratedClrTypeInfo(typeof(global::GrpcServices.CreateOrderCommand), global::GrpcServices.CreateOrderCommand.Parser, new[]{ "BuyerId", "ProductId", "UnitPrice", "Discount", "Units" }, null, null, null, null),
            new pbr::GeneratedClrTypeInfo(typeof(global::GrpcServices.CreateOrderResult), global::GrpcServices.CreateOrderResult.Parser, new[]{ "OrderId" }, null, null, null, null)
          }));
    }
    #endregion

  }

有點長。。。

看下OrderGrpc.cs的定義

// <auto-generated>
//     Generated by the protocol buffer compiler.  DO NOT EDIT!
//     source: Proto/order.proto
// </auto-generated>
#pragma warning disable 0414, 1591, 8981
#region Designer generated code

using grpc = global::Grpc.Core;

namespace GrpcServices {
  public static partial class OrderGrpc
  {
    static readonly string __ServiceName = "OrderGrpc";

    [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
    static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context)
    {
      #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION
      if (message is global::Google.Protobuf.IBufferMessage)
      {
        context.SetPayloadLength(message.CalculateSize());
        global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter());
        context.Complete();
        return;
      }
      #endif
      context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message));
    }

    [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
    static class __Helper_MessageCache<T>
    {
      public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T));
    }

使用自動生成的代碼

首先,我們要把服務端那個proto文件共享過來,我們編輯客戶端項目的項目文件demoForGrpcClient.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>4a768401-255f-4d25-804e-ab6643ba275d</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="..\demoForGrpcServer\Proto\order.proto" GrpcServices="Client" />
  </ItemGroup>

這裏根據相對路徑,把位於demoForGrpcServerorder.proto引入進來,並且標識其場景爲Client

接下來,創建名爲GrpcServices的文件夾,新建訂單服務類OrderService,繼承自OrderGrpc.OrderGrpcBase

如果前面一步成功生成,那麼這裏就可以繼承到,如果前面一步沒有生成,那這裏會找不到定義

/// <summary>
/// 訂單服務(基於Grpc)
/// </summary>
public class OrderService : OrderGrpc.OrderGrpcBase
{
    /// <summary>
    /// 重寫創建訂單
    /// </summary>
    /// <param name="request"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public override Task<CreateOrderResult> CreateOrder(CreateOrderCommand request, ServerCallContext context)
    {
        return base.CreateOrder(request, context);
    }
}

這裏在OrderGrpc.OrderGrpcBase就會存在我們前面在proto定義的那個CreateOrder方法,我們可以在這裏重寫它,以便插入我們想自定義的邏輯。

其中request是入參,context是當前請求的上下文,ServerCallContext是Grpc的一個內置類。

/// <summary>
/// Context for a server-side call.
/// </summary>
public abstract class ServerCallContext
{
    private Dictionary<object, object>? userState;

    /// <summary>
    /// Creates a new instance of <c>ServerCallContext</c>.
    /// </summary>
    protected ServerCallContext()
    {
    }

    /// <summary>
    /// Asynchronously sends response headers for the current call to the client. This method may only be invoked once for each call and needs to be invoked
    /// before any response messages are written. Writing the first response message implicitly sends empty response headers if <c>WriteResponseHeadersAsync</c> haven't
    /// been called yet.
    /// </summary>
    /// <param name="responseHeaders">The response headers to send.</param>
    /// <returns>The task that finished once response headers have been written.</returns>
    public Task WriteResponseHeadersAsync(Metadata responseHeaders)
    {
        return WriteResponseHeadersAsyncCore(responseHeaders);
    }

    /// <summary>
    /// Creates a propagation token to be used to propagate call context to a child call.
    /// </summary>
    public ContextPropagationToken CreatePropagationToken(ContextPropagationOptions? options = null)
    {
        return CreatePropagationTokenCore(options);
    }

    /// <summary>Name of method called in this RPC.</summary>
    public string Method => MethodCore;

    /// <summary>Name of host called in this RPC.</summary>
    public string Host => HostCore;

    /// <summary>Address of the remote endpoint in URI format.</summary>
    public string Peer => PeerCore;

    /// <summary>Deadline for this RPC. The call will be automatically cancelled once the deadline is exceeded.</summary>
    public DateTime Deadline => DeadlineCore;

    /// <summary>Initial metadata sent by client.</summary>
    public Metadata RequestHeaders => RequestHeadersCore;

最終我們可以根據實際業務需要來改造這個服務代碼

/// <summary>
/// 訂單服務(基於Grpc)
/// </summary>
public class OrderService : OrderGrpc.OrderGrpcBase
{
    /// <summary>
    /// 重寫創建訂單
    /// </summary>
    /// <param name="request"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public override Task<CreateOrderResult> CreateOrder(CreateOrderCommand request, ServerCallContext context)
    {
        // 可替換成真實的創建訂單服務的業務代碼
        return Task.FromResult(new CreateOrderResult { OrderId = 11 });
    }
}

我們在Startup.csConfigure方法直接設計一次gRPC調用

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            OrderGrpcClient service = context.RequestServices.GetService<OrderGrpcClient>();
            try
            {
                CreateOrderResult result = service.CreateOrder(new CreateOrderCommand { BuyerId = "abc" });
                await context.Response.WriteAsync(result.OrderId.ToString());
            }
            catch (Exception ex)
            {

            }
        });

        endpoints.MapControllers();
    });
}

當訪問根路徑/的時候,我們獲取OrderGrpcClient實例並且調用一次CreateOrder方法,並且我們將拿到的結果值放到輸出結果中。

運行之後,我們發現報錯了

fail: Grpc.Net.Client.Internal.GrpcCall[6]
      Error starting gRPC call.
System.Net.Http.HttpRequestException: 由於目標計算機積極拒絕,無法連接。
 ---> System.Net.Sockets.SocketException (10061): 由於目標計算機積極拒絕,無法連接。
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---

image

服務端側沒響應,因爲服務側還沒起來,我們將服務端弄起來,發現繼續報錯,這次是報SSL證書問題。

接下來,我們將服務端內置的HTTP服務器Kestrel的配置稍作修改appsettings.json

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://+:5000"
      },
      "Https": {
        "Url": "https://+:5001"
      },
      "Http2": {
        "Url": "http://+:5002",
        "Protocols": "Http2"
      }
    }
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

這裏設置HTTP協議端口是5000,HTTPS協議端口是5001,還另外監聽一個端口5002,它是HTTP協議,但是把它標記爲HTTP2,這樣它就是不加密的HTTP2協議,gRPC通信的時候,走的是HTTP2協議,但是會使用HTTP的證書,所以接下來,我們就改用5001給客戶端用。

這時候,把服務端項目設置爲啓動項目,右鍵查看模式啓動它

image

image

那麼接下來,我們在客戶端項目中,切換到5001

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    });
}

運行客戶端項目,這時候我們看到成功收到了來自服務端的響應,這次gRPC調用成功了

info: System.Net.Http.HttpClient.OrderGrpcClient.LogicalHandler[100]
      Start processing HTTP request POST https://localhost:5001/OrderGrpc/CreateOrder
info: System.Net.Http.HttpClient.OrderGrpcClient.ClientHandler[100]
      Sending HTTP request POST https://localhost:5001/OrderGrpc/CreateOrder
info: System.Net.Http.HttpClient.OrderGrpcClient.ClientHandler[101]
      Received HTTP response after 364.8463ms - OK
info: System.Net.Http.HttpClient.OrderGrpcClient.LogicalHandler[101]
      End processing HTTP request after 378.509ms - OK

image

image

不配置證書使用gRPC

如果想不配置證書就使用gRPC,我們也可以直接請求http協議的5002端口。

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        //grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
        grpcClientFactoryOptions.Address = new Uri("http://localhost:5002");
    });
}

但是這樣會報個錯

fail: Grpc.Net.Client.Internal.GrpcCall[6]
      Error starting gRPC call.
System.Net.Http.HttpRequestException: An error occurred while sending the request.
 ---> System.IO.IOException: The response ended prematurely.
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ReadNextResponseHeaderLineAsync(Boolean foldedHeadersAllowed)
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)

我們可以給客戶端一個特殊配置,允許使用不加密的HTTP/2協議

public void ConfigureServices(IServiceCollection services)
{
    // 允許使用不加密的HTTP/2協議
    AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        // 使用不加密的HTTP/2協議地址
        grpcClientFactoryOptions.Address = new Uri("http://localhost:5002");
    });

這樣就可以正常使用了。

image

在Kubernetes內網環境下我們可以考慮不使用證書的用法來去使用gRPC。

使用自簽名證書

我們使用了HTTPS證書,但是如果這個證書是自簽名的證書,那就意味着在操作系統層面,它不認爲這個證書是有效的,我們可以通過配置HttpClient讓它繞過證書的檢測。

通過IIS生成一個自簽名證書並且將它導出成.pfx

image

image

image

將它添加進來,並且設置"如果較新則複製"

image

appsettings.json中把自簽名證書配置進來。

{
    "Kestrel": {
        "Endpoints": {
            "Http": {
                "Url": "http://+:5000"
            },
            "Https": {
                "Url": "https://+:5001"
            },
            "Http2": {
                "Url": "http://+:5002",
                "Protocols": "Http2"
            }
        },
        "Certificates": {
            "Default": {
                "Path": "cer.pfx",
                "Password": "xxxxxxxxxxxxx"
            }
        }
    }
}

這時候,我們重新啓動並查看服務端項目。

接着我們去客戶端項目,將請求地址改回Https的5001

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    });
}

運行看看

fail: Grpc.Net.Client.Internal.GrpcCall[6]
      Error starting gRPC call.
System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.

確實報錯了,因爲SSL連接問題,肯定了,我們採用了一個不受信任的證書。

不用擔心,這個局當然可以破。

我們添加一些設置即可ConfigurePrimaryHttpMessageHandler

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<OrderGrpc.OrderGrpcClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    }).ConfigurePrimaryHttpMessageHandler(serviceProvider =>
    {
        var handler = new SocketsHttpHandler();
        // 允許無效或自簽名證書
        handler.SslOptions.RemoteCertificateValidationCallback = (a, b, c, d) => true;
        return handler;
    });
}

它的工作原理是在配置HttpMessageHandler的時候,最內部的那個HttpHandler是SocketsHttpHandler,同樣我們可以設置構造自己的SocketsHttpHandler注入進去,這裏將一個忽略證書錯誤的SocketsHttpHandler注入進去。

SslOptions.RemoteCertificateValidationCallback = (a, b, c, d) => true代表驗證證書時永遠返回true,不管證書有效沒效都認爲它是有效的。

這樣設置後,我們就可以使用自簽名證書來訪問gRPC了,這時候一切就正常了。

image

gRPC異常處理

在服務端項目中,我們可以增加一個異常攔截器GrpcExceptionInterceptor

/// <summary>
/// Grpc異常攔截器
/// </summary>
public class GrpcExceptionInterceptor : Interceptor
{
    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await base.UnaryServerHandler(request, context, continuation);
        }
        catch (System.Exception ex)
        {
            var metaData = new Metadata();
            metaData.Add("message", ex.Message);
            throw new RpcException(new Status(StatusCode.Unknown, "Unknown"), metaData);
        }
    }
}

怎麼使用它呢?

Startup.csConfigureServices方法中AddGrpc是添加進來。

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpc(grpcServiceOptions =>
    {
        // 生產環境需要將內部錯誤信息輸出關閉掉
        grpcServiceOptions.EnableDetailedErrors = false;
        // 添加一個異常攔截器
        grpcServiceOptions.Interceptors.Add<GrpcExceptionInterceptor>();
    });
}

OrderService模擬觸發一個異常

/// <summary>
/// 訂單服務(基於Grpc)
/// </summary>
public class OrderService : OrderGrpc.OrderGrpcBase
{
    /// <summary>
    /// 重寫創建訂單
    /// </summary>
    /// <param name="request"></param>
    /// <param name="context"></param>
    /// <returns></returns>
    public override Task<CreateOrderResult> CreateOrder(CreateOrderCommand request, ServerCallContext context)
    {
        throw new System.Exception("我是一個gRPC異常");

        // 可替換成真實的創建訂單服務的業務代碼
        return Task.FromResult(new CreateOrderResult { OrderId = 11 });
    }
}

先註釋掉異常攔截器,看看原始的異常是怎麼樣。

info: Grpc.Net.Client.Internal.GrpcCall[3]
      Call failed with gRPC error status. Status code: 'Unknown', Message: 'Exception was thrown by handler.'.

image

如果啓用異常攔截器之後呢?

推薦的做法是在服務端注入攔截器,將服務端的錯誤信息進行一定的封裝然後傳輸給客戶端。

image

image

image

info: Grpc.Net.Client.Internal.GrpcCall[3]
      Call failed with gRPC error status. Status code: 'Unknown', Message: 'Bad gRPC response. HTTP status code: 500'.

.Net上gRPC

gRPC是一種與語言無關的高性能遠程過程調用(RPC)框架。

gRPC的主要優點是:

  • 現代高性能輕量級RPC框架。
  • 協定優先API開發,默認使用協議緩衝區,允許與語言無關的實現。
  • 可用於多種語言的工具,以生成強類型服務器和客戶端。
  • 支持客戶端、服務器和雙向流式處理調用。
  • 使用Protobuf二進制序列化減少對網絡的使用。

這些優點使gRPC適用於:

  • 效率至關重要的輕量級微服務。
  • 需要多種語言用於開發的Polyglot系統。
  • 需要處理流式處理請求或響應的點對點實時服務。

.NET gRPC客戶端要求

Grpc.Net.Client包支持在.NET Core 3以及.NET 5或更高版本上通過HTTP/2進行gRPC調用。

.NET Framework上對gRPC over HTTP/2的支持有限。其他.NET版本(例如UWP、Xamarin和Unity)不具備所需的HTTP/2支持,必須改爲使用gRPC-Web。

.NET 實現 gRPC over HTTP/2 gRPC-Web
.NET 5 或更高版本 ✔️ ✔️
.NET Core 3 ✔️ ✔️
.NET Core 2.1 ✔️
.NET Framework 4.6.1 ⚠️† ✔️
Blazor WebAssembly ✔️
Mono 5.4 ✔️
Xamarin.iOS 10.14 ✔️
Xamarin.Android 8.0 ✔️
通用 Windows 平臺 10.0.16299 ✔️
Unity 2018.1 ✔️

Grpc核心工具包

  • Grpc.Tools,工程需要引入的Nuget包
  • dotnet-grpc,命令行插件工具

dotnet-grpc命令行工具

https://www.nuget.org/packages/dotnet-grpc

dotnet-grpc是一種.NET Core全局工具,用於在.NET gRPC項目中管理Protobuf(.proto)引用。該工具可以用於添加、刷新、刪除和列出Protobuf引用。

安裝它

dotnet tool install dotnet-grpc -g

image

常見命令

image

  • dotnet grpc add-file,將指定目錄下proto文件添加到工程裏面
  • dotnet grpc add-url,將一個Http的URL地址指定的proto文件添加到工程裏面
  • dotnet grpc remove,將添加的proto文件的引用移除,從工程中移除
  • dotnet grpc refresh,更新我們的proto文件,如果是遠程url引用的proto文件,那就意味着可以通過它來更新這個文件

proto文件最佳實踐

  • 使用單獨的Git倉庫管理Proto文件
  • 使用submodule將proto文件集成到工程目錄中
  • 使用dotnet-grpc命令行添加proto文件及相關依賴包引用

由proto生成的代碼文件位於obj目錄中,不會被git簽入到倉庫中。

使用dotnet-grpc命令行添加proto文件

我們先移除之前對客戶端項目的proto的引用。

image

在終端裏面,切換到客戶端項目目錄

cd F:\TempSpace\HelloRemoteCall\demoForGrpcClient\

image

通過命令行來講之前服務端項目的proto文件添加進來。

dotnet grpc add-file ..\demoForGrpcServer\Proto\order.proto

注意,筆者在這一步竟然折騰了半天,報錯了,好像是net 7預覽版有干擾,建議如果裝了預覽版的可以卸載試試。

image

這時候demoForGrpcClient.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>4a768401-255f-4d25-804e-ab6643ba275d</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="..\demoForGrpcServer\Proto\order.proto" Link="Protos\order.proto" />
  </ItemGroup>
</Project>

這裏會看到它會主動添加Grpc.AspNetCore的包,並且添加order.proto文件。

使用dotnet-grpc命令行添加proto地址

https://github.com/grpc/grpc/tree/master/examples/protos

這裏從github官方庫裏面選取一個地址,通過命令行方式來添加它

dotnet grpc add-url 'https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/helloworld.proto' -o Protos\helloworld.proto

image

image

它就真的按我們指定的輸出位置放置進來了哈。

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>4a768401-255f-4d25-804e-ab6643ba275d</UserSecretsId>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="..\demoForGrpcServer\Proto\order.proto" Link="Protos\order.proto" />
    <Protobuf Include="Protos\helloworld.proto">
      <SourceUrl>https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/helloworld.proto</SourceUrl>
    </Protobuf>
  </ItemGroup>
</Project>

我們看下helloworld.proto的定義

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// 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;
}

從定義看,這裏定義了一個Greeter服務,裏面有一個SayHello方法。

public void ConfigureServices(IServiceCollection services)
{
    services.AddGrpcClient<Greeter.GreeterClient>(grpcClientFactoryOptions =>
    {
        grpcClientFactoryOptions.Address = new Uri("https://localhost:5001");
    }).ConfigurePrimaryHttpMessageHandler(serviceProvider =>
    {
        var handler = new SocketsHttpHandler();
        // 允許無效或自簽名證書
        handler.SslOptions.RemoteCertificateValidationCallback = (a, b, c, d) => true;
        return handler;
    });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            GreeterClient service = context.RequestServices.GetService<GreeterClient>();
            try
            {
                HelloReply result = service.SayHello(new HelloRequest { Name = "abc" });
                await context.Response.WriteAsync(result.Message.ToString());
            }
            catch (Exception ex)
            {

            }
        }
    });
}

我們也在服務端進行引入。

dotnet grpc add-url 'https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/helloworld.proto' -o Protos\helloworld.proto

image

public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply()
        {
            Message = "Hello"
        });
    }
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>();
        endpoints.MapControllers();
    });
}

兩邊運行後,哈哈,順利拿到值

image

使用dotnet-grpc命令行更新和移除proto地址

這種通過URL方式引入進來的,我們還可以通過命令dotnet grpc refresh來更新它

dotnet grpc refresh 'https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/helloworld.proto'

image

如果要移除這個文件,可以使用dotnet grpc remove

dotnet grpc remove 'https://raw.githubusercontent.com/grpc/grpc/master/examples/protos/helloworld.proto'

image

image

但是這裏注意的是,remove只是移除了工程文件對這個proto的映射關係,並沒有移除本地這個proto文件,這個文件本身的移除,還是需要我們手工操作。

總結

有了dotnet-grpc命令行工具我們就可以很方便的向工程中添加proto文件,添加不同的服務的proto文件來生成我們的客戶端代碼,同樣我們也可以將它定義到我們的Server端。

將gRPC納入Swagger展示

依賴包

https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.Swagger

https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.JsonTranscoding

dotnet add package Microsoft.AspNetCore.Grpc.Swagger --prerelease

這個包暫時還處於預發佈狀態,需要添加--prerelease才能裝成功。

很遺憾,需要.Net 7纔可以。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddGrpc(grpcServiceOptions =>
    {
        // 生產環境需要將內部錯誤信息輸出關閉掉
        grpcServiceOptions.EnableDetailedErrors = false;
        // 添加一個異常攔截器
        grpcServiceOptions.Interceptors.Add<GrpcExceptionInterceptor>();
    }).AddJsonTranscoding();
    services.AddGrpcSwagger();
    services.AddApiVersions();
    services.AddSwaggers();
}

這裏需要在AddGrpc後面使用AddJsonTranscoding啓用gRPC的Json轉碼。

然後添加AddGrpcSwagger方法,註冊用於Grpc的Swagger相關服務。

使用Git Submodule方式管理proto文件

我們可以單獨把proto文件建立一個獨立的項目,這裏我們建立一個示例項目HelloGrpcProto

這裏面只存儲純的proto文件。

我們在當前項目的git中,將它以Submodule的方式加進來。

git submodule add https://github.com/TaylorShi/HelloGrpcProto

image

這時候,我們會看到根目錄就多了一個HelloGrpcProto,它包括了這個子項目的所有內容。

我們可以通過前面的dotnet grcp命令行工具來添加proto文件。

dotnet grpc add-file ..\HelloGrpcProto\Protos\order.proto

image

這樣我們就可以愉快的使用它了。

image

我們看到,這樣其實目錄裏面新增了一個.gitmodules文件,這裏面記錄了子模塊的地址

image

[submodule "HelloGrpcProto"]
	path = HelloGrpcProto
	url = https://github.com/TaylorShi/HelloGrpcProto

這樣有個好處,就是可以分開管理proto文件。

如果初次拉取項目的時候,也可以設置一起拉取,帶上--recurse-submodules參數就行。

git clone https://sssssssssssssssss.git --recurse-submodules

image

gRPC相關Nuget包

名稱 備註
Grpc.AspNetCore >= .NET 5.0;
>= .NET Core 3.0;
Grpc.AspNetCore.HealthChecks >= .NET 5.0;
>= .NET Core 3.0;
Grpc.AspNetCore.Web >= .NET 5.0;
>= .NET Core 3.0;
Grpc.AspNetCore.Server.Reflection >= .NET 5.0;
>= .NET Core 3.0;
Grpc.AspNetCore.Server >= .NET 5.0;
>= .NET Core 3.0;
Grpc.Tools
Grpc.Net.ClientFactory >= .NET 5.0;
>= .NET Standard 2.0;
Grpc.Net.Client >= .NET 5.0;
>= .NET Standard 2.0;
Grpc.Net.Common >= .NET 5.0;
>= .NET Standard 2.0;
Grpc.Net.Client.Web >= .NET 5.0;
>= .NET Standard 2.0;
Grpc.Core >= .NET Standard 1.5;
>= .NET Framework 4.5;
Grpc.Core.Api >= .NET Standard 1.5;
>= .NET Framework 4.6.2;
Grpc.Core.Xamarin >= .NET Standard 1.5;
>= .NET Framework 4.5;
Grpc.Core.NativeDebug
Grpc.HealthCheck >= .NET Standard 1.5;
>= .NET Framework 4.6.2;
Grpc.Auth >= .NET Standard 1.5;
>= .NET Framework 4.6.2;
Grpc.Reflection >= .NET Standard 1.5;
>= .NET Framework 4.6.2;
dotnet-grpc >= .NET Core 3.0;

HTTP狀態代碼

信息狀態代碼

信息狀態代碼反映臨時響應。客戶端應繼續使用同一請求並放棄響應

HTTP狀態代碼 HttpStatusCode
100 HttpStatusCode.Continue
101 HttpStatusCode.SwitchingProtocols
102 HttpStatusCode.Processing
103 HttpStatusCode.EarlyHints

成功狀態代碼

成功的狀態代碼指示客戶端的請求已被成功接收、理解和接受。

HTTP狀態代碼 HttpStatusCode
200 HttpStatusCode.OK
201 HttpStatusCode.Created
202 HttpStatusCode.Accepted
203 HttpStatusCode.NonAuthoritativeInformation
204 HttpStatusCode.NoContent
205 HttpStatusCode.ResetContent
206 HttpStatusCode.PartialContent
207 HttpStatusCode.MultiStatus
208 HttpStatusCode.AlreadyReported
226 HttpStatusCode.IMUsed

重定向狀態代碼

重定向狀態代碼要求用戶代理採取措施以完成請求。使用適當的標頭時,可以自動重定向

HTTP狀態代碼 HttpStatusCode
300 HttpStatusCode.MultipleChoices或HttpStatusCode.Ambiguous
301 HttpStatusCode.MovedPermanently或HttpStatusCode.Moved
302 HttpStatusCode.Found或HttpStatusCode.Redirect
303 HttpStatusCode.SeeOther或HttpStatusCode.RedirectMethod
304 HttpStatusCode.NotModified
305 HttpStatusCode.UseProxy
306 HttpStatusCode.Unused
307 HttpStatusCode.TemporaryRedirect或HttpStatusCode.RedirectKeepVerb
308 HttpStatusCode.PermanentRedirect

客戶端錯誤狀態代碼

客戶端錯誤狀態代碼指示客戶端的請求無效。

HTTP狀態代碼 HttpStatusCode
400 HttpStatusCode.BadRequest
401 HttpStatusCode.Unauthorized
402 HttpStatusCode.PaymentRequired
403 HttpStatusCode.Forbidden
404 HttpStatusCode.NotFound
405 HttpStatusCode.MethodNotAllowed
406 HttpStatusCode.NotAcceptable
407 HttpStatusCode.ProxyAuthenticationRequired
408 HttpStatusCode.RequestTimeout
409 HttpStatusCode.Conflict
410 HttpStatusCode.Gone
411 HttpStatusCode.LengthRequired
412 HttpStatusCode.PreconditionFailed
413 HttpStatusCode.RequestEntityTooLarge
414 HttpStatusCode.RequestUriTooLong
415 HttpStatusCode.UnsupportedMediaType
416 HttpStatusCode.RequestedRangeNotSatisfiable
417 HttpStatusCode.ExpectationFailed
418 我是茶壺🫖
421 HttpStatusCode.MisdirectedRequest
422 HttpStatusCode.UnprocessableEntity
423 HttpStatusCode.Locked
424 HttpStatusCode.FailedDependency
426 HttpStatusCode.UpgradeRequired
428 HttpStatusCode.PreconditionRequired
429 HttpStatusCode.TooManyRequests
431 HttpStatusCode.RequestHeaderFieldsTooLarge
451 HttpStatusCode.UnavailableForLegalReasons

服務器錯誤狀態代碼

服務器錯誤狀態代碼指示服務器遇到了阻止其完成請求的意外條件。

HTTP狀態代碼 HttpStatusCode
500 HttpStatusCode.InternalServerError
501 HttpStatusCode.NotImplemented
502 HttpStatusCode.BadGateway
503 HttpStatusCode.ServiceUnavailable
504 HttpStatusCode.GatewayTimeout
505 HttpStatusCode.HttpVersionNotSupported
506 HttpStatusCode.VariantAlsoNegotiates
507 HttpStatusCode.InsufficientStorage
508 HttpStatusCode.LoopDetected
510 HttpStatusCode.NotExtended
511 HttpStatusCode.NetworkAuthenticationRequired

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章