grpc實戰(zhàn):跨語言的rpc框架到底好不好用容为,試試就知道

原文鏈接:https://mp.weixin.qq.com/s/1Xbca4Dv0akonAZerrChgA

gRPC 這項技術真是太棒了,接口約束嚴格酷麦,性能還高凳厢,在 k8s 和很多微服務框架中都有應用。

作為一名程序員料身,學就對了。

之前用 Python 寫過一些 gRPC 服務衩茸,現(xiàn)在準備用 Go 來感受一下原汁原味的 gRPC 程序開發(fā)芹血。

本文的特點是直接用代碼說話,通過開箱即用的完整代碼,來介紹 gRPC 的各種使用方法祟牲。

代碼已經(jīng)上傳到 GitHub隙畜,下面正式開始抖部。

介紹

圖片

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

gRPC 是 Google 公司基于 Protobuf 開發(fā)的跨語言的開源 RPC 框架说贝。gRPC 基于 HTTP/2 協(xié)議設計,可以基于一個 HTTP/2 鏈接提供多個服務慎颗,對于移動設備更加友好乡恕。

入門

首先來看一個最簡單的 gRPC 服務,第一步是定義 proto 文件俯萎,因為 gRPC 也是 C/S 架構(gòu)傲宜,這一步相當于明確接口規(guī)范。

proto

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;
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

使用 protoc-gen-go 內(nèi)置的 gRPC 插件生成 gRPC 代碼:

protoc --go_out=plugins=grpc:. helloworld.proto

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

執(zhí)行完這個命令之后夫啊,會在當前目錄生成一個 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)
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

接下來就是寫服務端和客戶端的代碼,分別實現(xiàn)對應的接口撇眯。

server

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)
    // 注冊業(yè)務服務
    proto.RegisterGreeterServer(server, &greeter{})

    fmt.Println("grpc server start ...")
    if err := server.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

client

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)
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

這樣就完成了最基礎的 gRPC 服務的開發(fā)报嵌,接下來我們就在這個「基礎模板」上不斷豐富,學習更多特性熊榛。

流方式

接下來看看流的方式锚国,顧名思義,數(shù)據(jù)可以源源不斷的發(fā)送和接收玄坦。

流的話分單向流和雙向流血筑,這里我們直接通過雙向流來舉例。

proto

service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
    // Sends stream message
    rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {}
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

增加一個流函數(shù) SayHelloStream煎楣,通過 stream 關鍵詞來指定流特性豺总。

需要重新生成 helloworld.pb.go 文件,這里不再多說择懂。

server

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
        }
    }
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

在「基礎模板」上增加 SayHelloStream 函數(shù)喻喳,其他都不需要變。

client

client := proto.NewGreeterClient(conn)

// 流處理
stream, err := client.SayHelloStream(context.Background())
if err != nil {
    log.Fatal(err)
}

// 發(fā)送消息
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)
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

通過一個 goroutine 發(fā)送消息休蟹,主程序的 for 循環(huán)接收消息沸枯。

執(zhí)行程序會發(fā)現(xiàn),服務端和客戶端都不斷有打印輸出赂弓。

驗證器

接下來是驗證器绑榴,這個需求是很自然會想到的,因為涉及到接口之間的請求盈魁,那么對參數(shù)進行適當?shù)男r炇呛苡斜匾摹?/p>

在這里我們使用 protoc-gen-govalidators 和 go-grpc-middleware 來實現(xiàn)翔怎。

先安裝:

go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators

go get github.com/grpc-ecosystem/go-grpc-middleware

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

接下來修改 proto 文件:

proto

import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";

