1.概述
gRPC是一种现代的远程过程调用(RPC)框架,它支持跨语言、跨平台的通信。它基于HTTP/2协议,使用Protocol Buffers作为默认的消息编码方式,能够提供高效、低延迟的远程调用服务。gRPC主要用于构建分布式系统中的客户端和服务器之间的通信,例如微服务架构中的服务间通信。
gRPC中,客户机应用程序可以直接调用不同机器上的服务器应用程序上的方法,就像它是本地对象一样,使你更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,指定可以远程调用的方法及其参数和返回类型。在服务器端,服务器实现这个接口,并运行gRPC服务器来处理客户端调用。在客户端,客户端有一个存根(在某些语言中称为客户端),它提供与服务器相同的方法。
官网:Introduction to gRPC | gRPC
数据在进行网络传输的时候,需要进行序列化,序列化协议有很多种,比如xml,json,protobuf等,gRPC默认使用protocol buffers。

grpc数据使用proto格式传送,支持多种语言之间传送(与json不同,不支持所有语言),
Protocol Buffers(简称ProtoBuf)是一种用于序列化结构化数据的接口定义语言(IDL)和二进制数据交换格式。
ProtoBuf 定义数据结构和消息格式的方式是通过编写 .proto 文件,然后使用特定的编译器将其编译成各种编程语言的类或结构体,从而使得不同编程语言的系统能够相互通信和解析对方的数据。
官网:Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)
2.配置
使用Protocol Buffers,需要下载通用编译器
下载proto:Releases · protocolbuffers/protobuf (github.com)
目前最新版本
Release Protocol Buffers v26.0-rc1 · protocolbuffers/protobuf · GitHub

将解压后的bin目录加入环境变量

使用以下命令安装Go的协议编译器插件:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

安装后会在你定义的GOPATH的bin命令下生成


3.proto文件编写

在Proto语法中,一个数据结构被定义为一个消息(通过关键字 message 字段指定),并由一系列字段组成。
字段限制:
- required:字段必须包含一个非空值。如果没有提供该字段的值,编译器将会报错。
- optional:字段可以包含一个空值或者非空值。这是默认的字段限制。(生成指针类型)
- repeated:字段可以包含零个或多个值。消息体中可重复字段,重复的值的顺序会被保留,这种限制适用于数组或集合类型的字段。(生成切片类型)
标识号
标识号用于唯一标识消息类型中的字段。必须为消息定义中的每个字段提供1到536,870,911之间的数字,并具有以下限制:
- 给定的数字在该消息的所有字段中必须是唯一的。
- 字段号19,000到19,999是为协议缓冲区实现保留的。使用这些保留字段号之一,协议缓冲区编译器将报错。
- 不能使用任何先前保留的字段号或任何已分配给扩展的字段号。
- 对于最常设置的字段,应该使用字段号1到15。较低的字段数值在连线格式中占用的空间较少。例如,1到15范围内的字段号需要一个字节来编码。16到2047范围内的字段号占用两个字节。
proto在java与Go类型映射

