go proto, grpc复习笔记

1,023 阅读8分钟

使用指南

本文作为对一些自己没有深究过的内容做一个索引和复习,内容可能包含官方文档,培训课,公开课,博客,著名程序员cv&debug网站等等。

安装略

简单的proto文件定义

语法(略)

一般报这种错误的话,多半是/usr/local/include路径下缺失了必要文件 两种解决办法

  • empty.proto copy的本地 例如empty.proto 编译时指定empry.proto 的-I=xxx的绝对路径

image.png

  • 把这些需要的*.proto,例如empry.proto,copy到/usr/include/路径下
user.proto:2:1: Import "google/protobuf/empty.proto" was not found or had errors.

image.png 你可以很容易找到参考1的翻译版本。本文使用的是proto3。

简单定义

hello.proto定义如下

syntax = "proto3";
option go_package="xxxmodule/api/gen/v1;hello";

message HelloRequest {
  int64 a = 1;
  string b = 2;

}
message HelloResponse {
  string reply = 1;
}
service AuthService {
  rpc Login (HelloRequest) returns (HelloResponse);
}

现在option go_package是必须要指定的了,不然你会收获下面的提示。

image.png

一些需要注意的地方

image.png

编译命令

先从简单的命令入手。在hello.proto的同级目录

protoc  --go_out=.  hello.proto

--go_out表示输出的目录,这里是输出到到当前目录下, 会输出hello.pb.go。

进阶一点,依旧是同级目录。

 mkdir -p api
 protoc -I=. --go_out=plugins=grpc,paths=source_relative:./api hello.proto 

-I表示.proto文件的目录,--go_out=plugins=grpc参数来生成proto中的定义的Service部分的grpc代码,如果不加plugins=grpc,就只生成message数据。这里使用plugins,也就是protoc-gen-go,是因为ProtoBuf本身不支持Go语言,所以我们需要protoc-gen-go来协助我们。paths=souce_relative:指定了要输出的路径为proto文件同级路径下的api目录(这个目录必须存在),hello.proto指定proto目录下的输入文件。 但这里有一个容易犯错的地方source_relative:后面的路径是从你执行这个命令的路径开始,而不是从你指定的proto文件目录开始。 你可以试一试在上一级目录执行这个命令

protoBuf 参数 (略)

依然可以在文章1中找到

protoc-gen-go 的参数

要将额外的参数传递给插件,使用逗号分隔的参数列表,以冒号与输出目录分隔:

protoc --go_out=plugins=grpc,import_path=mypackage:. *.proto
  • paths=(import | source_relative) 包含两个可选命令,没有指定proto中的go_package时就需要import。source_relative 与输入文件.proto同级的路径。
  • plugins=plugin1+plugin2 指定要加载的子插件列表。这个 repo 中唯一的插件是grpc.
  • Mfoo/bar.proto=quux/shme M参数,指定.proto文件编译后的包名(foo/bar.proto编译后为包名为quux/shme) 以下参数已弃用,不应使用:
  • import_prefix=xxx - 添加到所有导入开头的前缀。
  • import_path=foo/bar- 如果没有输入文件声明,则用作包go_package。如果它包含斜杠,则忽略最右边斜杠之前的所有内容。

mac 踩坑

user.proto:2:1: Import "google/protobuf/empty.proto" was not found or had errors.

一般报这种错误的话,多半是/usr/local/include路径下缺失了必要文件 两种解决办法

  • empty.proto copy的本地 例如empty.proto 编译时指定empry.proto 的-I=xxx的绝对路径

image.png

  • 把这些需要的*.proto,例如empry.proto,copy到/usr/include/路径下
user.proto:2:1: Import "google/protobuf/empty.proto" was not found or had errors.

image.png 保证protoc和golang/protobuf版本一致即可。

容易产生困惑的地方

如果到这里好巧不巧你也搜到了文章1,顺着官网的quick_start做入门。那你大概率可能会遇到到几种写法了。此时你就已经满脸黑人问号了。我到底该用哪种啊。

image.png

假设你阅读过参考文章的1,2,3。那么有大概有如下几种写法。

  1. 本文一开始介绍的简单例子的进阶版
protoc --go_out=paths=source_relative:./gen -I. authenticator.proto
  1. Go Generated Code给出的版本
protoc --go_out=plugins=grpc:./gen --go_opt=paths=source_relative authenticator.proto
  • --go_opt通过flag变量 source_relative 参数控制protoc编译出的相对路径为./gen
  1. 新版本grpc quick_start的例子
protoc --go-grpc_out=./gen --go-grpc_opt=paths=source_relative authenticator.proto
  1. 以及我使用的与1类似的写法,当然这几种都是等价的。 之后包括grpc-gateway的编译,我都使用这种方式
protoc -I=$PROTO_PATH --go_out=plugins=grpc,paths=source_relative:$GO_OUT_PATH ${DOMAIN}.proto

着实心累。老老实实proto里option go_package写上。也不需要什么M参数指定--go_opt=M${PROTO_FILE}=${GO_IMPORT_PATH}。用法4编译就完事啦。

