我又造了個輪子:GrpcGateway

来源:https://www.cnblogs.com/chenyishi/archive/2022/07/30/16530016.html
-Advertisement-
Play Games

我個人對GRPC是比較感興趣的,最近在玩通過前端調用GRPC。通過前端調用GRPC業界有兩種方式:GRPC Web和GRPC JSON轉碼。 GRPC Web 通過JS或者Blazor WASM調用GRPC,微軟在這方面做的還是很好的,從.NET Core3.0之後就提供了兩種實現GRPC Web的 ...


我個人對GRPC是比較感興趣的,最近在玩通過前端調用GRPC。通過前端調用GRPC業界有兩種方式:GRPC Web和GRPC JSON轉碼。

 

GRPC Web

通過JS或者Blazor WASM調用GRPC,微軟在這方面做的還是很好的,從.NET Core3.0之後就提供了兩種實現GRPC Web的方式(Grpc.AspNetCore.Web與Envoy)。我在之前的一篇里也寫過如何通過Blazor WASM調用GRPC Web。

GRPC JSON

通過Restful api調用一個代理服務,代理服務將數據轉發到GRPC Server就是GRPC JSON。微軟從.NET7開始也正式提供了GRPC JSON轉碼的方式。

為什麼要造輪子

既然有了GRPC Web與GRPC Json,那我為啥還要再造這麼一個輪子?

原因是有位同行看了如何通過Blazor WASM調用GRPC Web 這篇文章後,告訴我微信小程式目前沒辦法通過這種方式調用GRPC。我當時覺得很奇怪,微信小程式也屬於前端,為啥不能調用GRPC呢?

GRPC Web+小程式遇到的問題

只是聽說還不能確認,要自己試一試,於是我用GRPC Web的方式讓小程式調用GRPC,首先需要生成GRPC JS Client代碼:

protoc.exe -I=. test.proto --js_out=import_style=commonjs:.\grpcjs\ --plugin=protoc-gen-grpc=.\protoc-gen-grpc-web.exe --grpc-web_out=import_style=commonjs,mode=grpcwebtext:.\grpcjs\

然後將生成的代碼引入小程式端,發現確實有問題,微信小程式編譯後無法正常識別GRPC的namespace,會報以下錯誤:

proto is not defined

去查了下原因,應該是因為小程式目前不支持protobuf序列化。然後我通過一種取巧的方式手動在生成的GRPC JS中添加了proto變數

var proto = {}

再次嘗試,雖然proto能找到,但是又找不到其他對象,並且最主要的是GRPC JS Client是通過proto工具生成的,每次生成手動定義proto變數也不現實。

 

GRPC Web+小程式遇到問題總結:

  1. 小程式目前不支持protobuf序列化
  2. 手動修改生成的GRPC JS Client不友好

既然小程式通過GRPC Web方式調用GRPC失敗,那還有GRPC Json。

GRPC JSON+Envoy+小程式遇到的問題

我使用了Envoy來充當restful代理,調用GRPC。我在之前有一篇通過Envoy JSON代理GRPC的帖子。按這個帖子來了一遍。

計劃通過docker-compose方式運行GRPC Server和Envoy代理。

既然用GRPC,那肯定用http2/http2,在docker里運行.net core必然需要證書,沒有證書就自己搞一個自簽證書。

openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.cer
openssl pkcs12 -export -in server.cer -inkey server.key -out server.pfx

證書有了,在GRPC里配置https

    builder.WebHost.ConfigureKestrel(o =>
    {

        o.ListenAnyIP(1111, p =>
        {
            p.Protocols = HttpProtocols.Http2;
            p.UseHttps("/app/server.pfx", "123456");
        });
    });

然後就開始配置envoy

首先生成grpc proto描述符

protoc.exe -I=.  --descriptor_set_out=.\test.pb --include_imports .\test.proto  --proto_path=.

然後定義envoy配置文件

admin:
  address:
    socket_address: {address: 0.0.0.0, port_value: 9901}

static_resources:
  listeners:
  - name: listener1
    address:
      socket_address: {address: 0.0.0.0, port_value: 10000}
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: grpc_json
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: {prefix: "/test"}
                route: 
                 cluster: grpc
          http_filters:
          - name: envoy.filters.http.grpc_json_transcoder
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
              proto_descriptor: "/etc/envoy/test.pb"
              services: ["test"]
              print_options:
                add_whitespace: true
                always_print_primitive_fields: true
                always_print_enums_as_ints: false
                preserve_proto_field_names: false
              auto_mapping: true
          - name: envoy.filters.http.router
            typed_config:
             "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: grpc
    type: static
    connect_timeout: 15s
    lb_policy: ROUND_ROBIN
    dns_lookup_family: V4_ONLY
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}
    load_assignment:
      cluster_name: grpc
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 某ip
                port_value: 1111

