本文章同時發佈於:
文章為自己的經驗與夥伴整理的內容,設計沒有標準答案,如有可以改進的地方,請告訴我,我會盡我所能的修改,謝謝大家~
大家好,這次要介紹 gRPC-Web 的實作,大家可以直接 Clone 此份Github 程式碼,直接先 run 起來會比較有感覺~
怎麼 run 起來?
docker run -v ${PWD}:/app -w /app node:14.3.0-alpine npm install
: 安裝web-cleint
的相依docker run -v ${PWD}:/app -w /app node:14.3.0-alpine npm run build
: 編譯web-client
的client.js
docker-compose build
: 安裝Golang GRPC Server
與Envoy
的相依docker-compose up
: 啟動web-client
,Golang GRPC Server
,Envoy
- 連入
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;
}
謝謝你的閱讀~