gRPC的四种通信模式

简单模式(Simple RPC)

这种模式最为传统,即客户端发起一次请求,服务端响应一个数据,这和大家平时熟悉的RPC没有什么大的区别,所以不再详细介绍。

服务器端流 RPC(Server Sreaming RPC)

这种模式是客户端发起一次请求,服务端返回一段连续的数据流。典型的例子是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。

客户端流 RPC(Client Streaming RPC)

与服务端数据流模式相反,这次是客户端源源不断的向服务端发送数据流,而在发送结束后,由服务端返回一个响应。典型的例子是物联网终端向服务器报送数据。

双向流 RPC(Bidirectional Streaming RPC)

顾名思义,这是客户端和服务端都可以向对方发送数据流,这个时候双方的数据可以同时互相发送,也就是可以实现实时交互。典型的例子是聊天机器人。

代码示例

官方文档

简单模式

.proto

syntax = "proto3";
option go_package = ".;hello";
service Test {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

client

package main

import (
   "context"
   "fmt"
   hello "go-interview/grpc"
   "google.golang.org/grpc"
)

func main() {
   conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure())
   if err != nil {
      panic(err)
   }
   defer conn.Close()
   c := hello.NewTestClient(conn)
   r, err := c.SayHello(context.Background(), &hello.HelloRequest{Name: "server"})
   if err != nil {
      panic(err)
   }
   fmt.Println(r.Message)
}

server

package main

import (
   "context"
   "fmt"
   hello "go-interview/grpc"
   "google.golang.org/grpc"

   "net"
)

type Server struct {
    //hello.UnimplementedTestServer //新版本的protoc编译后如需测试需要实现Server下的所有接口,为了方便测试就可以先引入这一条。
}

func (s *Server) SayHello(ctx context.Context, request *hello.HelloRequest) (*hello.HelloReply, error) {
   return &hello.HelloReply{Message: "Hello " + request.Name}, nil
}

func main() {
   g := grpc.NewServer()
   s := Server{}
   hello.RegisterTestServer(g, &s)
   lis, err := net.Listen("tcp", fmt.Sprintf(":8080"))
   if err != nil {
      panic("failed to listen: " + err.Error())
   }
   g.Serve(lis)
}

编译命令

protoc -I . xxx.proto --go_out=plugins=grpc:.

流模式

proto

syntax = "proto3";//声明proto的版本 只能 是3,才支持 grpc

//声明 包名
option go_package=".;proto";

//声明grpc服务
service Greeter {
  /*
  以下 分别是 服务端 推送流, 客户端 推送流 ,双向流。
  */
  rpc GetStream (StreamReqData) returns (stream StreamResData){}
  rpc PutStream (stream StreamReqData) returns (StreamResData){}
  rpc AllStream (stream StreamReqData) returns (stream StreamResData){}
}


//stream请求结构
message StreamReqData {
  string data = 1;
}
//stream返回结构
message StreamResData {
  string data = 1;
}

client

package main

import (
   "google.golang.org/grpc"
   proto "ttt"

   "context"
   _ "google.golang.org/grpc/balancer/grpclb"
   "log"
   "time"
)

const (
   ADDRESS = "localhost:50052"
)


func main(){
   //通过grpc 库 建立一个连接
   conn ,err := grpc.Dial(ADDRESS,grpc.WithInsecure())
   if err != nil{
      return
   }
   defer conn.Close()
   //通过刚刚的连接 生成一个client对象。
   c := proto.NewGreeterClient(conn)
   //调用服务端推送流
   reqstreamData := &proto.StreamReqData{Data:"aaa"}
   res,_ := c.GetStream(context.Background(),reqstreamData)
   for {
      aa,err := res.Recv()
      if err != nil {
         log.Println(err)
         break
      }
      log.Println(aa)
   }
   //客户端 推送 流
   putRes, _ := c.PutStream(context.Background())
   i := 1
   for {
      i++
      putRes.Send(&proto.StreamReqData{Data:"ss"})
      time.Sleep(time.Second)
      if i > 10 {
         break
      }
   }
   //服务端 客户端 双向流
   allStr,_ := c.AllStream(context.Background())
   go func() {
      for {
         data,_ := allStr.Recv()
         log.Println(data)
      }
   }()

   go func() {
      for {
         allStr.Send(&proto.StreamReqData{Data:"ssss"})
         time.Sleep(time.Second)
      }
   }()

   select {
   }

}

server

package main

import (
   "fmt"
   "google.golang.org/grpc"
   "log"
   "net"
   proto "ttt"

   "sync"
   "time"
)

const PORT  = ":50052"

type server struct {
}

//服务端 单向流
func (s *server)GetStream(req *proto.StreamReqData, res proto.Greeter_GetStreamServer) error{
   i:= 0
   for{
      i++
      res.Send(&proto.StreamResData{Data:fmt.Sprintf("%v",time.Now().Unix())})
      time.Sleep(1*time.Second)
      if i >10 {
         break
      }
   }
   return nil
}

