一、什么是 grpc 和 protobuf
1.1 grpc
gRPC 是由 Google 开发的一个高效、开源的远程过程调用(RPC)框架,用于在分布式系统中进行通信。它是基于 HTTP/2 协议,支持多种语言,能够让不同的系统或应用程序(即使使用不同的编程语言)进行高效的通信。
1.1.1 主要特点
- 高性能:gRPC 基于 HTTP/2 协议,支持流式传输、请求多路复用、头部压缩等特性,这些使得 gRPC 比传统的 HTTP/1.x 更加高效。
- 跨语言支持:gRPC 支持多种编程语言(如 C++, Java, Go, Python, Ruby, Node.js 等),使得不同语言开发的系统能够无缝通信。
- 支持同步和异步调用:gRPC 提供了同步和异步的调用方式,可以根据需求选择使用。
- 内置支持流式传输:gRPC 支持单向流和双向流,可以非常方便地实现实时通信。
- 自动生成代码:通过定义服务接口,gRPC 自动生成客户端和服务端的代码,减少手动编写的工作量。
1.1.2 典型应用场景
- 微服务架构中的服务之间的高效通信。
- 跨平台、跨语言的分布式系统通信。
- 实时数据传输或流处理。
1.2 protobuf
Protocol Buffers(简称 Protobuf)是 Google 开发的一种高效的序列化结构数据的机制,类似于 XML 或 JSON,但比它们更紧凑、更高效。Protobuf 用于数据的定义、传输和持久化,通常与 gRPC 一起使用。
1.2.1 主要特点
- 紧凑高效:Protobuf 使用二进制格式来存储数据,相比于 JSON 或 XML,它占用更少的空间,传输更快,解析也更高效。
- 平台和语言无关:Protobuf 支持多种语言(如 C++, Java, Python, Go 等)并能在不同平台间进行无缝传输。
- 结构化数据:数据的定义通过
.proto
文件进行,可以清晰地指定数据的类型、字段以及数据结构。 - 向后兼容与向前兼容:Protobuf 支持字段的增删和修改时,不影响已经存在的消息结构。新版本和旧版本可以兼容地进行通信。
1.2.2 工作原理
- 定义数据结构:你需要创建一个
.proto
文件来定义数据结构和服务接口。例如:syntax = "proto3"; message Person { string name = 1; int32 id = 2; string email = 3; } service Greeter { rpc SayHello (Person) returns (Person); }
-
编译
.proto
文件:使用protoc
编译器,将.proto
文件转化为目标语言的代码。 -
生成代码:Protobuf 会根据
.proto
文件生成语言特定的类和方法,可以通过这些类和方法来序列化、反序列化数据。 -
使用 gRPC:如果你定义了服务接口,gRPC 会根据
.proto
文件生成客户端和服务器端代码,帮助你构建高效的通信。
1.2.3 为什么结合使用 gRPC 和 Protobuf?
gRPC 通常与 Protobuf 一起使用,因为它们可以提供以下优势:
- 性能:Protobuf 的二进制格式非常高效,适合需要高性能的场景,尤其是微服务通信时。
- 简洁性:gRPC 和 Protobuf 的结合可以让你轻松地实现分布式服务,而无需担心数据格式的兼容性、传输效率等问题。
- 自动化:通过 Protobuf 定义的数据结构和 gRPC 服务接口,所有的代码生成和数据序列化/反序列化都自动化处理,极大地减少了开发工作量。
- gRPC 是一个高效的远程过程调用框架,用于不同系统之间的通信,支持多种语言,并且支持高效的流式传输。
- Protobuf 是一种高效的序列化数据格式,通常用于 gRPC 中,能够减少数据的存储空间,提高传输效率。
这两者结合使用,可以构建出高效、可靠且跨语言的分布式系统。
二、RPC 有四种需求
2.1 简单模式
这种模式最为传统,即客户端发起一次请求,服务端响应一个数据,这个大家平时熟悉的RPC没有大的区别,所以不再详细介绍。
2.1.1 proto 文件
syntax = "proto3";
package helloworld;
// 指定 Go 包的路径
option go_package = ".;proto";
// 这是一个rpc服务
service Hello {
rpc Hello(HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1; // 1 是编号
}
message HelloResponse {
string reply = 1;
}
我们在编译 proto 文件的时候,我们需要先将安装了最新版本的 protoc-gen-go
和 protoc-gen-go-grpc
插件。如果你还没有安装或需要更新它们,可以使用以下命令:
# 安装最新的 protoc-gen-go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# 安装最新的 protoc-gen-go-grpc 插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
我们需要生成普通Go代码的时候,使用 protoc 命令时,命令格式如下,我们会根据 helloworld.proto
文件生成普通的 Go 代码文件。
protoc -I . helloworld.proto --go_out=.
我们需要生成gRPC代码的时候,使用 protoc 命令时,命令格式如下,我们会使用 --go-grpc_out
来生成 gRPC 相关的代码(包括客户端和服务端的 gRPC 代码):
protoc -I . helloworld.proto --go-grpc_out=.
我们可以使用完整的命令行,来一次性生成所有的代码(普通的 Go 代码和 gRPC 相关的代码),你可以将两个命令合并成一个:
protoc -I . helloworld.proto --go_out=. --go-grpc_out=.
2.1.2 server端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"log"
"net"
pb "xuego/protobuf/helloworld/proto"
)
type Server struct {
pb.UnimplementedHelloServer // 继承自动生成的 Unimplemented 方法,可以确保将来不破坏接口兼容性
}
// SayHello 实现了 Greeter 服务的 SayHello 方法
func (s *Server) Hello(ctx context.Context, request *pb.HelloRequest) (*pb.HelloResponse, error) {
// 构造并返回响应
return &pb.HelloResponse{
Reply: "Hello " + request.Name,
}, nil
}
func main() {
// 创建一个新的 gRPC 服务器
g := grpc.NewServer()
// 注册 Greeter 服务到 gRPC 服务器
pb.RegisterHelloServer(g, &Server{})
// 定义服务器监听地址和端口
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 输出日志,表明服务器开始监听
fmt.Println("Server is listening on port :50051")
// 启动 gRPC 服务器
if err := g.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
2.1.3 client端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"xuego/protobuf/helloworld/proto"
)
func main() {
conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewHelloClient(conn)
r, err := c.Hello(context.Background(), &proto.HelloRequest{Name: "xuego"})
if err != nil {
panic(err)
}
fmt.Errorf(r.Reply)
}
2.2 服务端数据流模式
这种模式是客户端发起一次请求,服务端返回一段连续的数据流。典型的例子是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。
2.3 客户端数据流模式
与服务端数据流模式相反,这次是客户端源源不断的向服务端发送数据流,而在发送结束后,由服务端返回一个响应。典型的例子就是物联网终端向服务器报发数据。
2.4 双向数据流模式
顾名思义,这是客户端和服务端都可以向对方发送数据流,这个时候双方的数据可以同时互相发送,也就是可以实现实时交互。典型的例子就是聊天机器人。
2.5 服务端数据流模式
&& 客户端数据流模式
&& 双向数据流模式
2.5.1 proto 文件
syntax = "proto3";
option go_package = ".;proto";
service Greeter {
rpc GetStream(StreamReqData) returns (stream StreamResData); // 服务端流模式
rpc PutStream(stream StreamReqData) returns (StreamResData); // 客户端流模式
rpc AllStream(stream StreamReqData) returns (stream StreamResData); // 双向流模式
}
message StreamReqData {
string data = 1;
}
message StreamResData {
string data = 1;
}
在这些模式下,他们在服务端中写的函数的参数与简单模式的不同,需要注意一下!!!!
2.5.2 server端
package main
import (
"fmt"
"google.golang.org/grpc"
"net"
"sync"
"time"
pb "xuego/stream_grpc_test/proto"
)
const PORT = ":50052"
type Server struct {
pb.UnimplementedGreeterServer
}
func (s *Server) GetStream(req *pb.StreamReqData, res pb.Greeter_GetStreamServer) error {
i := 0
for {
i++
_ = res.Send(&pb.StreamResData{
Data: fmt.Sprintf("%v", time.Now().Unix()),
})
time.Sleep(time.Second)
if i > 10 {
break
}
}
return nil
}
func (s *Server) PutStream(cliStr pb.Greeter_PutStreamServer) error {
for {
if a, err := cliStr.Recv(); err != nil {
fmt.Println(err)
break
} else {
fmt.Println(a.Data)
}
}
return nil
}
func (s *Server) AllStream(allStr pb.Greeter_AllStreamServer) error {
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
data, _ := allStr.Recv()
fmt.Println("收到客户端下消息:" + data.Data)
}
}()
go func() {
defer wg.Done()
for {
_ = allStr.Send(&pb.StreamResData{Data: "我是服务器"})
time.Sleep(time.Second)
}
}()
wg.Wait()
return nil
}
func main() {
lis, err := net.Listen("tcp", PORT)
if err != nil {
panic(err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &Server{})
err = s.Serve(lis)
if err != nil {
panic(err)
}
}
2.5.3 client端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"sync"
"time"
pb "xuego/stream_grpc_test/proto"
)
func main() {
conn, err := grpc.Dial("localhost:50052", grpc.WithInsecure())
if err != nil {
panic(err)
}
defer conn.Close()
// 服务端流模式
c := pb.NewGreeterClient(conn)
res, _ := c.GetStream(context.Background(), &pb.StreamReqData{Data: "陌客网"})
for {
a, err := res.Recv()
if err != nil {
fmt.Println(err)
break
}
fmt.Println(a.Data)
}
// 客户端流模式
putS, _ := c.PutStream(context.Background())
i := 0
for {
i++
putS.Send(&pb.StreamReqData{
Data: fmt.Sprintf("慕课网%d", i),
})
time.Sleep(time.Second)
if i > 10 {
break
}
}
// 双向流模式
allStr, _ := c.AllStream(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for {
data, _ := allStr.Recv()
fmt.Println("收到客户端下消息:" + data.Data)
}
}()
go func() {
defer wg.Done()
for {
_ = allStr.Send(&pb.StreamReqData{Data: "我是客户段"})
time.Sleep(time.Second)
}
}()
wg.Wait()
}