美文网首页
grep-web相关整理

grep-web相关整理

作者: sherry_718 | 来源:发表于2020-05-07 09:32 被阅读0次

    目的

    为了让浏览器可以支持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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>gRPC-Web Example</title>
    <script src="./dist/main.js"></script>
    </head>
    <body>
      <p>Open up the developer console and see the logs for the output.</p>
    </body>
    </html>
    

    其中使用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相关整理

          本文链接:https://www.haomeiwen.com/subject/jtjtghtx.html