grep-web相关整理

目的

为了让浏览器可以支持grpc的调用,以此记录相关研究过程,共勉

grpc-web搭建

此处使用官网helloworld例子进行说明,github入口

服务端

  1. 使用protocol buffers定义回声服务原型,命名为helloworld.proto
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply);
  rpc SayRepeatHello(RepeatHelloRequest) returns (stream HelloReply);
  rpc SayHelloAfterDelay(HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message RepeatHelloRequest {
  string name = 1;
  int32 count = 2;
}

message HelloReply {
  string message = 1;
}
  1. 编写服务器代码,例子中使用的是NodeJS
var PROTO_PATH = __dirname + '/helloworld.proto';

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

/**
 * @param {!Object} call
 * @param {function():?} callback
 */
function doSayHello(call, callback) {
  callback(null, {message: 'Hello! '+ call.request.name});
}

/**
 * @param {!Object} call
 */
function doSayRepeatHello(call) {
  var senders = [];
  function sender(name) {
    return (callback) => {
      call.write({
        message: 'Hey! ' + name
      });
      _.delay(callback, 500); // in ms
    };
  }
  for (var i = 0; i < call.request.count; i++) {
    senders[i] = sender(call.request.name + i);
  }
  async.series(senders, () => {
    call.end();
  });
}

/**
 * @param {!Object} call
 * @param {function():?} callback
 */
function doSayHelloAfterDelay(call, callback) {
  function dummy() {
    return (cb) => {
      _.delay(cb, 5000);
    };
  }
  async.series([dummy()], () => {
    callback(null, {
      message: 'Hello! '+call.request.name
    });
  });
}

/**
 * @return {!Object} gRPC server
 */
function getServer() {
  var server = new grpc.Server();
  server.addService(helloworld.Greeter.service, {
    sayHello: doSayHello,
    sayRepeatHello: doSayRepeatHello,
    sayHelloAfterDelay: doSayHelloAfterDelay
  });
  return server;
}

if (require.main === module) {
  var server = getServer();
  server.bind('0.0.0.0:9090', grpc.ServerCredentials.createInsecure());
  server.start();
}

exports.getServer = getServer;
  1. 配置Envoy代理将浏览器的gRPC Web请求转发到后端。命名为envoy.yaml。例子中envoy监听端口8080,将任何gRPC Web请求转发到端口9090的群集。
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.http_connection_manager
        config:
          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
                  max_grpc_timeout: 0s
              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.grpc_web
          - name: envoy.cors
          - name: envoy.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
    hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
    # 以下是为了满足https请求,http不需要添加
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext

注意:

  • 如果docker是运行在Mac/Windows,socket_address需要更改地址为host.docker.internal
hosts: [{ socket_address: { address: host.docker.internal, port_value: 9090 }}]
  • 如果是https请求报以下错误
upstream connect error or disconnect/reset before headers. reset reason: connection failure

是因为需要加上transport_socket

  1. 创建Dockerfile,为之后运行envoy 。命名为envoy.Dockerfile
    注意以下,官网中是envoyproxy/envoy:last 版本可修改,参考
FROM envoyproxy/envoy:v1.14.1
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
  1. 使用envoy的二进制文件构建镜像,使用docker命令
$ docker build -t helloworld/envoy -f ./envoy.Dockerfile .
$ docker run -d -p 8080:8080 -p 9901:9901 --network=host helloworld/envoy

如果docker是部署在Mac/Windows,命令中去掉 --network=host option:

$ docker run -d -p 8080:8080 -p 9901:9901 helloworld/envoy

docker安装:确保您已经安装了最新版本的 docker、docker-compose 和 docker-machine。
安装这些软件最简单的方式是使用 Docker Toolbox。

  1. 启动服务器
$ node server.js &

服务监听端口9090,需要注意的nodejs运行需要安装模块,package.json定义在后面说明

客户端

  1. 客户端请求代码,命名client.js
const {HelloRequest, RepeatHelloRequest,
       HelloReply} = require('./helloworld_pb.js');
const {GreeterClient} = require('./helloworld_grpc_web_pb.js');

var client = new GreeterClient('http://' + window.location.hostname + ':8080',
                               null, null);

// simple unary call
var request = new HelloRequest();
request.setName('World');

client.sayHello(request, {}, (err, response) => {
  console.log(response);
  //console.log(response.getMessage());
});


// server streaming call
var streamRequest = new RepeatHelloRequest();
streamRequest.setName('World');
streamRequest.setCount(5);

var stream = client.sayRepeatHello(streamRequest, {});
stream.on('data', (response) => {
  console.log(response);
  //console.log(response.getMessage());
});
  

// deadline exceeded
var deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 1);