//客户端 单向流
func (s *server) PutStream(cliStr proto.Greeter_PutStreamServer) error {

   for {
      if tem, err := cliStr.Recv(); err == nil {
         log.Println(tem)
      } else {
         log.Println("break, err :", err)
         break
      }
   }

   return nil
}

//客户端服务端 双向流
func(s *server) AllStream(allStr proto.Greeter_AllStreamServer) error {

   wg := sync.WaitGroup{}
   wg.Add(2)
   go func() {
      for {
         data, _ := allStr.Recv()
         log.Println("收到客户端的消息", data.Data)
      }
      wg.Done()
   }()

   go func() {
      for {
         allStr.Send(&proto.StreamResData{Data:"我是服务器"})
         time.Sleep(time.Second)
      }
      wg.Done()
   }()

   wg.Wait()
   return nil
}

func main(){
   //监听端口
   lis,err := net.Listen("tcp",PORT)
   if err != nil{
      panic(err)
      return
   }
   //创建一个grpc 服务器
   s := grpc.NewServer()
   //注册事件
   proto.RegisterGreeterServer(s,&server{})
   //处理链接
   err = s.Serve(lis)
   if err != nil {
      panic(err)
   }
}

需要注意的改动

我文中都是基于protoc 3.11构建的。如果是最新那么,server端定义空struct需要嵌入UnimplementedXXX hello.UnimplementedTestServer

metadata

官方文档

1. 新建metadata

type MD map[string][]string

创建的时候可以像创建普通的map类型一样使用new关键字进行创建:

//第一种方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第二种方式 key不区分大小写,会被统一转成小写。
md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
    "key2", "val2",
)

2. 发送metadata

md := metadata.Pairs("key", "val")

// 新建一个有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 单向 RPC
response, err := client.SomeRPC(ctx, someRequest)

3. 接收metadata

func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

4. grpc中使用metadata

待补充

interceptor 拦截器

proto

syntax = "proto3";
option go_package = ".;proto";
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}
//将 sessionid放入 放入cookie中 http协议
message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

client

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"time"

	"start/grpc_interceptor/proto"
)

func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	start := time.Now()
	err := invoker(ctx, method, req, reply, cc, opts...)
	fmt.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
	return err
}

func main(){
	//stream
	var opts []grpc.DialOption

	opts = append(opts, grpc.WithInsecure())
	// 指定客户端interceptor
	opts = append(opts, grpc.WithUnaryInterceptor(interceptor))

	conn, err := grpc.Dial("localhost:50051", opts...)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)
	r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name:"bobby"})
	if err != nil {
		panic(err)
	}
	fmt.Println(r.Message)
}

server

package main

import (
	"context"
	"fmt"
	"net"

	"google.golang.org/grpc"

	"start/grpc_interceptor/proto"
)


type Server struct{}

func (s *Server) SayHello(ctx context.Context, request *proto.HelloRequest) (*proto.HelloReply,
	error){
	return &proto.HelloReply{
		Message: "hello "+request.Name,
	}, nil
}


func main(){
	var interceptor grpc.UnaryServerInterceptor
	interceptor = func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
		// 继续处理请求
		fmt.Println("接收到新请求")
		res, err := handler(ctx, req)
		fmt.Println("请求处理完成")
		return res, err
	}
	var opts []grpc.ServerOption
	opts = append(opts, grpc.UnaryInterceptor(interceptor))

	g := grpc.NewServer(opts...)
	proto.RegisterGreeterServer(g, &Server{})
	lis, err := net.Listen("tcp", "0.0.0.0:50051")
	if err != nil{
		panic("failed to listen:"+err.Error())
	}
	err = g.Serve(lis)
	if err != nil{
		panic("failed to start grpc:"+err.Error())
	}
}


状态码

文档

snap-code

健康检查

文档

  1. GRPC 作为 RPC 服务,跟普通的 RPC 服务类似,一个 health check API 来检测是否可以正常返回。类似: ping => pong ;
  2. 它可以做的更丰富,比如检测每个服务(rpc service)的健康状态;
  3. 作为 GRPC 服务,它可以重用现有的账单计费,配合等基础架构等。因为服务器可以完全控制运行状态以及检测服务的访问;
  4. 搭配诸如consul等,注册grpc的健康检查来检测服务运行是否正常。 .proto
syntax = "proto3";

package grpc.health.v1;

message HealthCheckRequest {
  string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;  // Used only by the Watch method.
  }
  ServingStatus status = 1;
}

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);

  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}

在grpc-go中的使用

healthcheck := health.NewServer()
healthpb.RegisterHealthServer(s, healthcheck)

服务注册 & 负载均衡

主要是实现grpc的resolver 和 balance 接口。

参考

  1. Go Generated Code
  2. Go support for Protocol Buffers
  3. protoc go插件编写之四 (实现生成自己的proto文件)
  4. stackoverflow.com/questions/6…
  5. grpc.io/docs/langua…
  6. gRPC模式简介
  7. gRPC健康检测
  8. 基于gRPC的注册发现与负载均衡的原理和实战
  9. gRPC 源码分析之 Resolver 篇