gRPC - Using gRPC in Web Browser

  • 创建 ptoto 原型定义
syntax = "proto3";

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;
}
  • 构建服务端
/*
 *
 * Copyright 2015 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

var PROTO_PATH = __dirname + '/helloworld.proto';

var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

/**
 * Implements the SayHello RPC method.
 */
function sayHello(call, callback) {
  console.log(`call sayHello at ${Date.now()}`);
  callback(null, {message: 'Hello ' + call.request.name});
}

/**
 * Starts an RPC server that receives requests for the Greeter service at the
 * sample server port
 */
function main() {
  var server = new grpc.Server();
  server.addService(hello_proto.Greeter.service, {sayHello: sayHello});
  server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
    server.start();
  });
}

main();
  • 构建客户端
/*
 *
 * Copyright 2015 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

var PROTO_PATH = __dirname + '/helloworld.proto';

var parseArgs = require('minimist');
var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function main() {
  var argv = parseArgs(process.argv.slice(2), {
    string: 'target'
  });
  var target;
  if (argv.target) {
    target = argv.target;
  } else {
    target = 'localhost:50051';
  }
  var client = new hello_proto.Greeter(target,
                                       grpc.credentials.createInsecure());
  var user;
  if (argv._.length > 0) {
    user = argv._[0]; 
  } else {
    user = '[client name]';
  }
  client.sayHello({name: user}, function(err, response) {
    console.log('Greeting:', response.message);
  });
}

main();
  • 初始化
npm init
npm i @grpc/grpc-js @grpc/proto-loader minimist
{
  "name": "grpc-demo",
  "version": "1.0.0",
  "description": "",
  "main": "greeter_client.js",
  "scripts": {
    "start": "node greeter_server.js",
    "test": "node greeter_client.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@grpc/grpc-js": "^1.3.0",
    "@grpc/proto-loader": "^0.6.1",
    "minimist": "^1.2.5"
  }
}
  • 测试 - 启动服务端
> npm start

> grpc-demo@1.0.0 start C:\Workspace\grpc-demo
> node greeter_server.js
  • 测试 - 启动客户端
> npm test

> grpc-demo@1.0.0 test C:\Workspace\grpc-demo
> node greeter_client.js

Greeting: Hello [client name]

服务工作正常

  • 配置 Envoy

浏览器上的 gRPC 应用依赖于一个协议代理,这里选择了主流的 Envoy

admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9901 }

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    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
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: greeter_service
              cors:
                allow_origin_string_match:
                - prefix: "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                max_age: "1728000"
                expose_headers: custom-header-1,grpc-status,grpc-message
          http_filters:
          - name: envoy.filters.http.grpc_web
          - name: envoy.filters.http.cors
          - name: envoy.filters.http.router
  clusters:
  - name: greeter_service
    connect_timeout: 0.25s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
    load_assignment:
      cluster_name: cluster_0
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: host.docker.internal
                    port_value: 50051
  • 启动 Envoy
$ docker pull getenvoy/envoy:stable
$ docker run --rm -it -v "C:\grpc-demo:/config" -p 9901:9901 -p 8080:8080 -p 50051:50051 'getenvoy/envoy:stable' -c '/config/envoy.yaml'
  • 调用 gRPC

生成 js 文件

$ protoc -I=. --js_out=import_style=commonjs:./ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./ helloworld.proto
$ ls
helloworld_pb.js
helloworld_grpc_web_pb.js
import { GreeterClient } from './helloworld_grpc_web_pb';
import { HelloRequest } from './helloworld_pb';

console.log('****** GRPC CLIENT ******');

const client = new GreeterClient('http://localhost:8080');
const metadata = { "Content-Type": "application/grpc-web+proto" };

const req = new HelloRequest();

export function webSayHello(callback) {
  req.setName("[GRPC CLIENT]");
  client.sayHello(req, metadata, (err, ret) => {
    console.log("****** GRPC RESPONSE ******");
    console.log(err, ret);
    if (!err) {
      var msg = ret.getMessage();
      callback(msg);
    }
  });
}

webSayHello((x) => console.log(x));