默认值
- 对于数字类型(如 int32、int64、uint32、uint64、float、double 等),默认值为 0。
- 对于布尔类型(bool),默认值为 false。
- 对于字符串类型(string),默认值为空字符串 “”。
- 对于字节类型(bytes),默认值为空字节。
- 对于枚举类型,对于枚举,默认值是第一个定义的枚举值,该值必须为0。。
- 对于消息类型,对于消息字段,没有设置该字段。它的确切值与语言有关。
在proto包下新建文件
//指定版本 默认版本2
syntax = "proto3";
// 指定等会文件生成出来的package
package server;
//代码生成目录
option go_package = "grpc/chitchat/proto";
service SayService{
//rpc服务的函数名 (传人参数)返回(返回参数)
rpc SayHello(SayRequest) returns(SayResponse){}
}
//枚举
enum Gender {
//女
WOMAN = 0;
//男
MAN = 1;
//保留18,39,和20到30,保留就不能使用
reserved 18, 39, 20 to 30;
}
//定义消息
message SayRequest{
//标识号用于唯一标识消息类型中的字段。
string name = 1;
// 自定义枚举值
Gender gender = 2;
uint32 age = 3;
optional string msg = 5;
//保留18,39,和50到70
reserved 18, 39, 50 to 70;
}
message SayResponse{
//每一个message里的标识号唯一就行
string name = 1;
optional string msg = 5;
}保留就不能使用
//保留18,39,和50到70 reserved 18, 39, 50 to 70; //保留字段 reserved "phone","email";
如果想要定义时间,考虑时间戳定义不同,使用import使用第三方时间类型
import "google/protobuf/timestamp.proto";
定义方式
google.protobuf.Timestamp time = 6;
还可以自定义类型
message Address{
string country = 1;
string city = 2;
}使用方法
Address address = 7;
还有数组和集合键值对
Any是第三方任意类型的意思
//任意类型 import "google/protobuf/any.proto";
//数组 repeated string hobys = 8; //键值对 map<string,google.protobuf.Any> data = 9;
定义服务
根据实际需求定义多个 RPC 方法,并在服务中进行声明。每个方法都可以有自己的输入和输出消息类型。
- 一元调用:一元调用是最简单的 RPC 调用方式,也是最常见的方式。在一元调用中,客户端向服务器发送一个请求消息,并等待服务器返回一个响应消息。这种调用方式适用于请求和响应都是独立的场景,例如获取用户信息、创建订单等。
- 客户端流式 RPC:客户端流式 RPC 是一种特殊的 RPC 调用方式,其中客户端可以通过一个流式通道向服务器发送多个请求消息,并等待服务器返回一个响应消息。客户端可以根据需要发送任意数量的请求消息,服务器可以根据接收到的请求逐个处理,并在最后返回响应。这种调用方式适用于客户端需要批量发送数据或长时间发送数据的场景,例如上传文件、批量处理等。
- 服务端流式 RPC:在服务端流式 RPC 中,客户端向服务器发送一次请求,并等待服务器返回一个流式响应。这意味着服务器可以向客户端发送多个消息,而客户端只需等待响应即可。服务端流式 RPC 通常用于处理需要分批次发送数据或长时间运行的任务。
- 双向流式 RPC:在双向流式 RPC 中,客户端和服务器之间可以建立一个双向数据流,双方可以随时向对方发送消息。这种方式可以实现实时的数据交互,例如聊天室、游戏应用等。
service SayService{
//rpc服务的函数名 (传人参数)返回(返回参数)
//一元调用
rpc SayHello(SayRequest) returns(SayResponse){}
//客户端流
rpc SayHelloClientStream(stream SayRequest) returns(SayResponse){}
//服务端流
rpc SayHelloServerStream(SayRequest) returns(stream SayResponse){}
//双向流
rpc SayHellobothwayStream(SayRequest) returns(stream SayResponse){}
}
全部代码如下
//指定版本 默认版本2
syntax = "proto3";
// 指定等会文件生成出来的package
package server;
//代码生成目录
option go_package = "grpc/chitchat/proto";
//考虑时间戳定义不同,使用import使用第三方时间类型
import "google/protobuf/timestamp.proto";
//任意类型
import "google/protobuf/any.proto";
service SayService{
//rpc服务的函数名 (传人参数)返回(返回参数)
//一元调用
rpc SayHello(SayRequest) returns(SayResponse){}
//客户端流
rpc SayHelloClientStream(stream SayRequest) returns(SayResponse){}
//服务端流
rpc SayHelloServerStream(SayRequest) returns(stream SayResponse){}
//双向流
rpc SayHellobothwayStream(SayRequest) returns(stream SayResponse){}
}
//枚举
enum Gender {
//女
WOMAN = 0;
//男
MAN = 1;
//保留18,39,和20到30,保留就不能使用
reserved 18, 39, 20 to 30;
}
//定义消息
message SayRequest{
//标识号用于唯一标识消息类型中的字段。
string name = 1;
// 自定义枚举值
Gender gender = 2;
uint32 age = 3;
optional string msg = 5;
google.protobuf.Timestamp time = 6;
Address address = 7;
//数组
repeated string hobys = 8;
//键值对
map<string, google.protobuf.Any> contactWay = 9;
//保留18,39,和50到70
reserved 18, 39, 50 to 70;
//保留字段
reserved "phone", "email";
}
message SayResponse{
//每一个message里的标识号唯一就行
string name = 1;
optional string msg = 5;
}
message Address{
string country = 1;
string city = 2;
}生成命令
这个命令是使用 Protobuf 编译器(protoc)生成 Go 代码的命令,它将根据指定的选项和参数来生成 gRPC 的客户端和服务器端代码。
命令的含义如下:
- –go_out=.:表示生成 Go 代码,并将生成的文件放在当前目录下。
- –go_opt=paths=source_relative:表示使用源文件相对路径作为生成的 Go 代码文件的包导入路径。
- –go-grpc_out=.:表示生成 gRPC 的客户端和服务器端代码,并将生成的文件放在当前目录下。
- –go-grpc_opt=paths=source_relative:表示使用源文件相对路径作为生成的 gRPC 代码文件的包导入路径。
- helloworld/helloworld.proto:表示指定的 Protobuf 文件路径,这里是 .\chitchat\proto\helloworld.proto。
命令如下
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative .\ chitchat\proto\helloworld.proto

