关于golang:gRPC爆赞

7次阅读

共计 12900 个字符,预计需要花费 33 分钟才能阅读完成。

原文链接: gRPC,爆赞

gRPC 这项技术真是太棒了,接口束缚严格,性能还高,在 k8s 和很多微服务框架中都有利用。

作为一名程序员,学就对了。

之前用 Python 写过一些 gRPC 服务,当初筹备用 Go 来感受一下原汁原味的 gRPC 程序开发。

本文的特点是间接用代码谈话,通过开箱即用的残缺代码,来介绍 gRPC 的各种应用办法。

代码曾经上传到 GitHub,上面正式开始。

介绍

gRPC 是 Google 公司基于 Protobuf 开发的跨语言的开源 RPC 框架。gRPC 基于 HTTP/2 协定设计,能够基于一个 HTTP/2 链接提供多个服务,对于挪动设施更加敌对。

入门

首先来看一个最简略的 gRPC 服务,第一步是定义 proto 文件,因为 gRPC 也是 C/S 架构,这一步相当于明确接口标准。

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

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

接下来就是写服务端和客户端的代码,别离实现对应的接口。

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)
    // 注册业务服务
    proto.RegisterGreeterServer(server, &greeter{})

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

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

这样就实现了最根底的 gRPC 服务的开发,接下来咱们就在这个「根底模板」上不断丰富,学习更多个性。

流形式

接下来看看流的形式,顾名思义,数据能够源源不断的发送和接管。

流的话分单向流和双向流,这里咱们间接通过双向流来举例。

proto

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 文件,这里不再多说。

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

在「根底模板」上减少 SayHelloStream 函数,其余都不须要变。

client

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 文件:

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 认证

终于到认证环节了,先看 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

如果用户名和明码正确,则能够失常返回。

单向证书认证

证书认证分两种形式:

  1. 单向认证
  2. 双向认证

先看一下单向认证形式:

生成证书

首先通过 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。

  • key: 服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接管到数据的解密。
  • csr: 证书签名申请文件,用于提交给证书颁发机构(CA)对证书签名。
  • crt: 由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,蕴含证书持有人的信息,持有人的公钥,以及签订者的签名等信息。

gRPC 代码

证书有了之后,剩下的就是革新程序了,首先是服务端代码。

// 证书认证 - 单向认证
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?

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

gRPC 代码

接下来开始批改代码,先看服务端:

// 证书认证 - 双向认证
// 从证书相干文件中读取和解析信息,失去证书公钥、密钥对
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,
})

功败垂成。

Python 客户端

后面曾经说了,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

举荐浏览:

  • 应用 grpcurl 通过命令行拜访 gRPC 服务
  • 举荐三个实用的 Go 开发工具
  • 被 Docker 日志坑惨了
  • 这个 TCP 问题你得懂:Cannot assign requested address

参考文章:

  • https://github.com/mwitkow/go-proto-validators
  • https://github.com/Bingjian-Zhu/go-grpc-example
  • http://gaodongfei.com/archives/start-grpc
  • https://liaoph.com/openssl-san/
  • https://www.cnblogs.com/jackluo/p/13841286.html
正文完
 0