我個人對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,首先需要生成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+小程式遇到問題總結:
既然小程式通過GRPC Web方式呼叫GRPC失敗,那還有GRPC Json。
我使用了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+小程式遇到問題總結:
既然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了,整個人都舒服了...