可以看到生成了两个文件

此时在安装一下缺少的依赖
go mod tidy
4.简单使用
编写服务端和客户端
新建server文件夹和client文件夹

在server包下新建server.go
代码如下
package server
import "grpc/chitchat/proto"
type server struct {
proto.UnimplementedSayServiceServer
}proto.UnimplementedSayServiceServer是proto包下我们生成的代码helloworld_grpc.pb.go下
gRPC 服务服务器端结构体 UnimplementedSayServiceServer,它实现了我们定义的 SayServiceServer 接口,但每个方法都返回了一个未实现的错误
这些方法的实现都调用了 status.Errorf 函数,返回一个未实现的错误(codes.Unimplemented)。

这是我们定义的业务的默认实现,如果被调用不至于直接报错
我们要实现它,复制过来,正确导包即可
package server
import (
"context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"grpc/chitchat/proto"
)
type server struct {
proto.UnimplementedSayServiceServer
}
func (server) SayHello(ctx context.Context, in *proto.SayRequest) (*proto.SayResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (server) SayHelloClientStream(stream proto.SayService_SayHelloClientStreamServer) error {
return status.Errorf(codes.Unimplemented, "method SayHelloClientStream not implemented")
}
func (server) SayHelloServerStream(in *proto.SayRequest, stream proto.SayService_SayHelloServerStreamServer) error {
return status.Errorf(codes.Unimplemented, "method SayHelloServerStream not implemented")
}
func (server) SayHellobothwayStream(stream proto.SayService_SayHellobothwayStreamServer) error {
return status.Errorf(codes.Unimplemented, "method SayHellobothwayStream not implemented")
}
一元调用
我们先实现SayHello方法,来演示一元调用
func (server) SayHello(ctx context.Context, in *proto.SayRequest) (*proto.SayResponse, error) {
log.Printf("server : %v\n", in)
hello := "Hello client , This is server!"
return &proto.SayResponse{
Name: "Dreams",
Msg: &hello,
}, nil
}
再然后在server.go创建个main方法启动
func main() {
// tcp监听9090
listen, err := net.Listen("tcp", "localhost:9090")
if err != nil {
log.Fatal("出现错误", err)
return
}
newServer := grpc.NewServer()
proto.RegisterSayServiceServer(newServer, &server{})
log.Printf("server listen : %d", listen.Addr())
err = newServer.Serve(listen)
if err != nil {
log.Fatal(err)
}
}gRPC 服务器端的主函数,用于创建和启动一个 gRPC 服务器。
首先,代码通过调用 net.Listen 函数,在本地的 9090 端口上创建了一个 TCP 监听器。如果创建监听器时发生错误,就会输出错误信息并终止程序运行。
接下来,代码通过调用 grpc.NewServer 函数创建了一个新的 gRPC 服务器实例。
然后,通过调用 proto.RegisterSayServiceServer 函数将一个实现了 SayServiceServer 接口的结构体 server 注册到 gRPC 服务器中。通过调用 proto.RegisterSayServiceServer 函数,gRPC 服务器会将服务实现与服务定义关联起来,以便在收到客户端请求时正确地调用服务实现的方法。这表示服务器将使用该结构体来处理来自客户端的请求。
最后,通过调用 newServer.Serve(listen) 函数,启动了 gRPC 服务器并开始监听来自客户端的连接和请求。如果启动过程中发生错误,就会输出错误信息并终止程序运行。
在client包下创建client.go
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/timestamppb"
"grpc/chitchat/proto"
"log"
)
func main() {
//建立与服务器的连接
connect, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
return
}
defer connect.Close()
//创建客户端对象 client
client := proto.NewSayServiceClient(connect)
sayHello(client)
}
func sayHello(client proto.SayServiceClient) {
//创建一个上下文对象 ctx
ctx := context.Background()
sayRequest := getRequest()
sayHello, err := client.SayHello(ctx, sayRequest)
if err != nil {
log.Fatal(err)
return
}
log.Println(sayHello.Msg)
}
func getRequest() *proto.SayRequest {
//创建一个当前时间戳的操作
time := timestamppb.Now()
//Msg使用了optional参数,所以要传指针
hello := "Hello server"
//创建一个 google.protobuf.Any 类型的对象
updatatime, _ := anypb.New(time)
sayRequest := &proto.SayRequest{
Name: "Dreams",
Gender: proto.Gender_MAN,
Age: 21,
Msg: &hello,
Time: time,
Address: &proto.Address{
Country: "璃月",
City: "璃月港",
},
Hobys: []string{"编程", "睡觉"},
ContactWay: map[string]*anypb.Any{
"updatatime": updatatime,
},
}
return sayRequest
}
gRPC 客户端的示例,用于向 gRPC 服务器发送请求并处理响应。
首先,代码通过 grpc.Dial 方法建立与服务器的连接。”localhost:9090″ 是服务器的地址,grpc.WithTransportCredentials(insecure.NewCredentials()) 表示使用不安全的连接方式建立连接。在实际生产环境中,应该使用安全的传输凭证来保护通信。
然后,代码创建了一个 proto.SayServiceClient 的客户端对象 client,用于与服务器进行通信
context.Background() 创建一个上下文对象 ctx,用于传递给服务方法调用。getRequest获取一个sayRequest是我们定义的SayRequest格式消息,按照格式赋值。
接着,代码调用 client.SayHello(ctx, sayRequest) 方法来向服务器发送请求,并接收响应。SayHello 是服务端定义的一个方法,用于处理客户端的 SayRequest 请求并返回相应的响应。
最后,代码打印出响应中的消息内容。
运行服务端
go run .\chitchat\server\server.go
运行后,服务端持续监听9090打开端口