下麵就定義envoy的dockerfile,主要是信任自簽證書

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM envoyproxy/envoy-dev:e834c24e061b710348ffd72016d5d1069698b4ff

COPY ["server.crt","/usr/local/share/ca-certificates/"]

RUN ["update-ca-certificates"]

最後就是定義docker-compsoe.yaml

version: '3.4'

services:
  myenvoy:
    image: myenvoy
    container_name: myenvoy
    command: "-c /etc/envoy/envoy.yaml  --log-level debug"
    build:
      context: .
      dockerfile: GrpcServer/DockerfileEnvoy
    volumes:
     - "grpcpbs/:/etc/envoy/"
     - "grpcpbs/logs:/logs"
    ports:
      - "9901:9901"
      - "10000:10000"
    depends_on:
      - grpcserver
    networks:
    - mynetwork
  grpcserver:
    image: grpcserver
    container_name: grpcserver
    networks:
    - mynetwork
    build:
      context: .
      dockerfile: GrpcServer/Dockerfile
    ports:
      - "1111:1111"

networks:
  mynetwork:

最後通過docker-compsoe up -d運行,但是postman調用的時候,envoy與grpcserver的通信連接成功了,但是數據傳輸時總是被 connection reset,去github上找原因也沒找到。至此grpc json+envoy又失敗了。

GRPC JSON+Envoy+小程式遇到問題總結:

  1. 數據傳輸時connection 被莫名reset

既然envoy走不通不行,那就自己造一個吧。

開始造輪子

GRPC JSON的形式,原理就是通過一個web api接收restful請求,將請求數據轉發到GRPC Server。

首先創建一個web api命名為GrpcGateway,並引入proto文件,生成grpc client代碼

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.20.1" />
    <PackageReference Include="Grpc.Net.Client" Version="2.46.0" />
    <PackageReference Include="Grpc.Tools" Version="2.46.1">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
  </ItemGroup>
  <ItemGroup>
    <Protobuf Include="..\*.proto" GrpcServices="Client" />
  </ItemGroup>

然後創建一個控制器去接受restful請求,而grpc client可採用反射來創建。

 [ApiController]
    [Route("[controller]")]
    public class ProcessGrpcRequestController : ControllerBase
    {
        private readonly ILogger<ProcessGrpcRequestController> _logger;
        private readonly Func<string, ClientBase> _getGrpcClient;
        public ProcessGrpcRequestController(ILogger<ProcessGrpcRequestController> logger, Func<string, ClientBase> getGrpcClient)
        {
            _logger = logger;
            _getGrpcClient = getGrpcClient;
        }

        /// <summary>
        /// 調用grpc
        /// </summary>
        /// <param name="serviceName">Grpc Service Name 從proto文件中查詢</param>
        /// <param name="method">Grpc Method Name 從proto文件中查詢</param>
        /// <returns></returns>
        [HttpPost("serviceName/{serviceName}/method/{method}")]
        public async Task<IActionResult> ProcessAsync(string serviceName, string method)
        {
            try
            {
                if (string.IsNullOrEmpty(serviceName))
                {
                    return BadRequest("serviceName不能為空");
                }
                if (string.IsNullOrEmpty(method))
                {
                    return BadRequest("method不能為空");
                }

                using var sr = new StreamReader(Request.Body, leaveOpen: true, encoding: Encoding.UTF8);
                var paramJson = await sr.ReadToEndAsync();
                if (string.IsNullOrEmpty(paramJson))
                {
                    return BadRequest("參數不能為空");
                }
                var client = _getGrpcClient(serviceName);
                if (client == null)
                {
                    return NotFound();
                }

                Type t = client.GetType();
                var processMethod = t.GetMethods().Where(e => e.Name == method).FirstOrDefault();
                if (processMethod == null)
                {
                    return NotFound();
                }

                var parameters = processMethod.GetParameters();
                if (parameters == null)
                {
                    return NotFound();
                }

                var param = JsonConvert.DeserializeObject(paramJson, parameters[0].ParameterType);
                if (param == null)
                {
                    return BadRequest("參數不能為空");
                }
                var pt = param.GetType();
                var headers = new Metadata();

                if (Request.Headers.Keys.Contains("Authorization"))
                {
                    headers.Add("Authorization", Request.Headers["Authorization"]);
                }

                var result = processMethod.Invoke(client, new object[] { param, headers, null, null });

                return Ok(result);
            }
            catch(Exception ex) when (
            ex.InnerException !=null && ex.InnerException !=null && ex.InnerException is RpcException && 
            ((ex.InnerException as RpcException).StatusCode == Grpc.Core.StatusCode.Unauthenticated || 
            ((ex.InnerException as RpcException).StatusCode == Grpc.Core.StatusCode.PermissionDenied)))
            {
                _logger.LogError(ex, ex.ToString());
                return Unauthorized();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, ex.Message);
                return BadRequest(ex.ToString());
            }


        }
    }

