grpc学习

139 阅读8分钟

推荐一个适合入门的学习地址: lixd/grpc-go-example: grpc go example 以及系列教程 (github.com)

下载

  1. protocol编译器
  1. grpc核心库
go get google.golang.org/grpc
  1. 代码生成工具
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

开始

proto文件语法

  1. 说明使用的是proto3语法
syntax = "proto3";
  1. 说明生成的go文件所在目录以及其包名,两个梣属之间用分号;隔开
option go_package = ".;service";
  1. 定义一个服务

服务中的方法可以接受客户端的参数,然后再返回服务端的响应。

service Hello {
    rpc SayHello(Request) returns (Response) {}
}
  1. message关键字,可以理解为go中的结构体

在变量等号后面的数字是指这个变量在这个message中的位置。

message Request {
    string name = 1;
    int64 age = 2;
}
message Response {
    string Msg = 1;
}
  • optional关键字:默认都是optional,在定义中是什么类型就生成什么类型
  • repeated关键字:可重复字段,在go中会生成为切片
  • 嵌套消息:可以在消息中再次嵌套一个消息体,像防御嵌套结构体

生成代码

在编写完proto文件后进行代码生成,进入到proto文件所在的目录后,执行以下命令:

protoc --go_out=. proto文件名
protoc --go-grpc_out=. proto文件名
# -I 参数表示import的proto文件要到哪里寻找
protoc -I ./proto --go_opt=paths=source_relative --go_out=../backend/pb --go-grpc_opt=paths=source_relative --go-grpc_out=../backend/pb proto/*.proto

上述两条命令都接受两个参数,第一个是生成的位置,第二个是指定的proto文件

实现方法

服务端代码实现

  1. 自行编写一个结构体,里面继承了proto生成的代码文件中的Unimplemented+消息,然后在生成的_grpc.pb后缀的文件中将所有在proto文件中定义的方法粘贴下来,修改相应的错误,在自己的这个结构体中实现所有的方法。
定义自己的服务
type myServer struct {
   service.UnimplementedHelloServer
}

// 实现方法
func (myServer) SayHello(ctx context.Context, req *service.Request) (*service.Response, error) {
   msg := fmt.Sprintf("Hello!%v, now I know you are %v! ", req.Name, req.Age)
   return &service.Response{Msg: msg}, nil
}
  1. 在服务端的main函数中开启服务端监听:
listener, _ := net.Listen("tcp", ":9090")
  1. 创建grpc服务端
grpcServer := grpc.NewServer()
  1. 在grpc服务端中注册自己编写的服务:
service.RegisterHelloServer(grpcServer, &myServer{})
  1. 启动服务:
err := grpcServer.Serve(listener)

客户端代码实现

  1. 与服务端地址建立连接
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
   log.Fatalf("did not connevt: %v", err)
}

defer conn.Close()
  1. 建立grpc客户端
client := service.NewHelloClient(conn)
  1. 执行rpc调用
resp, _ := client.SayHello(context.Background(), &service.Request{Name: "LiP", Age: 20})

//对于proto中消息体里面字段的获取,可以使用Get+字段名()获取,grpc框架已经帮我们生成了。(但是为啥不直接 .+字段名获取?)

grpc stream

grpc流式传输适合用于需要服务端和客户端长时间数据交互的场景。

  1. proto文件

与普通的一致,不过在service中rpc函数的入口参数和出口参数需要带上stream关键字,哪个需要流哪个就带上

syntax = "proto3";

option go_package = ".;streamService";


service HelloWorld {
  rpc ServerStreaming (Request) returns (stream Response) {}
  rpc ClientStreaming (stream Request) returns (Response) {}
  rpc Streaming (stream Request) returns (stream Response) {}
}

message Request {
  string name = 1;
  int32 age = 2;
}

message Response {
  string msgHello = 1;
  string msgWelcome = 2;
}
  1. 流类型

分别有三种类型:客户端流,服务端流,双向流。下面依次列出相应代码。

客户端流

  1. 服务端代码:
func (myServer) ClientStreaming(stream pb.HelloWorld_ClientStreamingServer) error {
   var msg string
   for {
      req, err := stream.Recv()
      if err == io.EOF {
         break
      }
      if err != nil {
         return err
      }
      msg = fmt.Sprintf("Now I know you are %v, aged %v.", req.Name, req.Age)
   }
   if err0 := stream.SendAndClose(&pb.Response{MsgHello: msg}); err0 != nil {
      log.Fatalf("err0:%v", err0)
      return err0
   }
   return nil
}

需要用一个循环来一直接收客户端的Request直到io.EOF,在接收到所有信息之后调用SendAndClose来返回Response并关闭客户端输入流

  1. 客户端代码:
func ClientStreaming(client pb.HelloWorldClient) {
   stream, err := client.ClientStreaming(context.Background())
   if err != nil {
      log.Fatalf("err0:", err)
   }

   for i := 0; i < 10; i++ {
      s := strconv.Itoa(i)
      err := stream.Send(&pb.Request{Name: "LiP" + s, Age: 20})
      if err != nil {
         log.Fatalf("err1:", err)
      }
   }

   resp, err := stream.CloseAndRecv()
   if err != nil {
      log.Fatalf("err2:", err)
   }
   fmt.Println(resp.MsgWelcome, resp.MsgHello)
}

在使用循环将信息输入完毕后需要调用CloseAndRecv函数来关闭输入流并接受响应。

服务端流

  1. 服务端代码:
func (myServer) ServerStreaming(req *pb.Request, stream pb.HelloWorld_ServerStreamingServer) error {

   for i := 0; i < 10; i++ {

      if err := stream.Send(&pb.Response{MsgHello: "Hello!" + req.Name, MsgWelcome: "Welcome to my world"}); err != nil {
         log.Fatalf("erroe:%v", err)
         return err
      }
   }

   return nil
}

直接用Send函数发送响应即可。

  1. 客户端代码:
func ServerStreaming(c pb.HelloWorldClient) {
   stream, err := c.ServerStreaming(context.Background(), &pb.Request{Name: "LiP", Age: 20})
   if err != nil {
      log.Fatalf("error: %v", err)
   }

   for {
      resq, err := stream.Recv()
      if err == io.EOF {
         fmt.Println("Got all the messages.")
         break
      }
      if err != nil {
         fmt.Println("err:", err)
         continue
      }
      fmt.Println(resq.MsgHello, resq.MsgWelcome)
   }
}

用循环接收所有信息即可。

双向流

  1. 服务端代码:
func (myServer) Streaming(stream pb.HelloWorld_StreamingServer) error {
   var wg sync.WaitGroup
   msgChan := make(chan string, 20)

   wg.Add(2)
   go func() {
      defer wg.Done()

      for v := range msgChan {
         stream.Send(&pb.Response{MsgHello: "Hello" + v + "!"})
      }

   }()

   go func() {
      defer wg.Done()

      for {
         req, err := stream.Recv()
         if err == io.EOF {
            break
         }
         msgChan <- req.Name
      }
      close(msgChan)
   }()

   wg.Wait()

   return nil
}

启动双协程,一个用于接受一个用于发送,同时利用管道在两个协程之间通信。要记得在接收到io.EOF后需要将管道关闭,否则在读取协程中的管道遍历不会停止。

  1. 客户端代码:
func Streaming(client pb.HelloWorldClient) {
   var wg sync.WaitGroup

   stream, _ := client.Streaming(context.Background())
   wg.Add(2)

   go func() {
      defer wg.Done()
      for {
         req, err := stream.Recv()
         if err == io.EOF {
            fmt.Println("Client Got all the messages.")
            break
         }
         if err != nil {
            fmt.Println("err:", err)
            continue
         }
         fmt.Println(req.MsgHello, req.MsgWelcome)
      }
   }()

   go func() {
      defer wg.Done()
      for i := 0; i < 10; i++ {
         s := strconv.Itoa(i)
         err := stream.Send(&pb.Request{Name: "LiP" + s, Age: 20})
         if err != nil {
            fmt.Println("send err:", err)
            continue
         }
         time.Sleep(time.Second)
      }
      stream.CloseSend()

   }()

   wg.Wait()
}

与服务端相似,但是最后需要用CloseSend关闭输入流。

总结

在流中,只有客户端的流需要调用函数CloseAndRecvCloseSend手动将其关闭,服务端的不需要。

grpc的认证

grpc的认证指的是多个server和多个client之间,如何识别对方是谁,并且可以安全的进行数据传输

  • SSL/TLS认证方式(采用http2协议)
  • 基于Token的认证方式(基于安全连接)
  • 不采用任何措施连接,不安全(默认采用http1) -自定义的身份认证

SSL/TLS认证方式

TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,前身是SSL(Secure Socket Layer),它实现了将用一次的报文进行加密后再交由TCP进行传输的功能。 TLS协议主要解决如下三个网络安全问题:

  1. 保密,通过加密encryption实现,所有信息都加密传输。
  2. 完整性,通过MAX校验机制,一旦被篡改,通信双方会立刻发现
  3. 认证,双方认证,双方都可以配备证书,防止身份被冒充
  • KEY:服务器上的私钥文件,用于发送给客户端数据的加密,以及对客户端收到的数据的解密
  • CSR:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名
  • CRT:有证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息
  • PEM:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER

使用openssl生成证书

首先通过openssl生成证书和私钥

前提
  1. 下载:slproweb.com/products/Wi…
  2. 配置环境变量
  3. 命令行测试openssl
生成证书

在go项目中新建一个目录专门用于存放证书文件等,所有命令都在这个目录下执行

  1. 生成私钥
openssl genrsa -out server.key 2048

其中server.key为输出的文件名

  1. 生成证书
openssl req -new -x509 -key server.key -out server.crt -days 36500

其中server.key为上面生成的私钥key文件,server.crt为生成的证书文件,-days 36500为证书的有效期。

输入指令后会有以下信息需要填写,可以全部回车,表示留空

# 国家名字
Country Name (2 letter code) [AU]:CN

# 省名
State or Province Name (full name) [Some-State]:HuBei

# 城市名
Locality Name (eg, city) []:Wuhan

# 组织名
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Huake

# 部门名
Organizational Unit Name (eg, section) []:Dian

# 服务器/网站名
Common Name (e.g. server FQDN or YOUR name) []:GRPCLearn

# 邮箱
Email Address []:3192997488@qq.com
  1. 生成csr
openssl req -new -key server.key -out server.csr
  1. 修改配置文件
  • openssl/bin目录下的openssl.cnfopenssl.cfg文件复制到当前目录下
  • 打开copy_extensions = copy
  • 打开req_extensions = v3_req
  • [v3_req]中添加`subjectAltName = @alt_names
  • 在文件的最后写下
[ alt_names ]
DNS.1 = *.dian.com

最后一个操作写的是域名,通过哪个域名可以访问到代码 5. 生成证书私钥test.key

openssl genpkey -algorithm RSA -out test.key
  1. 通过私钥test.key生成证书请求文件test.csr
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cfg -extensions v3_req
  1. 生成SAN证书pem
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req
  1. 梳理 最终使用到的只有test.keytest.pem,其他文件都是签发证书需要

  2. 在服务端加入证书

creds, errC := credentials.NewServerTLSFromFile("../key/test.pem", "../key/test.key")
if errC != nil {
   log.Fatalf("did not connevt: %v", errC)
}

然后在创建grpc服务端时加上证书信息

grpcServer := grpc.NewServer(grpc.Creds(creds))
  1. 在客户端加上证书
creds, _ := credentials.NewClientTLSFromFile("E:\GoProject\gRPCStudy\key\test.pem", "*.dian.com")

在连接时带上证书信息

conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(creds))

自定义token验证

  1. 在客户端创建一个结构体,实现两个方法:
// map中要包含有需要验证的k-v对,如id,token等等
func GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)

// 如果需要证书验证则返回true,否则返回false
func RequireTransportSecurity() bool
  1. 在服务端进行token的验证
// 此处获取到的token就是上面自己写的写的map
token, ok := metadata.FromIncomingContext(ctx)
if !ok {
    ....
}
....