运行客户端
go run .\chitchat\client\client.go
运行客户端

可以看到服务端接收到消息

客户端再执行一次,服务端会再接收一次消息


客户端调用
服务端在main.go下实现SayHelloClientStream方法
func (server) SayHelloClientStream(stream proto.SayService_SayHelloClientStreamServer) error {
msg := "服务端已接收到消息"
//服务端应该持续不断接收客户端消息
for true {
//gRPC 服务端流式调用中用于接收客户端请求的方法。
recv, err := stream.Recv()
if err == io.EOF {
// gRPC 服务端流式调用中用于向客户端发送响应并关闭流的方法
return stream.SendAndClose(&proto.SayResponse{
Msg: &msg,
})
}
if err != nil {
log.Fatal(err)
return err
}
fmt.Printf("server 接收到消息 : %d\n", recv)
}
return nil
}上述代码接收客户端流式请求,并持续不断地接收客户端发送的消息。
stream.Recv() 是 gRPC 服务端流式调用中用于接收客户端请求的方法。
stream.SendAndClose() 是 gRPC 服务端流式调用中用于向客户端发送响应并关闭流的方法,该方法接收一个响应消息作为参数,并将其发送给客户端。然后该方法会自动关闭流。
io.EOF 是 Go 语言标准库中定义的一个错误,表示已达到文件或流的末尾。在 gRPC 中,当客户端关闭了流时,服务端会收到一个 io.EOF 错误。
在这段逻辑中,如果服务端接收到的消息的错误是 io.EOF,也就是客户端关闭了流,那么服务端会使用 stream.SendAndClose() 方法向客户端发送最终的响应,并结束流式调用。在这里,服务端会发送一个包含字符串 msg 的响应消息给客户端。
客户端在client.go下新加一个函数
func sayHelloClient(client proto.SayServiceClient) {
//创建一个上下文对象 ctx
ctx := context.Background()
list := []*proto.SayRequest{
getRequest(), getRequest(), getRequest(),
}
stream, err := client.SayHelloClientStream(ctx)
if err != nil {
log.Fatal(err)
}
for _, re := range list {
err := stream.Send(re)
if err != nil {
log.Fatal(err)
}
}
recv, err := stream.CloseAndRecv()
if err != nil {
log.Fatal(err)
}
log.Printf("客户端已接收到消息 : %v\n", recv)
}stream.Send(re) 是 gRPC 客户端流式调用中用于向服务端发送请求消息的方法。
stream.CloseAndRecv() 是 gRPC 客户端流式调用中用于关闭发送流并接收服务端最终响应消息的方法。
上述代码调用 client.SayHelloClientStream(ctx) 方法创建了一个客户端流式调用的流对象 stream。如果创建流对象时发生错误,代码会使用 log.Fatal(err) 来记录错误并终止程序。
然后,代码使用 for 循环遍历 list 切片,并通过 stream.Send(re) 方法将每个请求消息发送到服务端。如果发送请求消息时发生错误,代码同样会使用 log.Fatal(err) 记录错误并终止程序。
循环结束后,代码调用 stream.CloseAndRecv() 方法关闭流并接收服务端的最终响应消息。如果关闭流和接收响应消息时发生错误,代码同样会使用 log.Fatal(err) 记录错误并终止程序。
最后,代码使用 log.Printf() 输出客户端接收到的消息 recv。
客户端client.go改为调用sayHelloClient(client)函数
//sayHello(client) sayHelloClient(client)
运行
客户端将同时发送3条信息