client.sayHelloAfterDelay(request, {deadline: deadline.getTime()},
  (err, response) => {
    console.log('Got error, code = ' + err.code +
                ', message = ' + err.message);
  });
  1. 安装模块,定义package.json。同时安装server.js和client.js模块
{
  "name": "grpc-web-simple-example",
  "version": "0.1.0",
  "description": "gRPC-Web simple example",
  "devDependencies": {
    "@grpc/proto-loader": "^0.3.0",
    "google-protobuf": "^3.6.1",
    "grpc": "^1.15.0",
    "grpc-web": "^1.0.0",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0"
  }
}
  1. 定义html文件,命名为index.html




gRPC-Web Example



  

Open up the developer console and see the logs for the output.

其中使用webpack打包将生成/dist/main.js文件

  1. 使用protoc命令行工具生成CommonJS客户端代码
$ protoc -I=$DIR helloworld.proto --js_out=import_style=commonjs:$OUT_DIR

以上命令生成pb.js文件,此文件主要用于发送request,包含request相关函数。需要下载 proto 工具。

$ protoc -I=$DIR helloworld.proto --grpc-web_out=import_style=commonjs,mode=grpcwebtext:$OUT_DIR

以上命令生成web_pb.js文件,此文件主要用于获取response,包含response相关函数,同时包含请求地址的设置 。需要 protoc plugin

  1. 打包
$ npm install
$ npx webpack client.js
  1. 运行网站
#python2
$ python2 -m SimpleHTTPServer 8081 &
#python3
$ python3 -m http.server 8081 &

访问地址localhost:8081,打开开发者工具,会看到打印出Hello! World。成功~

grpc-web结合react

服务端按着以上方式启动。客户端只需要把pb.js以及web_pb.js放入react项目中,编写client.js文件。但是当run的时候会报错不成功。这是因为react默认配置了eslint,会检测出pb.js文件的部分变量undefined。知道问题的原因,是不是修改.eslintrc 规则,把未定义的变量加入globals就成功,却发现怎么修改eslint的配置都没有生效,查阅很多反馈,发现官网有说明:

可以看到的是,我们即使配置了 .eslintrc 规则,也只会影响到我们浏览器对于 eslint 规则的运用,无法在编译调试的过程中,对代码进行规范。
必须要用默认的配置,除非修改node_modules 内部,但是对小组开发并不友好,所以需要找到不修改 .eslintrc 能成功的办法。
发现可以定义到window中,默认 Windows 是一个全局变量,而第三方框架的全局变量肯定是会挂在到 Windows 对象上去的

问题参考链接
https://www.jianshu.com/p/7fec779528a6
https://create-react-app.dev/docs/using-global-variables/

在pb.js文件中添加

const proto = window.proto;
const COMPILED = window.COMPILED;

重新启动运行,成功~

相关概念

  1. rpc
    RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样

  2. grpc
    gRPC 是一个高性能、开源和通用的 RPC 框架,面向服务端和移动端,支持多语言,基于 HTTP/2 设计。
    特点:

    • 基于 IDL 文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub;
    • 通信协议基于标准的 HTTP/2 设计,支持双向流、消息头压缩、单 TCP的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;
    • 序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架,基于 HTTP/2 + PB, 保障了 RPC 调用的高性能。
  3. protocol buffers
    gRPC默认使用protocl buffers,protoc buffers 是谷歌成熟的开源的用于结构化数据序列化的机制,需要 protoc 编译工具

  4. grpc-web
    gRPC-Web是一个JavaScript客户端库,使Web应用程序能够直接与后端gRPC服务通信

  5. CommonJS
    CommonJS 是以在浏览器环境之外构建 javaScript 生态系统为目标而产生的写一套规范,主要是为了解决 javaScript 的作用域问题而定义的模块形式,可以使每个模块它自身的命名空间中执行,该规范的主要内容是,模块必须通过 module.exports 导出对外的变量或者接口,通过 require() 来导入其他模块的输出到当前模块的作用域中。CommonJS模块基本上包括两个基础的部分:一个取名为exports的自由变量,它包含模块希望提供给其他模块的对象,以及模块所需要的可以用来引入和导出其它模块的函数。

  6. http2
    2015年,HTTP/2 发布。HTTP/2是现行HTTP协议(HTTP/1.x)的替代,但它不是重写,HTTP方法/状态码/语义都与HTTP/1.x一样。HTTP/2基于SPDY,专注于性能,最大的一个目标是在用户和网站间只用一个连接(connection)。

  7. webpack
    webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

  8. envoy
    网络代理

  9. docker
    一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

参考链接

1. https://github.com/grpc/grpc-web
2. https://www.envoyproxy.io/
3. https://grpc.io/docs/tutorials/basic/web/

你可能感兴趣的:(grep-web相关整理)