【Go学习笔记】grpc和 grpc gateway

protobuf

什么是 Protocol Buffers

Protocol Buffers(简称 Protobuf)是由 Google 开发的一种语言中立平台中立可扩展序列化结构数据的方法。Protobuf 在数据存储和通信协议中非常高效,广泛应用于分布式系统、网络通信、配置文件等场景。

Protobuf 的核心理念是定义数据结构,然后使用编译器生成代码,这些代码可以在不同的编程语言中使用,从而使得数据的序列化和反序列化变得非常高效

其优势主要体现在:

  • 高效:Protobuf 使用紧凑的二进制格式,序列化和反序列化速度非常快,数据传输的体积也较小。
  • 跨平台:支持多种编程语言,能够在不同的平台之间无缝传输数据。
  • 可扩展:允许在不破坏现有数据结构的情况下添加新的字段,这使得它非常适合长期维护和迭代开发。
  • 类型安全:定义了明确的数据模型,减少了数据传输中的歧义。

protobuf 基本概念

Protobuf 使用 .proto 文件来定义数据的结构。在这个文件中,你可以定义消息(message)每个消息包含多个字段,每个字段都有一个唯一的编号。这些编号用于在序列化和反序列化过程中识别字段。
Protobuf 有两种版本的语法:proto2 和 proto3。目前,proto3 更常用。

protobuf 命令

  • protoc :protobuf 编译器命令,用于将 .proto 文件编译并生1成目标语言的代码;
  • -I directory/to/proto/path :指定 .proto 文件的搜索路径。-I 选项告诉 protocdirectory/to/proto/file 目录中查找 .proto 文件。

普通 pb 文件:

  • --go_out directory/to/pb/path :指定生成文件的输出路径。生成 Go 语言的 Protobuf 文件,并将生成的文件放在 directory/to/pb/path 目录中。
  • --go_opt paths=source_relative :指定生成的文件路径为相对路径,这样生成的文件路径会保留原始的目录结构。
    如果要生成别的语言文件,将 --go_out 改为 --java_out--cpp_out / --python_out / --js_out

gRPC 的 pb 文件:

  • --go-grpc_out directory/to/pb/grpc/path :指定生成 Go 语言的 gRPC 服务路径。生成 Go 语言的 gRPC 服务代码,并将生成的文件放在 directory/to/pb/grpc/path 目录中。
  • --go-grpc_opt paths=source_relative :指定生成的文件路径为相对路径。

gRPC gateway 的 pb 文件:

  • --grpc-gateway_out directory/to/pb/grpc/gateway/path :指定生成 gRPC-Gateway 代理代码路径。生成 gRPC-Gateway 代理代码,并将生成的文件放在 directory/to/pb/grpc/gateway/path 目录中。
  • --grpc-gateway_opt paths=source_relative :指定生成的文件路径为相对路径。

指定插件的选项:

  • --plugin

Protobuf 语法

声明版本
syntax = "proto3";    // 告诉编译器使用 proto3 版本
声明包
package person;
声明选项(golang的包路径、包别名)
option go_package="github/grpcStudy/pb/person;person";
声明消息体