然後註入動態反射創建grpc client的方法

            services.AddScoped(p => {
                Func<string, ClientBase> func = serviceName =>
                {
                    var channel = GrpcChannel.ForAddress(grpcServerAddress);

                    var parentClassName = $"{serviceName}";

                    var assembly = Assembly.Load("你的dll名字");
                    var parentType = assembly.GetType(parentClassName);

                    var clientType=  parentType.GetNestedType($"{serviceName}Client");
                    if (clientType == null)
                    {
                        throw new Exception($"serviceName:{serviceName}不存在");
                    }
                    var client = Activator.CreateInstance(clientType, new object[] { channel });
                    return (ClientBase)client;
                };
                return func;
            });

然後定義grpc gateway dockerfile ,最主要需要信任證書

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 16666

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["MyGateway/MyGateway.csproj", "MyGateway/"]
COPY . .
WORKDIR "/src/MyGateway"

FROM build AS publish
RUN dotnet publish "MyGateway.csproj" -c Release -o /app/publish

FROM base AS final
COPY ["server.crt","/usr/local/share/ca-certificates/"]
RUN ["update-ca-certificates"]
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyGateway.dll"]

 

最後通過定義docker-compose

version: '3.4'

services:
  mygateway:
    image: mygateway
    container_name: mygateway
    networks:
    - mynetwork
    build:
      context: .
      dockerfile: MyGateway/Dockerfile
    ports:
      - "2222:2222"
  grpcserver:
    image: grpcserver
    container_name: grpcserver
    networks:
    - mynetwork
    build:
      context: .
      dockerfile: GrpcServer/Dockerfile
    ports:
      - "1111:1111"

networks:
  mynetwork:

通過docker-compsoe up -d 啟動

通過postman調用,看到200狀態碼,終於成功了,最後試了下小程式也能通過這種方式調用後端GRPC了,整個人都舒服了...

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言:最近幾個月很忙,都沒有時間寫文章了,今天周末剛好忙完下班相對早點(20:00下班)就在家把之前想總結的知識點寫出來,於是就有了這篇文章。雖無很高深的技術,但小技巧有大用處。 有時我們經常需要將實現了某個基類或某個介面的所有Bean進行分類管理,在需要用到的時候按需獲取實現了某個基類或某個介面的 ...
  • 最近學習了es的視頻,感覺這個產品對於查詢來說非常方便,但是如何應用到我們自己的 產品中來呢。因為我們的產品數據更新太快,其實不太適合用es做主力存儲。並且我們的業務還沒有到那種巨量級別,產品的伺服器容量也有限,所以我打算根據es的倒排索引的原理,自己寫一個查詢的組件。 我的理解是這樣的,有大量的文 ...
  • 終止線程的執行 一、強制終止線程的執行 強制終止用的是stop()方法,因為這種方法會丟失數據,所以一般不採用這種方法。 原理是直接殺死線程,這樣的話線程中沒有保存的數據就會丟失 /* 在java中強制終止一個線程 */ public class ThreaTest09 { public stati ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • 獲取線程對象的名稱 1.獲取當前線程對象 //調用當前線程對象,currentThread()這方法出現在main()方法中,當前線程就是主線程 //這代碼出現在哪,就是獲取到哪個線程對象 Thread t=Thread.currentCurrent() 2.獲取線程對象的名字 String nam ...
  • 據我理解,標簽是為了使得jsp內容更加簡潔,編寫起來更加方便,不用再去在jsp里寫大段的java代碼,標簽就顯得與html格外契合;當然,並不是一定要使用標簽,只用jsp的語法來寫java代碼也可以實現功能,只是稍顯繁瑣。 言歸正傳,我們在使用標簽之前首先需要導入兩個依賴 <dependency> ...
  • 作者:Steven Giesel 翻譯:Alan Wang 校對:李衛涵 – 微軟 MVP 排版:Rani Sun 有什麼比參考包含分步說明和代碼示例的動手教程更好的學習新技術的方式呢?當你完成或 fork 本教程後,你將得到這樣一個應用程式: Steven Giesel 最近發佈了一個由5部分內容 ...
  • 自從2021年2月第20輪公佈的測試以後,一年半後 的2022年7月19日 發佈了 TechEmpower 21輪測試報告:Round 21 results - TechEmpower Framework Benchmarks。Techempower benchmark是包含範圍最廣泛的web框架性 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...