message HelloRequest {
    string name = 1 [
        (validator.field) = {regex: "^[z]{2,5}$"}
    ];
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

在這里對 name 參數(shù)進行校驗,需要符合正則的要求才可以正常請求。

還有其他驗證規(guī)則赤套,比如對數(shù)字大小進行驗證等飘痛,這里不做過多介紹。

接下來生成 *.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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

執(zhí)行成功之后,目錄下會多一個 helloworld.validator.pb.go 文件。

這里需要特別注意一下野瘦,使用之前的簡單命令是不行的毛秘,需要使用多個 proto_path 參數(shù)指定導入 proto 文件的目錄。

官方給了兩種依賴情況,一個是 google protobuf,一個是 gogo protobuf。我這里使用的是第二種羊苟。

即使使用上面的命令,也有可能會遇到這個報錯:

Import "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

但不要慌感憾,大概率是引用路徑的問題蜡励,一定要看好自己的安裝版本,以及在 GOPATH 中的具體路徑阻桅。

最后是服務端代碼改造:

引入包:

grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

然后在初始化的時候增加驗證器功能:

server := grpc.NewServer(
    grpc.UnaryInterceptor(
        grpc_middleware.ChainUnaryServer(
            grpc_validator.UnaryServerInterceptor(),
        ),
    ),
    grpc.StreamInterceptor(
        grpc_middleware.ChainStreamServer(
            grpc_validator.StreamServerInterceptor(),
        ),
    ),
)

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

啟動程序之后凉倚,我們再用之前的客戶端代碼來請求,會收到報錯:

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

因為 name: zhangsan 是不符合服務端正則要求的鳍刷,但是如果傳參 name: zzz占遥,就可以正常返回了。

Token 認證

終于到認證環(huán)節(jié)了输瓜,先看 Token 認證方式瓦胎,然后再介紹證書認證。

先改造服務端尤揣,有了上文驗證器的經(jīng)驗搔啊,那么可以采用同樣的方式,寫一個攔截器北戏,然后在初始化 server 時候注入负芋。

認證函數(shù):

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
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

metadata.FromIncomingContext 從上下文讀取用戶名和密碼,然后和實際數(shù)據(jù)進行比較嗜愈,判斷是否通過認證旧蛾。

攔截器:

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
    }
    // 繼續(xù)處理請求
    return handler(ctx, req)
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

初始化:

server := grpc.NewServer(
    grpc.UnaryInterceptor(
        grpc_middleware.ChainUnaryServer(
            authInterceptor,
            grpc_validator.UnaryServerInterceptor(),
        ),
    ),
    grpc.StreamInterceptor(
        grpc_middleware.ChainStreamServer(
            grpc_validator.StreamServerInterceptor(),
        ),
    ),
)

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

除了上文的驗證器蠕嫁,又多了 Token 認證攔截器 authInterceptor锨天。

最后是客戶端改造,客戶端需要實現(xiàn) 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
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

GetRequestMetadata 方法返回認證需要的必要信息病袄,RequireTransportSecurity 方法表示是否啟用安全鏈接搂赋,在生產(chǎn)環(huán)境中,一般都是啟用的益缠,但為了測試方便脑奠,暫時這里不啟用了。

實現(xiàn)接口:

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
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

連接:

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

好了幅慌,現(xiàn)在我們的服務就有 Token 認證功能了宋欺。如果用戶名或密碼錯誤,客戶端就會收到:

2021/10/11 20:39:35 rpc error: code = Unauthenticated desc = invalid token
exit status 1

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

如果用戶名和密碼正確欠痴,則可以正常返回迄靠。

單向證書認證

證書認證分兩種方式:

  1. 單向認證

  2. 雙向認證

先看一下單向認證方式:

生成證書

首先通過 openssl 工具生成自簽名的 SSL 證書。

1喇辽、生成私鑰:

openssl genrsa -des3 -out server.pass.key 2048

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

2、去除私鑰中密碼:

openssl rsa -in server.pass.key -out server.key

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

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"

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

4菩咨、生成證書:

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

再多說一句,分別介紹一下 X.509 證書包含的三個文件:key陡厘,csr 和 crt抽米。

  • key: 服務器上的私鑰文件,用于對發(fā)送給客戶端數(shù)據(jù)的加密糙置,以及對從客戶端接收到數(shù)據(jù)的解密云茸。

  • csr: 證書簽名請求文件,用于提交給證書頒發(fā)機構(gòu)(CA)對證書簽名谤饭。

  • crt: 由證書頒發(fā)機構(gòu)(CA)簽名后的證書标捺,或者是開發(fā)者自簽名的證書,包含證書持有人的信息揉抵,持有人的公鑰亡容,以及簽署者的簽名等信息。