服务端调用
func (server) SayHelloServerStream(in *proto.SayRequest, stream proto.SayService_SayHelloServerStreamServer) error {
log.Printf("server 接收到消息 : %d\n", in)
hello := "Hello client , This is server!"
sayResponse := &proto.SayResponse{
Name: "Dreams",
Msg: &hello,
}
list := []*proto.SayResponse{
sayResponse, sayResponse, sayResponse,
}
for _, re := range list {
err := stream.Send(re)
if err != nil {
log.Fatal(err)
}
}
return nil
}
客户端client.go
func sayHelloServer(client proto.SayServiceClient) {
//创建一个上下文对象 ctx
ctx := context.Background()
request := getRequest()
stream, err := client.SayHelloServerStream(ctx, request)
if err != nil {
log.Fatal(err)
}
for true {
recv, err := stream.Recv()
if err == io.EOF {
stream.CloseSend()
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("客户端接收到消息 : %d\n", recv)
}
}客户端client.go改为调用sayHelloServer函数
//sayHello(client) //sayHelloClient(client) sayHelloServer(client)
运行


再运行


双向流调用
客户端client.go
func sayHellobothway(client proto.SayServiceClient) {
//创建一个上下文对象 ctx
ctx := context.Background()
list := []*proto.SayRequest{
getRequest(), getRequest(), getRequest(),
}
stream, err := client.SayHellobothwayStream(ctx)
if err != nil {
log.Fatal(err)
}
var done = make(chan struct{}, 0)
go func() {
for true {
recv, err := stream.Recv()
if err == io.EOF {
close(done)
return
}
if err != nil {
close(done)
log.Println(err)
}
fmt.Printf("客户端接收到消息 : %d\n", recv.Msg)
}
}()
for _, re := range list {
err := stream.Send(re)
if err != nil {
log.Fatal(err)
}
}
err = stream.CloseSend()
<-done
if err != nil {
log.Println(err)
}
}stream.CloseSend() 是 gRPC 双向流式调用中用于关闭发送流的方法。
通过调用 client.SayHellobothwayStream(ctx) 方法创建了一个双向流式调用的流对象 stream。如果创建流对象时发生错误,代码会使用 log.Fatal(err) 来记录错误并终止程序。
然后,代码使用 go 关键字创建了一个协程,其中使用 stream.Recv() 方法循环接收服务端的响应消息。如果接收到的响应消息是结束标志 io.EOF,则关闭 done 通道并返回。如果接收到的响应消息有其他错误,则同样关闭 done 通道并记录错误。
接下来,代码使用 for 循环遍历 list 切片,并通过 stream.Send(re) 方法将每个请求消息发送给服务端。如果发送请求消息时发生错误,代码同样会使用 log.Fatal(err) 记录错误并终止程序。
循环结束后,代码调用 stream.CloseSend() 方法关闭发送流,表示请求消息已全部发送完毕。
最后,代码从 done 通道中接收数据,等待协程执行完毕。如果在等待期间发生错误,代码同样会记录错误。
客户端client.go改为调用sayHelloClient(client)函数
//sayHello(client) //sayHelloClient(client) //sayHelloServer(client) sayHellobothway(client)
服务端在server.go下实现SayHellobothwayStream方法
func (server) SayHellobothwayStream(stream proto.SayService_SayHellobothwayStreamServer) error {
msg := "这是服务端发送的信息"
//服务端应该持续不断接收客户端消息
for true {
recv, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
return err
}
fmt.Printf("服务端接收到消息 : %d\n", recv)
stream.Send(&proto.SayResponse{
Msg: &msg,
})
}
return nil
}运行如下




