gRPC-Web,讓你的前端也吃到 gRPC 的惡魔果實 - 實作篇

本文章同時發佈於:

文章為自己的經驗與夥伴整理的內容,設計沒有標準答案,如有可以改進的地方,請告訴我,我會盡我所能的修改,謝謝大家~

大家好,這次要介紹 gRPC-Web 的實作,大家可以直接 Clone 此份Github 程式碼,直接先 run 起來會比較有感覺~

怎麼 run 起來?

  1. docker run -v ${PWD}:/app -w /app node:14.3.0-alpine npm install: 安裝web-cleint的相依
  2. docker run -v ${PWD}:/app -w /app node:14.3.0-alpine npm run build: 編譯web-clientclient.js
  3. docker-compose build: 安裝Golang GRPC ServerEnvoy的相依
  4. docker-compose up: 啟動web-client, Golang GRPC Server, Envoy
  5. 連入localhost:8060,打開console會看到web-cleint發出的Hello得到Hello World的回傳

envoy 設定

整體架構如 DAY11 所說的,後端與前端之間多了一個 envoy proxy,來把瀏覽器的 HTTP1.1 request 轉換成 HTTP2,

圖片來源: Envoy and gRPC-Web: a fresh new alternative to REST

所以,我們要在 docker-compose 設定前端、後端、envoy proxy 三者,

# docker-compose.yml
version: "3"
services:
  envoy:
    build:
      context: ./
      dockerfile: ./docker/envoy/Dockerfile
    image: grpcweb/envoy
    ports:
      - "8080:8080"
      - "9901:9901"
    links:
      - server
  server:
    image: golang:1.14.6-alpine
    volumes:
      - ${PWD}:/server
    working_dir: /server
    ports:
      - "8070:8070"
    entrypoint: go run server/main.go
  web:
    image: httpd:2.4
    volumes:
      - ${PWD}/web-client:/usr/local/apache2/htdocs/
    ports:
      - "8060:80"
    links:
      - envoy

注意dockerfile: ./docker/envoy/Dockerfile處,我們必須 build 一個 envoy docker image,目標是把envoy.yaml放入 image 中,如下:

# docker/envoy/Dockerfile
FROM envoyproxy/envoy:v1.15.0

COPY ./envoy.yaml /etc/envoy/envoy.yaml

CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l trace --log-path /tmp/envoy_info.log

而 envoy 會依照此 config 來設定 proxy,

# envoy.yaml
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:
        # 講解1
        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.config.filter.network.http_connection_manager.v2.HttpConnectionManager
                codec_type: auto
                stat_prefix: ingress_http
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      # 講解2
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: echo_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:
                  # 講解4
                  - name: envoy.filters.http.grpc_web
                  - name: envoy.filters.http.cors
                  - name: envoy.filters.http.router
  clusters:
    - name: echo_service
      connect_timeout: 0.25s
      type: logical_dns
      http2_protocol_options: {}
      lb_policy: round_robin
      load_assignment:
        cluster_name: cluster_0
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    # 講解3
                    socket_address:
                      address: server
                      port_value: 50052

講解1處,為 envoy 對外的 port 號,request 依照此 port 號進入後,可以依照講解2的 routes 來設定路由,如"/"導到了clusters裡的echo_service,而echo_service裡就可以實際將 requests 導到 docker-compose 中的 server,而 port 號是 50052。

講解4處,因為有設定envoy.filters.http.grpc_web,所以對 HTTP1.1 流量會轉換成 HTTP2 的流量。

來產生 JS 與 Golanag 的 gRPC code

在 script 資料夾中,我已經寫好了 JS 與 Golang 產 gRPC code 的 script,分別如下:

# scirpt/build-proto-js.sh
#! /bin/bash

docker run \
    -v "${PWD}/proto:/protofile" \
    -e "protofile=helloworld.proto" \
    juanjodiaz/grpc-web-generator

mv ./proto/generated/* ./proto

rm -r proto/generated
# scirpt/build-proto-go.sh
docker run \
    -v "${PWD}/proto:/protofile" \
    -e "protofile=helloworld.proto" \
    juanjodiaz/grpc-web-generator

mv ./proto/generated/* ./proto

rm -r proto/generated

主要是透過 juanjodiaz 大大寫的juanjodiaz/grpc-web-generator來產生 gRPC code,省去了很多 gRPC 安裝的時間 XD。

run 完之後就會產生以下的 code,我們要 import 他們到我們實際的 code 中。

// web-client/client.js

const { HelloRequest } = require("../proto/helloworld_pb.js");
const { GreeterPromiseClient } = require("../proto/helloworld_grpc_web_pb.js");

(async () => {
  try {
    const greeterService = new GreeterPromiseClient("http://localhost:8080");
    const request = new HelloRequest();
    request.setMessage("Hello");

    const response = await greeterService.sayHello(request, {});
    console.log(response.getMessage());
  } catch (err) {
    console.log(err.code);
    console.log(err.message);
  }
})();

前端中,我們利用GreeterPromiseClient來產生 gRPC client,並產生HelloRequest這個 message,使用setMessage將裡頭的 value 設定好,並透過sayHello傳送。

可以看到,sayHello使用HelloRequest是完全符合 gRPC 的 schema 的,前後端的定義都來自於 schema,溝通更 match 惹~

# proto/helloworld.proto
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string message = 1;
}

message HelloReply {
  string message = 1;
}

謝謝你的閱讀~

參考

comments powered by Disqus