<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
gRPC 這項技術真是太棒了,介面約束嚴格,效能還高,在 k8s 和很多微服務架構中都有應用。
作為一名程式設計師,學就對了。
之前用 Python 寫過一些 gRPC 服務,現在準備用 Go 來感受一下原汁原味的 gRPC 程式開發。
本文的特點是直接用程式碼說話,通過開箱即用的完整程式碼,來介紹 gRPC 的各種使用方法。
程式碼已經上傳到 GitHub,下面正式開始。
gRPC 是 Google 公司基於 Protobuf 開發的跨語言的開源 RPC 框架。gRPC 基於 HTTP/2 協定設計,可以基於一個 HTTP/2 連結提供多個服務,對於移動裝置更加友好。
首先來看一個最簡單的 gRPC 服務,第一步是定義 proto 檔案,因為 gRPC 也是 C/S 架構,這一步相當於明確介面規範。
syntax = "proto3"; package proto; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
使用 protoc-gen-go 內建的 gRPC 外掛生成 gRPC 程式碼:
protoc --go_out=plugins=grpc:. helloworld.proto
執行完這個命令之後,會在當前目錄生成一個 helloworld.pb.go 檔案,檔案中分別定義了伺服器端和使用者端的介面:
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type GreeterClient interface { // Sends a greeting SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) } // GreeterServer is the server API for Greeter service. type GreeterServer interface { // Sends a greeting SayHello(context.Context, *HelloRequest) (*HelloReply, error) }
接下來就是寫伺服器端和使用者端的程式碼,分別實現對應的介面。
package main import ( "context" "fmt" "grpc-server/proto" "log" "net" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) type greeter struct { } func (*greeter) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) { fmt.Println(req) reply := &proto.HelloReply{Message: "hello"} return reply, nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } server := grpc.NewServer() // 註冊 grpcurl 所需的 reflection 服務 reflection.Register(server) // 註冊業務服務 proto.RegisterGreeterServer(server, &greeter{}) fmt.Println("grpc server start ...") if err := server.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
package main import ( "context" "fmt" "grpc-client/proto" "log" "google.golang.org/grpc" ) func main() { conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatal(err) } defer conn.Close() client := proto.NewGreeterClient(conn) reply, err := client.SayHello(context.Background(), &proto.HelloRequest{Name: "zhangsan"}) if err != nil { log.Fatal(err) } fmt.Println(reply.Message) }
這樣就完成了最基礎的 gRPC 服務的開發,接下來我們就在這個「基礎模板」上不斷豐富,學習更多特性。
接下來看看流的方式,顧名思義,資料可以源源不斷的傳送和接收。
流的話分單向流和雙向流,這裡我們直接通過雙向流來舉例。
service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} // Sends stream message rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {} }
增加一個流函數 SayHelloStream
,通過 stream
關鍵詞來指定流特性。
需要重新生成 helloworld.pb.go 檔案,這裡不再多說。
func (*greeter) SayHelloStream(stream proto.Greeter_SayHelloStreamServer) error { for { args, err := stream.Recv() if err != nil { if err == io.EOF { return nil } return err } fmt.Println("Recv: " + args.Name) reply := &proto.HelloReply{Message: "hi " + args.Name} err = stream.Send(reply) if err != nil { return err } } }
在「基礎模板」上增加 SayHelloStream
函數,其他都不需要變。
client := proto.NewGreeterClient(conn) // 流處理 stream, err := client.SayHelloStream(context.Background()) if err != nil { log.Fatal(err) } // 傳送訊息 go func() { for { if err := stream.Send(&proto.HelloRequest{Name: "zhangsan"}); err != nil { log.Fatal(err) } time.Sleep(time.Second) } }() // 接收訊息 for { reply, err := stream.Recv() if err != nil { if err == io.EOF { break } log.Fatal(err) } fmt.Println(reply.Message) }
通過一個 goroutine 傳送訊息,主程式的 for
迴圈接收訊息。
執行程式會發現,伺服器端和使用者端都不斷有列印輸出。
接下來是驗證器,這個需求是很自然會想到的,因為涉及到介面之間的請求,那麼對引數進行適當的校驗是很有必要的。
在這裡我們使用 protoc-gen-govalidators 和 go-grpc-middleware 來實現。
先安裝:
go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators go get github.com/grpc-ecosystem/go-grpc-middleware
接下來修改 proto 檔案:
import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto"; message HelloRequest { string name = 1 [ (validator.field) = {regex: "^[z]{2,5}$"} ]; }
在這裡對 name
引數進行校驗,需要符合正則的要求才可以正常請求。
還有其他驗證規則,比如對數位大小進行驗證等,這裡不做過多介紹。
接下來生成 *.pb.go 檔案:
protoc --proto_path=${GOPATH}/pkg/mod --proto_path=${GOPATH}/pkg/mod/github.com/gogo/protobuf@v1.3.2 --proto_path=. --govalidators_out=. --go_out=plugins=grpc:. *.proto
執行成功之後,目錄下會多一個 helloworld.validator.pb.go 檔案。
這裡需要特別注意一下,使用之前的簡單命令是不行的,需要使用多個 proto_path
引數指定匯入 proto 檔案的目錄。
官方給了兩種依賴情況,一個是 google protobuf,一個是 gogo protobuf。我這裡使用的是第二種。
即使使用上面的命令,也有可能會遇到這個報錯:
Import "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors
但不要慌,大概率是參照路徑的問題,一定要看好自己的安裝版本,以及在 GOPATH
中的具體路徑。
最後是伺服器端程式碼改造:
引入包:
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
然後在初始化的時候增加驗證器功能:
server := grpc.NewServer( grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( grpc_validator.UnaryServerInterceptor(), ), ), grpc.StreamInterceptor( grpc_middleware.ChainStreamServer( grpc_validator.StreamServerInterceptor(), ), ), )
啟動程式之後,我們再用之前的使用者端程式碼來請求,會收到報錯:
2021/10/11 18:32:59 rpc error: code = InvalidArgument desc = invalid field Name: value 'zhangsan' must be a string conforming to regex "^[z]{2,5}$" exit status 1
因為 name: zhangsan
是不符合伺服器端正則要求的,但是如果傳參 name: zzz
,就可以正常返回了。
終於到認證環節了,先看 Token 認證方式,然後再介紹證書認證。
先改造伺服器端,有了上文驗證器的經驗,那麼可以採用同樣的方式,寫一個攔截器,然後在初始化 server 時候注入。
func Auth(ctx context.Context) error { md, ok := metadata.FromIncomingContext(ctx) if !ok { return fmt.Errorf("missing credentials") } var user string var password string if val, ok := md["user"]; ok { user = val[0] } if val, ok := md["password"]; ok { password = val[0] } if user != "admin" || password != "admin" { return grpc.Errorf(codes.Unauthenticated, "invalid token") } return nil }
metadata.FromIncomingContext
從上下文讀取使用者名稱和密碼,然後和實際資料進行比較,判斷是否通過認證。
var authInterceptor grpc.UnaryServerInterceptor authInterceptor = func( ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler, ) (resp interface{}, err error) { //攔截普通方法請求,驗證 Token err = Auth(ctx) if err != nil { return } // 繼續處理請求 return handler(ctx, req) }
server := grpc.NewServer( grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( authInterceptor, grpc_validator.UnaryServerInterceptor(), ), ), grpc.StreamInterceptor( grpc_middleware.ChainStreamServer( grpc_validator.StreamServerInterceptor(), ), ), )
除了上文的驗證器,又多了 Token 認證攔截器 authInterceptor
。
最後是使用者端改造,使用者端需要實現 PerRPCCredentials
介面。
type PerRPCCredentials interface { // GetRequestMetadata gets the current request metadata, refreshing // tokens if required. This should be called by the transport layer on // each request, and the data should be populated in headers or other // context. If a status code is returned, it will be used as the status // for the RPC. uri is the URI of the entry point for the request. // When supported by the underlying implementation, ctx can be used for // timeout and cancellation. // TODO(zhaoq): Define the set of the qualified keys instead of leaving // it as an arbitrary string. GetRequestMetadata(ctx context.Context, uri ...string) ( map[string]string, error, ) // RequireTransportSecurity indicates whether the credentials requires // transport security. RequireTransportSecurity() bool }
GetRequestMetadata
方法返回認證需要的必要資訊,RequireTransportSecurity
方法表示是否啟用安全連結,在生產環境中,一般都是啟用的,但為了測試方便,暫時這裡不啟用了。
type Authentication struct { User string Password string } func (a *Authentication) GetRequestMetadata(context.Context, ...string) ( map[string]string, error, ) { return map[string]string{"user": a.User, "password": a.Password}, nil } func (a *Authentication) RequireTransportSecurity() bool { return false }
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))
好了,現在我們的服務就有 Token 認證功能了。如果使用者名稱或密碼錯誤,使用者端就會收到:
2021/10/11 20:39:35 rpc error: code = Unauthenticated desc = invalid token exit status 1
如果使用者名稱和密碼正確,則可以正常返回。
證書認證分兩種方式:
先看一下單向認證方式:
首先通過 openssl 工具生成自簽名的 SSL 證書。
1、生成私鑰:
openssl genrsa -des3 -out server.pass.key 2048
2、去除私鑰中密碼:
openssl rsa -in server.pass.key -out server.key
3、生成 csr 檔案:
openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=beijing/L=beijing/O=grpcdev/OU=grpcdev/CN=example.grpcdev.cn"
4、生成證書:
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
再多說一句,分別介紹一下 X.509 證書包含的三個檔案:key,csr 和 crt。
證書有了之後,剩下的就是改造程式了,首先是伺服器端程式碼。
// 證書認證-單向認證 creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key") if err != nil { log.Fatal(err) return } server := grpc.NewServer(grpc.Creds(creds))
只有幾行程式碼需要修改,很簡單,接下來是使用者端。
由於是單向認證,不需要為使用者端單獨生成證書,只需要把伺服器端的 crt 檔案拷貝到使用者端對應目錄下即可。
// 證書認證-單向認證 creds, err := credentials.NewClientTLSFromFile("keys/server.crt", "example.grpcdev.cn") if err != nil { log.Fatal(err) return } conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
好了,現在我們的服務就支援單向證書認證了。
但是還沒完,這裡可能會遇到一個問題:
2021/10/11 21:32:37 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0" exit status 1
原因是 Go 1.15 開始廢棄了 CommonName,推薦使用 SAN 證書。如果想要相容之前的方式,可以通過設定環境變數的方式支援,如下:
export GODEBUG="x509ignoreCN=0"
但是需要注意,從 Go 1.17 開始,環境變數就不再生效了,必須通過 SAN 方式才行。所以,為了後續的 Go 版本升級,還是早日支援為好。
最後來看看雙向證書認證。
還是先生成證書,但這次有一點不一樣,我們需要生成帶 SAN 擴充套件的證書。
什麼是 SAN?
SAN(Subject Alternative Name)是 SSL 標準 x509 中定義的一個擴充套件。使用了 SAN 欄位的 SSL 證書,可以擴充套件此證書支援的域名,使得一個證書可以支援多個不同域名的解析。
將預設的 OpenSSL 組態檔拷貝到當前目錄。
Linux 系統在:
/etc/pki/tls/openssl.cnf
Mac 系統在:
/System/Library/OpenSSL/openssl.cnf
修改臨時組態檔,找到 [ req ]
段落,然後將下面語句的註釋去掉。
req_extensions = v3_req # The extensions to add to a certificate request
接著新增以下設定:
[ v3_req ] # Extensions to add to a certificate request basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names [ alt_names ] DNS.1 = www.example.grpcdev.cn
[ alt_names ]
位置可以設定多個域名,比如:
[ alt_names ] DNS.1 = www.example.grpcdev.cn DNS.2 = www.test.grpcdev.cn
為了測試方便,這裡只設定一個域名。
1、生成 ca 證書:
openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=example.grpcdev.com" -days 5000 -out ca.pem
2、生成伺服器端證書:
# 生成證書 openssl req -new -nodes -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" -config <(cat openssl.cnf <(printf "[SAN]nsubjectAltName=DNS:www.example.grpcdev.cn")) -keyout server.key -out server.csr # 簽名證書 openssl x509 -req -days 365000 -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") -out server.pem
3、生成使用者端證書:
# 生成證書 openssl req -new -nodes -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" -config <(cat openssl.cnf <(printf "[SAN]nsubjectAltName=DNS:www.example.grpcdev.cn")) -keyout client.key -out client.csr # 簽名證書 openssl x509 -req -days 365000 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") -out client.pem
接下來開始修改程式碼,先看伺服器端:
// 證書認證-雙向認證 // 從證書相關檔案中讀取和解析資訊,得到證書公鑰、金鑰對 cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key") // 建立一個新的、空的 CertPool certPool := x509.NewCertPool() ca, _ := ioutil.ReadFile("cert/ca.pem") // 嘗試解析所傳入的 PEM 編碼的證書。如果解析成功會將其加到 CertPool 中,便於後面的使用 certPool.AppendCertsFromPEM(ca) // 構建基於 TLS 的 TransportCredentials 選項 creds := credentials.NewTLS(&tls.Config{ // 設定證書鏈,允許包含一個或多個 Certificates: []tls.Certificate{cert}, // 要求必須校驗使用者端的證書。可以根據實際情況選用以下引數 ClientAuth: tls.RequireAndVerifyClientCert, // 設定根證書的集合,校驗方式使用 ClientAuth 中設定的模式 ClientCAs: certPool, })
再看使用者端:
// 證書認證-雙向認證 // 從證書相關檔案中讀取和解析資訊,得到證書公鑰、金鑰對 cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key") // 建立一個新的、空的 CertPool certPool := x509.NewCertPool() ca, _ := ioutil.ReadFile("cert/ca.pem") // 嘗試解析所傳入的 PEM 編碼的證書。如果解析成功會將其加到 CertPool 中,便於後面的使用 certPool.AppendCertsFromPEM(ca) // 構建基於 TLS 的 TransportCredentials 選項 creds := credentials.NewTLS(&tls.Config{ // 設定證書鏈,允許包含一個或多個 Certificates: []tls.Certificate{cert}, // 要求必須校驗使用者端的證書。可以根據實際情況選用以下引數 ServerName: "www.example.grpcdev.cn", RootCAs: certPool, })
大功告成。
前面已經說了,gRPC 是跨語言的,那麼,本文最後我們用 Python 寫一個使用者端,來請求 Go 伺服器端。
使用最簡單的方式來實現:
proto 檔案就使用最開始的「基礎模板」的 proto 檔案:
syntax = "proto3"; package proto; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} // Sends stream message rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
同樣的,也需要通過命令列的方式生成 pb.py 檔案:
python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ./*.proto
執行成功之後會在目錄下生成 helloworld_pb2.py 和 helloworld_pb2_grpc.py 兩個檔案。
這個過程也可能會報錯:
ModuleNotFoundError: No module named 'grpc_tools'
別慌,是缺少包,安裝就好:
pip3 install grpcio pip3 install grpcio-tools
最後看一下 Python 使用者端程式碼:
import grpc import helloworld_pb2 import helloworld_pb2_grpc def main(): channel = grpc.insecure_channel("127.0.0.1:50051") stub = helloworld_pb2_grpc.GreeterStub(channel) response = stub.SayHello(helloworld_pb2.HelloRequest(name="zhangsan")) print(response.message) if __name__ == '__main__': main()
這樣,就可以通過 Python 使用者端請求 Go 啟的伺服器端服務了。
本文通過實戰角度出發,直接用程式碼說話,來說明 gRPC 的一些應用。
內容包括簡單的 gRPC 服務,流處理模式,驗證器,Token 認證和證書認證。
除此之外,還有其他值得研究的內容,比如超時控制,REST 介面和負載均衡等。以後還會抽時間繼續完善剩下這部分內容。
本文中的程式碼都經過測試驗證,可以直接執行,並且已經上傳到 GitHub,小夥伴們可以一遍看原始碼,一遍對照文章內容來學習。
原始碼地址:
https://github.com/yongxinz/go-example/tree/main/grpc-example
https://github.com/yongxinz/gopher/tree/main/blog
以上就是Go語言程式開發gRPC服務的詳細內容,更多關於Go語言開發gRPC服務的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45