gRPC 代碼

證書有了之后冤今,剩下的就是改造程序了闺兢,首先是服務端代碼。

// 證書認證-單向認證
creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
if err != nil {
    log.Fatal(err)
    return
}

server := grpc.NewServer(grpc.Creds(creds))

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

只有幾行代碼需要修改戏罢,很簡單屋谭,接下來是客戶端。

由于是單向認證龟糕,不需要為客戶端單獨生成證書桐磁,只需要把服務端的 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))

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

好了翩蘸,現(xiàn)在我們的服務就支持單向證書認證了所意。

但是還沒完,這里可能會遇到一個問題:

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

原因是 Go 1.15 開始廢棄了 CommonName,推薦使用 SAN 證書扶踊。如果想要兼容之前的方式泄鹏,可以通過設置環(huán)境變量的方式支持,如下:

export GODEBUG="x509ignoreCN=0"

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

但是需要注意秧耗,從 Go 1.17 開始备籽,環(huán)境變量就不再生效了,必須通過 SAN 方式才行分井。所以车猬,為了后續(xù)的 Go 版本升級,還是早日支持為好尺锚。

雙向證書認證

最后來看看雙向證書認證珠闰。

生成帶 SAN 的證書

還是先生成證書,但這次有一點不一樣瘫辩,我們需要生成帶 SAN 擴展的證書伏嗜。

什么是 SAN?

SAN(Subject Alternative Name)是 SSL 標準 x509 中定義的一個擴展伐厌。使用了 SAN 字段的 SSL 證書承绸,可以擴展此證書支持的域名,使得一個證書可以支持多個不同域名的解析挣轨。

將默認的 OpenSSL 配置文件拷貝到當前目錄军熏。

Linux 系統(tǒng)在:

/etc/pki/tls/openssl.cnf

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

Mac 系統(tǒng)在:

/System/Library/OpenSSL/openssl.cnf

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

修改臨時配置文件,找到 [ req ] 段落卷扮,然后將下面語句的注釋去掉荡澎。

req_extensions = v3_req # The extensions to add to a certificate request

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

接著添加以下配置:

[ 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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

[ alt_names ] 位置可以配置多個域名,比如:

[ alt_names ]
DNS.1 = www.example.grpcdev.cn
DNS.2 = www.test.grpcdev.cn

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

為了測試方便画饥,這里只配置一個域名衔瓮。

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

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

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

gRPC 代碼

接下來開始修改代碼,先看服務端:

// 證書認證-雙向認證
// 從證書相關文件中讀取和解析信息衔彻,得到證書公鑰薇宠、密鑰對
cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
// 創(chuàng)建一個新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 嘗試解析所傳入的 PEM 編碼的證書艰额。如果解析成功會將其加到 CertPool 中澄港,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 構(gòu)建基于 TLS 的 TransportCredentials 選項
creds := credentials.NewTLS(&tls.Config{
    // 設置證書鏈,允許包含一個或多個
    Certificates: []tls.Certificate{cert},
    // 要求必須校驗客戶端的證書柄沮』匚啵可以根據(jù)實際情況選用以下參數(shù)
    ClientAuth: tls.RequireAndVerifyClientCert,
    // 設置根證書的集合废岂,校驗方式使用 ClientAuth 中設定的模式
    ClientCAs: certPool,
})

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

再看客戶端:

// 證書認證-雙向認證
// 從證書相關文件中讀取和解析信息,得到證書公鑰狱意、密鑰對
cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
// 創(chuàng)建一個新的湖苞、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 嘗試解析所傳入的 PEM 編碼的證書。如果解析成功會將其加到 CertPool 中详囤,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 構(gòu)建基于 TLS 的 TransportCredentials 選項
creds := credentials.NewTLS(&tls.Config{
    // 設置證書鏈财骨,允許包含一個或多個
    Certificates: []tls.Certificate{cert},
    // 要求必須校驗客戶端的證書〔亟悖可以根據(jù)實際情況選用以下參數(shù)
    ServerName: "www.example.grpcdev.cn",
    RootCAs:    certPool,
})

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

大功告成隆箩。

Python 客戶端

前面已經(jīng)說了,gRPC 是跨語言的羔杨,那么捌臊,本文最后我們用 Python 寫一個客戶端,來請求 Go 服務端问畅。

使用最簡單的方式來實現(xiàn):

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;
}

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

