作者:Venil Noronha
譯者:王全根
原文:venilnoronha.io/seamless-cl…
gRPC-Web使Web應用能夠通過類似於Envoy的代理訪問gRPC後端。Envoy是Istio的預設代理,因此,我們可以利用Istio的EnvoyFilter構件來建立無縫連線的雲原生應用。
介紹
在這篇文章中,我將引導你構建一個簡單的Web應用,使用emoji替換使用者輸入文字中的關鍵字,並使用gRPC-Web和Istio與gRPC後端進行通訊。
以下是我們建立emoji應用的步驟大綱:
使用Protobuf定義協議格式;
編譯Protobuf定義檔案,來生成Go和JavaScript檔案;
構建並測試基於Go的gRPC服務,該服務使用emoji替換輸入文字中的關鍵字;
使用gRPC-Web為emoji服務建立Web介面;
配置EnvoyFilter並通過Istio部署後端;
部署Web應用程式並測試我們的emoji服務。
架構
讓我們進一步理解emoji服務的最終架構是什麼樣子。
簡而言之,只要使用者提供一些文字,Web應用就會利用gRPC-Web庫向Istio Gatway傳送HTTP請求。然後,Istio閘道器將HTTP請求路由到emoji服務旁執行的Proxy sidecar,後者使用Envoy的gRPC-Web filter將HTTP呼叫轉換成gRPC呼叫。
定義協議格式
首先,讓我們使用Protobuf定義協議格式。
syntax = "proto3";
package emoji;
service EmojiService {
rpc Emojize (EmojizeRequest) returns (EmojizeReply);
}
message EmojizeRequest {
string text = 1;
}
message EmojizeReply {
string emojized_text = 1;
}複製程式碼
我們定義一個名為EmojiService
的service
,處理名為Emojize
的rpc
呼叫,該呼叫接受EmojizeRequest
物件引數並返回一個EmojizeReply
例項。
EmojizeRequest
訊息引數包含一個名為text
的string
型別的欄位,表示使用者輸入的文字。同樣,EmojizeReply
包含一個名為emojized_text
的string
型別的欄位,表示最終輸出的字元,也即服務端將emoji關鍵字替換為emoji表情符號的輸出內容。
編譯Protobuf定義檔案
我們先建立一個名為grpc-web-emoji/emoji/
的專案目錄結構,然後把前面的定義內容寫入名為emoji.proto
的檔案。
然後編譯emoji.proto檔案並生成所需要的Go檔案。
$ protoc -I emoji/ emoji/emoji.proto --go_out=plugins=grpc:emoji複製程式碼
同樣,我們也生成JavaScript檔案。
$ protoc -I emoji/ emoji/emoji.proto --js_out=import_style=commonjs:emoji \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:emoji複製程式碼
此時,您將獲得如下所示的目錄結構。
── grpc-web-emoji
└── emoji
├── emoji.pb.go
├── emoji.proto
├── emoji_grpc_web_pb.js
└── emoji_pb.js複製程式碼
構建和測試Go後端程式
現在讓我們建立一個實現EmojiService
API的Go程式。為此,我們使用以下內容建立一個名為main.go
的檔案。
package main
import (
"context"
"log"
"net"
proto "github.com/venilnoronha/grpc-web-emoji/emoji"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
emoji "gopkg.in/kyokomi/emoji.v1"
)
// server is used to implement the EmojiService interface
type server struct{}
// Emojize takes a input string via EmojizeRequest, replaces known keywords with
// actual emoji characters and returns it via a EmojizeReply instance.
func (s *server) Emojize(c context.Context, r *proto.EmojizeRequest)
(*proto.EmojizeReply, error) {
return &proto.EmojizeReply{EmojizedText: emoji.Sprint(r.Text)}, nil
}
func main() {
// listen to TCP requests over port 9000
lis, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("listening on %s", lis.Addr())
// register the EmojiService implementation with the gRPC server
s := grpc.NewServer()
proto.RegisterEmojiServiceServer(s, &server{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}複製程式碼
我已經使用 kyokomi/emoji
庫來完成繁重的工作,即將輸入文字中的關鍵字轉換為表情符號。
啟動服務後如下所示:
$ go run -v main.go
2018/11/12 10:45:12 listening on [::]:9000複製程式碼
我們建立一個名為emoji_client.go的客戶端,來實現通過程式測試emoji服務。
package main
import (
"log"
"time"
proto "github.com/venilnoronha/grpc-web-emoji/emoji"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
func main() {
// connect to the server
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
log.Fatalf("could not connect to the service: %v", err)
}
defer conn.Close()
// send a request to the server
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
c := proto.NewEmojiServiceClient(conn)
resp, err := c.Emojize(ctx, &proto.EmojizeRequest{
Text: "I like :pizza: and :sushi:!",
})
if err != nil {
log.Fatalf("could not call service: %v", err)
}
log.Printf("server says: %s", resp.GetEmojizedText())
}複製程式碼
我們現在可以執行emoji服務客戶端,如下所示。
$ go run emoji_client.go
2018/11/12 10:55:52 server says: I like ? and ? !複製程式碼
瞧!gRPC版本的emoji服務如期工作了,現在是時候讓Web前端啟動並執行了。
使用gRPC-Web建立Web介面
首先,讓我們建立一個名為index.html
的HTML頁面。該頁面向使用者顯示一個文字編輯器,並呼叫一個emojize
函式(我們稍後將定義)將使用者輸入傳送到後端emoji服務。emojize
函式還將消費後端服務返回的gRPC響應,並使用服務端返回的資料更新使用者輸入框。
<!DOCTYPE html>
<html>
<body>
<div id="editor" contentEditable="true" hidefocus="true" onkeyup="emojize()"></div>
<script src="dist/main.js"></script>
</body>
</html>複製程式碼
我們將如下所示的JavaScript程式碼放入名為client.js的前端檔案。
const {EmojizeRequest, EmojizeReply} = require('emoji/emoji_pb.js');
const {EmojiServiceClient} = require('emoji/emoji_grpc_web_pb.js');
var client = new EmojiServiceClient('http://192.168.99.100:31380');
var editor = document.getElementById('editor');
window.emojize = function() {
var request = new EmojizeRequest();
request.setText(editor.innerText);
client.emojize(request, {}, (err, response) => {
editor.innerText = response.getEmojizedText();
});
}複製程式碼
請注意,EmojiServiceClient
與後端emoji服務的連線地址是http://192.168.99.100:31380
,而非http://localhost:9000
。這是因為Web應用程式無法直接與gRPC後端通訊,因此,我們將通過Istio部署我們的後端emoji服務。Istio將在Minikube上執行,其IP地址為192.168.99.100
,預設的Istio Ingress HTTP埠為31380
。
現在,我們需要一些庫來生成index.html
中引用的dist/main.js
檔案。為此,我們使用如下的npm package.json
配置。
{
"name": "grpc-web-emoji",
"version": "0.1.0",
"description": "gRPC-Web Emoji Sample",
"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"
}
}複製程式碼
此時,我們使用如下命令來安裝庫並生成dist/main.js
。
$ npm install
$ npx webpack client.js複製程式碼
通過Istio部署後端服務
我們現在可以將後端emoji服務打包到一個容器,並通過Istio進行部署。我們需要安裝gRPC-Web EnvoyFilter
,以便將後端gRPC服務的呼叫在gRPC和HTTP間轉換。
我們使用如下內容的Dockerfile
構建Docker image。
FROM golang:1.11 as builder
WORKDIR /root/go/src/github.com/venilnoronha/grpc-web-emoji/
COPY ./ .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -v -o emoji-service main.go
FROM scratch
WORKDIR /bin/
COPY --from=builder /root/go/src/github.com/venilnoronha/grpc-web-emoji/emoji-service .
ENTRYPOINT [ "/bin/emoji-service" ]
CMD [ "9000" ]
EXPOSE 9000複製程式碼
我們可以如下所示build image,並將其推送到Docker Hub:
$ docker build -t vnoronha/grpc-web-emoji .
$ docker push vnoronha/grpc-web-emoji複製程式碼
接下來,我們定義Kubernetes Service
和Deployment
配置,如下所示,並命名為backend.yaml
。
apiVersion: v1
kind: Service
metadata:
name: backend
labels:
app: backend
spec:
ports:
- name: grpc-port
port: 9000
selector:
app: backend
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: backend
spec:
replicas: 1
template:
metadata:
labels:
app: backend
version: v1
spec:
containers:
- name: backend
image: vnoronha/grpc-web-emoji
imagePullPolicy: Always
ports:
- containerPort: 9000複製程式碼
注意,一旦我們通過Istio部署此服務,由於Service
ports name
中的grpc-
字首,Istio會將其識別為gRPC服務。
由於我們希望將gRPC-Web filter安裝在backend
sidecar代理上,因此我們需要在部署backend
服務之前安裝它。EnvoyFilter
配置如下所示,我們將其命名為filter.yaml
。
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: grpc-web-filter
spec:
workloadLabels:
app: backend
filters:
- listenerMatch:
listenerType: SIDECAR_INBOUND
listenerProtocol: HTTP
insertPosition:
index: FIRST
filterType: HTTP
filterName: "envoy.grpc_web"
filterConfig: {}複製程式碼
接下來,我們需要定義一個Istio Gateway
來將HTTP流量路由到後端服務。為此,我們將以下配置寫入名為gateway.yaml
的檔案。
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: backend
spec:
host: backend
subsets:
- name: v1
labels:
version: v1
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: vs
spec:
hosts:
- "*"
gateways:
- gateway
http:
- match:
- port: 80
route:
- destination:
host: backend
port:
number: 9000
subset: v1
corsPolicy:
allowOrigin:
- "*"
allowMethods:
- POST
- GET
- OPTIONS
- PUT
- DELETE
allowHeaders:
- grpc-timeout
- content-type
- 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
maxAge: 1728s
exposeHeaders:
- custom-header-1
- grpc-status
- grpc-message
allowCredentials: true複製程式碼
注意,為了能讓gRPC-Web正常工作,我們在這裡定義了一個複雜的corsPolicy
。
我們現在可以按以下順序簡單地部署上述配置。
$ kubectl apply -f filter.yaml
$ kubectl apply -f <(istioctl kube-inject -f backend.yaml)
$ kubectl apply -f gateway.yaml複製程式碼
backend
pod啟動之後,我們可以驗證gRPC-Web filter在sidecar代理中的配置是否正確,如下所示:
$ istioctl proxy-config listeners backend-7bf6c8f67c-8lbm7 --port 9000 -o json
...
"http_filters": [
{
"config": {},
"name": "envoy.grpc_web"
},
...複製程式碼
部署和測試Web前端
我們現在已經到了實驗的最後階段。我們通過Python啟動一個HTTP服務,來為我們的Web應用提供服務。
$ python2 -m SimpleHTTPServer 8080
Serving HTTP on 0.0.0.0 port 8080 ...複製程式碼
讓我們前往emoji web頁面http://localhost:8080
.
如果一切順利,你將擁有一個功能完整的基於gRPC-Web的Web應用,如下所示。
如果你在Chrome等瀏覽器上開啟開發者工具,你將會看到如下所示的gRPC-Web HTTP請求。
結論
gRPC-Web提供了一種將gRPC服務的優勢帶給Web應用的好方法。它目前需要一箇中間代理,如Istio資料平面(即Envoy代理),以便將資料在HTTP和gRPC之間轉換。然而,一旦我們準備好了基礎架構,開發人員就可以無縫使用gRPC構建Web應用。
參考
gRPC-Web Hello World指南
WebpageFx Emoji清單