使用 message 定义消息体,消息体命名大写,每一个属性后面的数字表示其唯一标识符。

	message Person{
	  string name = 1;
	  int32 age = 2;
	  bool sex = 3;
	}
  • 对于单个类型,protobuf 官网给出了 protobuf 类型与各语言的类型对照表
    protobuf 为每个字段提供的默认值:
    类型默认值
    string""
    byte''
    boolfalse
    numeric(数字)0
    enum第一个枚举值(0
    对象类型随语言而定
  • 对于切片类型,通过前面添加关键字 repeated 示意这是一个重复字段,表示切片。
  • 对于 map 类型,定义 map<TYPE, TYPE> 即可。
  • 对于嵌套类型,直接使用即可。也支持嵌套定义 message TYPE {...} 一个新类型,并直接在此 message 内部使用。
    完整示例:
syntax = "proto3";    // 告诉编译器使用 proto3 版本

package person;

option go_package="github/grpcStudy/pb/person;person";

message Class {
  repeated Person persons = 1;
  message  Teacher  {
    string name = 1;
  }
  Teacher teacher = 2;
}

message Person{
  string name = 1;
  int32 age = 2;
  bool sex = 3;
  repeated string hobbies = 4;
  map<string, string> scores = 5;
}
预留字段名、唯一标识值

可以通过关键字 reserved 来提前声明保留一个字段名或唯一标识值,在使用 reserved 提前声明的情况下,这个字段名/唯一标识值会被不允许使用。

message Person{
  string name = 1;
  int32 age = 2;
  bool sex = 3;
  repeated string hobbies = 4;
  map<string, string> scores = 5;

  reserved "futureField";
  reserved 10 to 19;
}

在这种情况下,Person 消息体不允许出现名称为 futureField 的属性名和 10-19 的唯一标识。
在这里插入图片描述
在这里插入图片描述

声明枚举
message Person{
  string name = 1;
  int32 age = 2;
  enum SEX {
    MALE = 0;
    FEMALE = 1;
  }
  SEX sex = 3;
  repeated string hobbies = 4;
  map<string, string> scores = 5;

  reserved "futureField";
  reserved 10 to 19;
}

❗注:枚举定义中必须有 0 值。

如果需要在枚举中设置别名,可以通过 option allow_alias = true 来设置,例如:

message Person{
  string name = 1;
  int32 age = 2;
  enum SEX {
    option allow_alias = true;
    MALE = 0;
    FEMALE = 1;
    BOY = 0;
    GIRL = 1;
  }
  SEX sex = 3;
  repeated string hobbies = 4;
  map<string, string> scores = 5;

  reserved "futureField";
  reserved 10 to 19;
}
oneof 属性

oneof 是一种特殊的字段集合,表示同一时间只能设置其中的一个字段。它类似于 C 语言中的 union,用于节省内存并防止同一时间设置多个互斥字段。
其作用有:

  • 节省内存:因为 oneof 中同时只能有一个字段被设置,所以在内存中只需要分配一个字段的空间。
  • 数据完整性:防止同时设置多个互斥字段,确保数据的一致性和完整性。
  • 简化逻辑:在处理互斥字段时,简化了代码逻辑,不需要额外的检查和处理。
  • 示例
    为上面的 Person 声明一个属性 Married
    message Person{
      string name = 1;
      int32 age = 2;
      enum SEX {
        option allow_alias = true;
        MALE = 0;
        FEMALE = 1;
        BOY = 0;
        GIRL = 1;
      }
      SEX sex = 3;
      repeated string hobbies = 4;
      map<string, string> scores = 5;
    
      oneof Married {
        bool isMarried = 6;
        bool notMarried = 7;
      }
    
      reserved "futureField";
      reserved 10 to 19;
    }
    
    想要表达的意思是:这个人要么 isMarried 要么 notMarried
    在 Go 项目中使用它:
    func main() {
    	person := &personpb.Person{
    		Name:    "Alice",
    		Age:     20,
    		Sex:     personpb.Person_MALE,
    		Hobbies: []string{"reading", "painting"},
    		Scores:  map[string]string{"math": "A", "english": "B"},
    		Married: &personpb.Person_IsMarried{
    			IsMarried: true,
    		},
    	}
    	fmt.Println(person)
    }
    
导入其它 .proto 中的消息类型

通过 import "" 语法进行导入。
需要注意的是,这里写的相对路径并不是以当前 .proto 文件为基准,是以编译时命令的选项 --proto_path 的值作为基准目录,如果没有设置该选项,会将运行命令的目录作为基准目录。

声明服务
  • 服务声明的语法:
    service SERVICE_NAME {
    	rpc FUNCTINON_NAME(Person) returns (Person);   // 传统的 即刻相应的
    	rpc FUNCTINON_NAME_IN(stream Person) returns (Person);    // 入参为流
    	rpc FUNCTINON_NAME_OUT(Person) returns (stream Person);    // 出参为流
    	rpc FUNCTINON_NAME_IO(stream Person) returns (stream Person);    // 出入参都为流
    }
    

以上代码使用了声明 rpc 方法的四种方式。
声明服务后,在 protobuf 编译后会新生成 _grpc.pb.go 的文件,作为使用 ServerClient 的源代码。

grpc Server 的使用

普通服务

没什么特别的,要熟练掌握编写客户端和服务端的初始化代码。

  • 示例
    person.proto
syntax = "proto3";    // 告诉编译器使用 proto3 版本

package person;

option go_package="github/grpcStudy/pb/personpb;personpb";

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

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

service SearchService {
  rpc Search(PersonReq) returns (PersonRes);    // 传统的 即刻相应的
  rpc SearchIn(stream PersonReq) returns (PersonRes);   // 入参为流
  rpc SearchOut(PersonReq) returns (stream PersonRes);   // 出参为流
  rpc SearchIO(stream PersonReq) returns (stream PersonRes);    // 入参、出参均为流
}

server/main.go (服务端):

package main

import (
	"context"
	"google.golang.org/grpc"
	personpb "grpcStudy2/pb/person"
	"log"
	"net"
)

type personServer struct {
	personpb.UnimplementedSearchServiceServer
}

func (*personServer) Search(ctx context.Context, req *personpb.PersonReq) (*personpb.PersonRes, error) {
	name := req.GetName()
	age := req.GetAge()
	res := &personpb.PersonRes{
		Name: "Got" + name,
		Age:  age,
	}
	return res, nil
}
func (*personServer) SearchIn(grpc.ClientStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
	return nil
}
func (*personServer) SearchOut(*personpb.PersonReq, grpc.ServerStreamingServer[personpb.PersonRes]) error {
	return nil
}
func (*personServer) SearchIO(grpc.BidiStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
	return nil
}

func main() {
	l, err := net.Listen("tcp", ":8888")
	if err != nil {
		log.Fatal(err)
	}

	s := grpc.NewServer()
	personpb.RegisterSearchServiceServer(s, &personServer{})
	err = s.Serve(l)
	if err != nil {
		log.Fatal(err)
	}
}

client/main.go (客户端):

package main

import (
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	personpb "grpcStudy2/pb/person"
	"log"
)

func main() {
	conn, err := grpc.NewClient("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	client := personpb.NewSearchServiceClient(conn)

	// 普通 gRPC 服务
	req, err := client.Search(context.Background(), &personpb.PersonReq{Name: "Alice", Age: 24})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(req)
}
流式传入

流式传入,是针对服务端来说的,客户端发送流,服务端接收流,并且在停止接收流的时候服务端会给客户端一个流的反馈。

服务端

对于服务端来说,实现一个流失传入的方法时,会接收到一个 grpc.ClientStreamingServer[Req any, Res any] 的参数(Req 和 Res 是在 proto 中定义的),这个参数有以下方法:
在这里插入图片描述

  • Recv 表示接收(Receive)。
    Recv 需要在无限循环的 for 中读取。
    for {
    	req, err := server.Recv()
    	fmt.Println(req)
    	if err != nil {
    		server.SendAndClose(&personpb.PersonRes{Name: "Error receiving request", Age: -1})
    		break
    	}
    }
    
  • SendAndClose 表示发送一条信息表示流结束。
    完整服务端方法示例:
    func (*personServer) SearchIn(server grpc.ClientStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
    	for {
    		req, err := server.Recv()
    		fmt.Println(req)
    		if err != nil {
    			server.SendAndClose(&personpb.PersonRes{Name: "Error receiving request", Age: -1})
    			break
    		}
    	}
    	return nil
    }
    
客户端

在客户端调用流式传入的方法时,会得到一个 grpc.ClientStreamingClient[Req any, Res any] 类型的对象,该对象有以下方法:
在这里插入图片描述

  • Send 表示发送信息
  • CloseAndRecv 表示关闭请求流并等待服务端的返回。
    完整客户端请求代码:
    // 流式传入
    c, err := client.SearchIn(context.Background())
    if err != nil {
    	log.Fatal(err)
    }
    for i := 0; i < 10; i++ {
    	time.Sleep(1 * time.Second)
    	err = c.Send(&personpb.PersonReq{Name: fmt.Sprintf("Person%d", i), Age: 25})
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    res, err := c.CloseAndRecv()
    fmt.Println("CloseAndRecv:", res)
    
流式返回

做法跟上面基本相同,具体可以通过源码看到,直接写代码:

  • 服务端:
    func (*personServer) SearchOut(req *personpb.PersonReq, server grpc.ServerStreamingServer[personpb.PersonRes]) error {
    	name := req.GetName()
    	age := req.GetAge()
    
    	for i := 0; i < 10; i++ {
    		time.Sleep(1 * time.Second)
    		server.Send(&personpb.PersonRes{Name: fmt.Sprintf("Person%d%s", i, name), Age: age})
    	}
    	return nil
    }
    
  • 客户端:
    // 流式返回
    c, err := client.SearchOut(context.Background(), &personpb.PersonReq{Name: "Hreate", Age: 26})
    if err != nil {
    	log.Fatal(err)
    }
    for {
    	res, err := c.Recv()
    	if err != nil {
    		if err == io.EOF {
    			fmt.Println("Got All Information!!!")
    		} else {
    			log.Fatal(err)
    		}
    		break
    	}
    	fmt.Println("Got Res:", res)
    }
    
流式出入(双向流)
服务端:
  • **第一版本:**Go 语言新手写的服务端
    func (*personServer) SearchIO(server grpc.BidiStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
    	info := make(chan string)
    	go func() {
    		for {
    			req, err := server.Recv()
    			if err != nil {
    				info <- "Stream End"
    				if err == io.EOF {
    					fmt.Println("Got All Information!!!")
    					break
    				} else {
    					log.Fatal(err)
    				}
    			}
    			info <- req.Name
    			fmt.Println("Server Got Info:", req)
    
    		}
    	}()
    	for {
    		s := <-info
    		if s == "Stream End" {
    			break
    		}
    		err := server.Send(&personpb.PersonRes{Name: s, Age: 23})
    		if err != nil {
    			if err == io.EOF {
    				fmt.Println("Server Send Ending...")
    				break
    			} else {
    				log.Fatal(err)
    			}
    		}
    	}
    	return nil
    }
    
    从该代码片段可以看出思路:
    • 服务端通过启动一个 goroutine 来接收客户端数据流,并通过一个管道 info 来将数据传出;
    • 下面再通过一个循环来将模拟,从管道中获取数据,并进行相应处理,然后返回给客户端;

但是,这段代码的写法并不具有 Go 语言的标准风格,改良后:

  • 第二版本:具有 Go 标准风格的服务端
func (*personServer) SearchIO(server grpc.BidiStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
	reqChan := make(chan *personpb.PersonReq)
	errChan := make(chan error)
	go func() {
		for {
			req, err := server.Recv()
			if err != nil {
				if err == io.EOF {
					// 如果请求流结束, 关闭 reqChan 管道并结束本 goroutine
					close(reqChan)
					return
				}
				errChan <- err
			}
			reqChan <- req
		}
	}()
	for {
		select {
		case req, ok := <-reqChan:
			if !ok {
				// 如果 reqChan 管道关闭,表示请求流结束
				fmt.Println("Got All Information!!!")
			}
			fmt.Println("Server Got Info:", req)
			err := server.Send(&personpb.PersonRes{Name: req.Name, Age: req.Age})
			if err != nil {
				return err
			}
		case err := <-errChan:
			return err
		}
	}
}

可以看到,比上面的版本好的不止一星半点,体现在如下几个方面:

  • 第一个 goroutine 不做任何异常处理,而是通过开启一个 errChan 管道来传递异常,由主 goroutine 来统一处理异常;
  • 请求流结束时,直接通过 close(reqChan) 关闭管道,而不是传一个字符串(不够具有可扩展性);
  • 主 goroutine 通过使用 select 针对两个管道可以非常方便的处理,即:
  • 如果第一个 goroutine 正常获取到了 req,会进入第一个 case ,进行相应的处理即可返回;
  • 如果第一个 goroutine 出现了异常,只有可能是未知异常,因为如果是请求流结束,reqChan 会直接关闭掉,而不会发送错误给 errChan ,所以主 goroutine 就可以直接将异常返回。
客户端:
// 流式出入
c, err := client.SearchIO(context.Background())
if err != nil {
	log.Fatal(err)
}

wg := sync.WaitGroup{}
wg.Add(2)
go func() {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		time.Sleep(1 * time.Second)
		err = c.Send(&personpb.PersonReq{Name: "Hreate", Age: 28})
		if err != nil {
			log.Fatal(err)
		}
	}
	err = c.CloseSend()
	if err != nil {
		log.Fatal(err)
	}
}()

go func() {
	defer wg.Done()
	for {
		res, err := c.Recv()
		if err != nil {
			if err == io.EOF {
				fmt.Println("Got All Information!!!")
				break
			} else {
				log.Fatal(err)
			}
		}
		fmt.Println("Client Got Res:", res)
	}
}()

wg.Wait()
  • 客户端通过启动两个 goroutine 来控制请求流和返回流,并通过 WaitGroup 来控制等待两个 goroutine 完成再结束此方法。
  • 请求流限制发送 10 次。
  • 返回流的处理对 err 进行判断,如果是 io.EOF 则表示返回流结束,退出该 goroutine

❗记得使用 WaitGroup 时,最好的方式是在启动每一个 goroutine 时,使用 defer wg.Done()

grpc gateway

gRPC-Gateway 是一个用于将 gRPC 服务转化为 JSON/HTTP API 的开源项目。它使得你可以在同一个服务中同时提供 gRPC 和 RESTful 接口,从而方便地集成到使用 HTTP/JSON 的现有系统中。

grpc gateway 的使用

安装 grpc gateway 的 Go 依赖

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest

然后添加包依赖

go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest

添加 gRPC-Gateway 的扩展选项

在 .proto 文件中引入 gRPC-Gateway annotations,并添加 gRPC-Gateway 的扩展选项。
参考官网:添加 grpc gatewat 扩展选项
官网中有写到,这个 proto 文件定义了如何将 gRPC 服务映射到 JSON 请求和返回值,所以需要将这两个 .proto 文件下载并放到目录下。
最终的目录结构:
在这里插入图片描述

service SearchService {
  rpc Search(PersonReq) returns (PersonRes){
    option (google.api.http) = {
      post: "/api/person",
      body: "*"
    };
  };    // 传统的 即刻响应的
}

这里的 option (google.api.http) 是一个扩展选项,表示这个 gRPC 方法可以通过 HTTP 进行访问

运行 protoc 命令,生成 gRPC 代码

在 pb 目录下运行:

protoc --go_out . --go_opt paths=source_relative  --go-grpc_out . --go-grpc_opt paths=source_relative  --grpc-gateway_out . --grpc-gateway_opt paths=source_relative ./person/person.proto

生成代码:
在这里插入图片描述
其中的 .gw.go 文件就是 gateway 文件。

在服务端创建一个 http 服务

创建一个 gRPC 连接

既然要把 grpc 映射到 HTTP 供外部访问,那当然要先连接到这个 grpc 服务了。

conn, err := grpc.NewClient("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
创建一个 ServeMux 实例,用于处理 HTTP 请求

gRPC-Gateway 的 mux 是指 ServeMux,它是 gRPC-Gateway 框架中的一个核心组件,用于处理 HTTP 请求并将其转发到相应的 gRPC 服务。ServeMux 实现了一个反向代理功能,将 HTTP 请求映射到对应的 gRPC 方法,并将 gRPC 方法的响应转换为 HTTP 响应返回给客户端。
ServeMux 的作用:

  1. 请求路由:
    ServeMux 负责将不同的 HTTP 请求路由到相应的 gRPC 方法。它根据 HTTP 方法(GET、POST 等)、请求路径和其他 HTTP 请求属性来决定将请求转发到哪个 gRPC 方法。
  2. 协议转换:
    ServeMuxHTTP 请求转换为 gRPC 请求,并将 gRPC 响应转换为 HTTP 响应。这包括将 JSON 请求体转换为 Protobuf 格式,以及将 Protobuf 格式的响应转换为 JSON 格式。
  3. 中间件支持:
    你可以在 ServeMux添加中间件,如日志记录、认证、限流等。
    创建一个 ServeMux 实例:
mux := runtime.NewServeMux()
创建一个 HTTP 服务

创建一个 HTTP 服务,并注册 gRPC 服务的 HTTP 处理程序,将 HTTP 请求转发到 gRPC 服务器。

gwServer := &http.Server{
	Handler: mux,
	Addr:    ":8090",
}
err = personpb.RegisterSearchServiceHandler(context.Background(), mux, conn)
if err != nil {
	log.Fatal(err)
}
监听网关服务

最后,监听网关服务

gwServer.ListenAndServe()
服务端整体示例代码
func main() {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go registerGRPCGateway(&wg)
	go registerGRPCServer(&wg)
	wg.Wait()
}

func registerGRPCGateway(wg *sync.WaitGroup) {
	defer wg.Done()
	conn, err := grpc.NewClient("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
	mux := runtime.NewServeMux()
	gwServer := &http.Server{
		Handler: mux,
		Addr:    ":8090",
	}
	err = personpb.RegisterSearchServiceHandler(context.Background(), mux, conn)
	if err != nil {
		log.Fatal(err)
	}
	err = gwServer.ListenAndServe()
	if err != nil {
		log.Fatal(err)
	}
}

func registerGRPCServer(wg *sync.WaitGroup) {
	defer wg.Done()
	l, err := net.Listen("tcp", ":8888")
	if err != nil {
		log.Fatal(err)
	}

	s := grpc.NewServer()
	personpb.RegisterSearchServiceServer(s, &personServer{})
	err = s.Serve(l)
	if err != nil {
		log.Fatal(err)
	}
}

详解 grpc gateway 的 options

.proto 文件中 option (google.api.http) 是一个扩展选项,用于在 Protobuf 文件中定义 gRPC 方法的 HTTP 映射规则。这些规则包括 HTTP 方法、请求路径、请求体等。

HTTP 方法选项

gRPC-Gateway 支持多种 HTTP 方法和丰富的配置选项。以下是 gRPC-Gateway 中常用的 HTTP 映射选项:

  1. get: 用于映射 HTTP GET 请求。
  2. post: 用于映射 HTTP POST 请求。
  3. put: 用于映射 HTTP PUT 请求。
  4. patch: 用于映射 HTTP PATCH 请求。
  5. delete: 用于映射 HTTP DELETE 请求。

默认的 query 入参

如果定义的 option 中既没有以 path 也没有以 body 的形式定义入参,默认会支持以 query 的形式入参。
比如以下 rpc :

rpc GetPerson2(PersonReq) returns (PersonRes) {
  option (google.api.http) = {
    get: "/api/person"
  };
};

该 rpc 生成 HTTP 接口后,支持以 query 的形式入参:
在这里插入图片描述
并且还支持传入嵌套属性:

rpc GetPerson3(PersonReq2) returns (PersonRes2) {
  option (google.api.http) = {
    get: "/api/person2"
  };
};

调用:
在这里插入图片描述

path 入参

path 入参可以通过在 uri 中定义,并且:

path 入参支持嵌套属性

示例:

rpc GetPerson4(PersonReq2) returns (PersonRes2) {
  option (google.api.http) = {
    get: "/api/person2/{name}/{age}/{dog.name}"
  };
};

调用:
在这里插入图片描述

path 入参设置为固定值

path 入参支持可以设置为固定值(如果传入的不为固定值会 NOT FOUND)
示例:

rpc GetPerson4(PersonReq2) returns (PersonRes2) {
  option (google.api.http) = {
    get: "/api/person2/{name=hreate}/{age}/{dog.name}"
  };
};

故意传入别的 name :
在这里插入图片描述
在这里插入图片描述
结果:404 NOT FOUND

传入预先设置的 hreate
在这里插入图片描述
成功。

body 选项

body 选项用于指定 HTTP 请求体如何映射到 gRPC 请求消息。它有以下几种常见的设置方式:

  • body: "*": 表示整个 HTTP 请求体都映射到 gRPC 请求消息。
  • body: "field_name": 表示 HTTP 请求体中的某个字段映射到 gRPC 请求消息的指定字段

    ❗注:
    如果在 body 中指定了字段,这个接口就不支持传递整个 JSON 对象,只能传递这个字段对应的值
    比如:
    有一 PersonReq 结构体有属性 nameage ,在某一 POST 接口中定义了 body: "name" ,那在传递时只能传递:"NAME_CONTENT",而不支持传递:

    {
    	"name": "NAME_CONTENT"
    }
    
  • 省略 body 选项: 表示不使用请求体,所有参数都通过 URL 查询参数传递。

additional_bindings 选项

additional_bindings 允许你为同一个 gRPC 方法定义多个 HTTP 映射。这在需要支持多个 URL 路径或 HTTP 方法访问同一个 gRPC 方法时非常有用。

  • 示例
    假设有一个 GetPerson 方法,既希望通过 GET 请求获取用户信息,也希望通过 POST 请求获取用户信息:

    syntax = "proto3";
    
    package example;
    
    import "google/api/annotations.proto";
    
    message GetPersonReq {
      string id = 1;
    }
    
    message PersonRes {
      string name = 1;
      int32 age = 2;
    }
    
    service PersonService {
      rpc GetPerson(GetPersonReq) returns (PersonRes) {
        option (google.api.http) = {
          get: "/api/person/{id}"
          additional_bindings {
            post: "/api/person"
            body: "*"
          }
        };
      }
    }
    

    以上代码中,通过使用 additional_bindings 定义了一个额外的 HTTP 映射。

    这样,GetPerson 方法既可以通过 GET /api/person/{id} 访问,也可以通过 POST /api/person 并在请求体中传递 GetPersonReq 对象访问。

response_body 选项

response_body 允许你指定 gRPC 响应消息的哪个部分映射到 HTTP 响应体,在只希望返回响应消息的一部分时这个选项非常有用。

  • 示例
    假设你有一个 CreatePerson 方法,创建用户后希望只返回创建的用户的 id 而不是整个用户对象
    syntax = "proto3";
    
    package example;
    
    import "google/api/annotations.proto";
    
    message CreatePersonReq {
      string name = 1;
      int32 age = 2;
    }
    
    message CreatePersonRes {
      string id = 1;
      string name = 2;
      int32 age = 3;
    }
    
    service PersonService {
      rpc CreatePerson(CreatePersonReq) returns (CreatePersonRes) {
        option (google.api.http) = {
          post: "/api/person"
          body: "*"
          response_body: "id"
        };
      }
    }
    
    这样,当调用 CreatePerson 方法时,HTTP 响应体将只包含创建的用户的 id,而不是整个 CreatePersonRes 对象。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值