同樣的娃属,也需要通過命令行的方式生成 pb.py 文件:

python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ./*.proto

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

執(zhí)行成功之后會在目錄下生成 helloworld_pb2.py 和 helloworld_pb2_grpc.py 兩個文件。

這個過程也可能會報錯:

ModuleNotFoundError: No module named 'grpc_tools'

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

別慌护姆,是缺少包,安裝就好:

pip3 install grpcio
pip3 install grpcio-tools

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

最后看一下 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()

![](data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw== "點擊并拖拽以移動")

這樣掏击,就可以通過 Python 客戶端請求 Go 啟的服務端服務了卵皂。

總結(jié)

本文通過實戰(zhàn)角度出發(fā),直接用代碼說話砚亭,來說明 gRPC 的一些應用灯变。

內(nèi)容包括簡單的 gRPC 服務,流處理模式捅膘,驗證器添祸,Token 認證和證書認證。

除此之外寻仗,還有其他值得研究的內(nèi)容刃泌,比如超時控制,REST 接口和負載均衡等署尤。以后還會抽時間繼續(xù)完善剩下這部分內(nèi)容耙替。

本文中的代碼都經(jīng)過測試驗證,可以直接執(zhí)行曹体,并且已經(jīng)上傳到 GitHub俗扇,小伙伴們可以一遍看源碼,一遍對照文章內(nèi)容來學習箕别。


源碼地址:

微信公眾號【程序員黃小斜】作者是前螞蟻金服Java工程師铜幽,專注分享Java技術干貨和求職成長心得滞谢,不限于BAT面試,算法除抛、計算機基礎狮杨、數(shù)據(jù)庫、分布式镶殷、spring全家桶禾酱、微服務、高并發(fā)绘趋、JVM颤陶、Docker容器,ELK陷遮、大數(shù)據(jù)等滓走。關注后回復【book】領取精選20本Java面試必備精品電子書。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帽馋,一起剝皮案震驚了整個濱河市搅方,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绽族,老刑警劉巖姨涡,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吧慢,居然都是意外死亡涛漂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門检诗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匈仗,“玉大人,你說我怎么就攤上這事逢慌∮菩” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵攻泼,是天一觀的道長火架。 經(jīng)常有香客問我,道長坠韩,這世上最難降的妖魔是什么距潘? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮只搁,結(jié)果婚禮上音比,老公的妹妹穿的比我還像新娘。我一直安慰自己氢惋,他們只是感情好洞翩,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布稽犁。 她就那樣靜靜地躺著,像睡著了一般骚亿。 火紅的嫁衣襯著肌膚如雪已亥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天来屠,我揣著相機與錄音虑椎,去河邊找鬼。 笑死俱笛,一個胖子當著我的面吹牛捆姜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播迎膜,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼泥技,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了磕仅?” 一聲冷哼從身側(cè)響起珊豹,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎榕订,沒想到半個月后店茶,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡劫恒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年忽妒,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兼贸。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吃溅,靈堂內(nèi)的尸體忽然破棺而出溶诞,到底是詐尸還是另有隱情,我是刑警寧澤决侈,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布螺垢,位于F島的核電站,受9級特大地震影響赖歌,放射性物質(zhì)發(fā)生泄漏枉圃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一庐冯、第九天 我趴在偏房一處隱蔽的房頂上張望孽亲。 院中可真熱鬧,春花似錦展父、人聲如沸返劲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽篮绿。三九已至孵延,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亲配,已是汗流浹背尘应。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吼虎,地道東北人犬钢。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像鲸睛,于是被迫代替她去往敵國和親娜饵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容