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

创新互联从2013年成立,是专业互联网技术服务公司,拥有项目网站建设、成都网站设计网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元洛浦做网站,已为上家服务,为洛浦各地企业和个人服务,联系电话:18980820575
作为一名程序员,学就对了。
之前用 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,小伙伴们可以一遍看源码,一遍对照文章内容来学习。
                分享名称:gRPC这项技术真是太棒了,爆赞
                
                分享URL:http://www.csdahua.cn/qtweb/news11/73011.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网