1. Protobuf
1.1 Protobuf 简介
Protobuf(Protocol Buffers)是Google开源的一种数据序列化格式。它是一种语言无关、平台无关、可扩展的数据交换格式,用于结构化数据的序列化和反序列化,并提供了一个接口描述语言(IDL,Interface Description Language)来定义数据结构和服务接口。
1.2 Protobuf 用法
1.2.1 message
当使用Protocol Buffers(Protobuf)时,需要定义消息结构和服务接口,并通过编译器生成对应的代码。
首先,需要使用Protobuf提供的接口描述语言(IDL)来定义消息结构。IDL使用简洁的语法来描述消息结构和字段,类似于定义一个类的属性。下面是一个示例的Protobuf消息定义:
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}
上面的代码定义了一个名为Person的消息,包含name、age和hobbies三个字段。name字段是一个字符串,使用编号1;age字段是一个32位整数,使用编号2;hobbies字段是一个字符串数组,使用编号3。
接下来,需要使用Protobuf的编译器将消息定义文件(通常以.proto为扩展名)编译为对应的代码。编译器会根据消息定义生成相应语言的类、接口和相关方法。通过这些生成的代码,我们可以实例化、访问和操作Protobuf消息。
例如,使用上述消息定义生成Java代码:
protoc --java_out=. person.proto
这会生成与Person消息对应的Java类,允许我们在Java应用程序中使用Person对象。
在编写应用程序代码时,我们可以使用生成的代码来创建、设置和访问Protobuf消息,以及将消息序列化为二进制格式或反序列化为消息对象。
Person.Builder personBuilder = Person.newBuilder();
personBuilder.setName("Alice");
personBuilder.setAge(25);
personBuilder.addHobbies("reading");
personBuilder.addHobbies("hiking");
Person person = personBuilder.build();
上面的代码创建了一个Person对象,并对其属性进行设置。最后使用build()方法生成一个不可变的Person实例。
1.2.2 service和调用
在Protobuf中,除了消息(message)外,还可以定义服务接口(service),使得不同的应用程序之间可以通过定义的服务接口进行通信。
服务接口(service)定义了一组可供远程调用的方法,使用Protobuf的语法定义在IDL中。服务接口中的每个方法都有一个请求消息和一个响应消息。
下面是一个简单的例子,定义了一个名为UserService的服务接口,包含一个获取用户信息的方法getUserInfo:
syntax = "proto3";
message UserRequest {
string userId = 1;
}
message UserResponse {
string name = 1;
int32 age = 2;
}
service UserService {
rpc getUserInfo(UserRequest) returns (UserResponse);
}
在上述代码中,UserRequest是获取用户信息方法的请求消息,包含一个字段userId;UserResponse是获取用户信息方法的响应消息,包含字段name和age;UserService是服务接口的名称。
为了使用服务接口,需要通过编译器生成对应语言的代码,并实现具体的服务接口。生成的代码会包含服务接口的抽象定义和方法的具体实现,以及请求消息和响应消息的构造与解析。
在调用方的代码中,可以通过调用生成的服务接口对象的方法来发起远程调用。下面是一个示例代码:
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
UserRequest request = UserRequest.newBuilder()
.setUserId("123")
.build();
UserResponse response = stub.getUserInfo(request);
上述代码中,首先创建了一个用于远程调用的服务接口的客户端存根(stub)。然后,构建了一个UserRequest请求消息对象,并携带参数userId。最后,通过调用stub的getUserInfo方法,将请求消息发送给服务端,并获取服务端返回的UserResponse响应消息。
通过Protobuf的服务接口机制,可以方便地定义和使用远程调用接口,实现不同应用程序之间的通信和交互。无论是客户端还是服务端,都只需要使用生成的代码和定义的服务接口来实现远程调用的功能。
通过Protobuf,我们可以将这些消息在网络中进行传输和存储,不同编程语言的应用程序可以实现跨平台、跨语言的通信和数据交换。
需要注意的是,Protobuf还支持版本升级和消息的扩展,可以通过向消息定义中添加新的字段来实现消息结构的演化。同时,Protobuf提供了其他高级特性,如枚举类型、嵌套消息、自定义选项等,以满足不同的需求。
1.3 Protobuf优势
与其他数据交换格式(如XML和JSON)相比,Protobuf具有以下优势:
-
紧凑性:Protobuf使用二进制编码,因此数据量相对较小,传输和存储的效率更高。
-
快速性:由于数据编码和解码的过程相对简单,Protobuf的速度较快。
-
可扩展性:当数据的定义发生变化时,可以很方便地向已有定义中添加新字段,而不会影响已有代码的兼容性。
-
语言无关性:Protobuf定义数据格式的操作与具体编程语言无关,因此可以在不同语言之间进行数据交换。
总的来说,Protobuf是一种高效、灵活、可扩展的数据交换格式,适用于在不同语言和平台之间进行数据传输和存储。
2. gRPC
2.1 gRPC 的概念和使用场景
gRPC是一种高性能、开源的远程过程调用(RPC)框架,在gPRC里客户端可以向调用本地对象一样直接调用另一台不同机器上服务端应用的方法,可以用于构建可靠和高效的分布式系统。
gRPC基于Protobuf序列化协议开发,且支持众多开发语言。面向服务端和协议端,基于http/2设计,带来诸如双向流,流控,头部压缩,单TCP连接上的多路复用请求等特性。这些特性使得其在移动设备上表现的更好,更省电和节省空间。
与许多RPC系统类似,gRPC也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口。并运行一个gRPC服务器来处理客户端调用。在客户端拥有一个存根能够向服务端一样的方法。
gRPC的概念包括:
-
通信协议:gRPC使用HTTP/2作为底层的通信协议,它提供了低延迟和高效的多路复用功能,可以同时在一个TCP连接上处理多个RPC请求和响应。
-
序列化协议:gRPC使用Protocol Buffers作为默认的数据序列化协议,它可以将结构化数据进行序列化和反序列化,实现不同语言之间的数据交换。
-
接口定义语(IDL):gRPC使用ProtoBuf(Protocol Buffers)来定义服务接口和消息类型。ProtoBuf是一种轻量级、高效的接口定义语言,可以生成不同编程语言的代码。
使用gRPC的场景包括:
-
微服务架构:gRPC适用于构建分布式系统中的微服务,可以实现多个微服务之间的快速、可靠的通信。gRPC提供了强类型的接口定义和代码生成,简化了微服务间的接口调用。
-
移动和浏览器应用:gRPC支持多种编程语言和平台,包括Java、C++、Python、Go、Node.js等,可以在移动和浏览器端使用,方便构建跨平台的应用。
-
高性能应用:gRPC基于HTTP/2和Protocol Buffers,具有较低的延迟和高吞吐量的特点。因此,gRPC适用于对性能要求较高的应用场景,如实时推送、数据同步等。
2.2 gRPC 的大致请求流程
1.客户端(gRPC Stub)调用A方法,发起RPC调用
2.对请求信息使用Protobuf进行对象序列化压缩(IDL)
3.服务端(gPRC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回。
4.对响应结果使用Protobuf进行对象序列化压缩(IDL)
5.客户端接受到服务端响应,解码请求体。回调被调用的A方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果
2.3 gRPC的4种模式
gRPC的使用流程可以简单概括为,在 proto 文件定义要调用的方法,要传入的参数、返回值。然后 grpc 会根据 proto 生成两个东西,一个是客户端存根stub,用于给客户端提供调用方法名和塞入参;另一个是服务端接口,用于给服务端实现这个接口,返回给客户端。gRPC 就像一个桥梁一样连接着客户端和服务端,实现了远程调用。 四种模式及在 proto 文件中定义分别为
-
单向流(Unary):客户端发送一个请求,服务器返回一个响应。这是最简单也是最常见的模式。客户端发送一个请求消息给服务器,并且等待服务器返回一个响应消息。
rpc UnaryMode (UnaryRequest) returns (UnaryResponse) {}
-
服务器流(Server Streaming):客户端发送一个请求,服务器返回一个响应流。在这个模式下,客户端发送一个请求给服务器,并且服务器通过一个流返回多个响应消息,直到服务器完成处理为止。
rpc ServerStreamingMode (ServerStreamingRequest) returns (stream ServerStreamingResponse) {}
-
客户端流(Client Streaming):客户端发送一个请求流,服务器返回一个响应。在这个模式下,客户端通过一个流发送多个请求消息给服务器,并且服务器返回一个响应消息。
rpc ClientStreamingMode (stream ClientStreamingRequest) returns (ClientStreamingResponse) {}
-
双向流(Bidirectional Streaming):客户端发送一个请求流,服务器返回一个响应流。在这个模式下,客户端通过一个流发送多个请求消息给服务器,同时服务器也通过一个流返回多个响应消息给客户端。这种模式下通信是双向的,客户端和服务器可以并发地发送和接收消息。
rpc BidirectionalStreamingMode (stream BidirectionalStreamingRequest) returns (stream BidirectionalStreamingResponse) {}
2.4 gRPC代码示范
示例代码实现了一个简单的gRPC服务和客户端,用于展示四种gRPC通信模式:Unary、Server Streaming、Client Streaming和Bidirectional Streaming。服务端实现了一个ModeService服务,包含以下四个方法
-
UnaryMode
:一元RPC方法,客户端向服务端发送一条消息,服务端返回一条响应消息。 -
ServerStreamingMode
:服务端流式RPC方法,客户端向服务端发送一条消息,服务端返回一系列的响应消息,客户端通过流式接收。 -
ClientStreamingMode
:客户端流式RPC方法,客户端通过流式发送一系列的消息给服务端,最后服务端返回一条响应消息。 -
BidirectionalStreamingMode
:双向流式RPC方法,客户端和服务端都可以通过流式发送和接收多条消息。
客户端分别调用这四个方法来演示四种通信模式,并打印服务端返回的消息。通过运行这个示例代码,可以学习到如何在gRPC中实现不同的通信模式,并理解它们的工作原理。
2.4.1 proto文件
syntax = "proto3";
package grpc_modes;
service ModeService {
rpc UnaryMode (UnaryRequest) returns (UnaryResponse) {}
rpc ServerStreamingMode (ServerStreamingRequest) returns (stream ServerStreamingResponse) {}
rpc ClientStreamingMode (stream ClientStreamingRequest) returns (ClientStreamingResponse) {}
rpc BidirectionalStreamingMode (stream BidirectionalStreamingRequest) returns (stream BidirectionalStreamingResponse) {}
}
message UnaryRequest {
string message = 1;
}
message UnaryResponse {
string message = 1;
}
message ServerStreamingRequest {
string message = 1;
int32 count = 2;
}
message ServerStreamingResponse {
string message = 1;
}
message ClientStreamingRequest {
string message = 1;
}
message ClientStreamingResponse {
string message = 1;
}
message BidirectionalStreamingRequest {
string message = 1;
}
message BidirectionalStreamingResponse {
string message = 1;
}
2.4.2 client
package main
import (
"context"
"fmt"
"io"
"log"
"google.golang.org/grpc"
pb "path/to/your/proto/file"
)
func main() {
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("failed to dial: %v", err)
}
defer conn.Close()
client := pb.NewModeServiceClient(conn)
unaryMode(client)
serverStreamingMode(client)
clientStreamingMode(client)
bidirectionalStreamingMode(client)
}
func unaryMode(client pb.ModeServiceClient) {
req := &pb.UnaryRequest{
Message: "Hello from client",
}
response, err := client.UnaryMode(context.Background(), req)
if err != nil {
log.Fatalf("failed unary mode: %v", err)
}
fmt.Println("Unary response:", response.Message)
}
func serverStreamingMode(client pb.ModeServiceClient) {
req := &pb.ServerStreamingRequest{
Message: "Hello from client",
Count: 5,
}
stream, err := client.ServerStreamingMode(context.Background(), req)
if err != nil {
log.Fatalf("failed server streaming mode: %v", err)
}
for {
response, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("failed to receive server streaming response: %v", err)
}
fmt.Println("Server streaming response:", response.Message)
}
}
func clientStreamingMode(client pb.ModeServiceClient) {
stream, err := client.ClientStreamingMode(context.Background())
if err != nil {
log.Fatalf("failed client streaming mode: %v", err)
}
for i := 0; i < 5; i++ {
req := &pb.ClientStreamingRequest{
Message: "Client streaming message " + strconv.Itoa(i+1),
}
err := stream.Send(req)
if err != nil {
log.Fatalf("failed to send client streaming request: %v", err)
}
response, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("failed to receive client streaming response: %v", err)
}
fmt.Println("Client streaming response:", response.Message)
}
}
func bidirectionalStreamingMode(client pb.ModeServiceClient) {
stream, err := client.BidirectionalStreamingMode(context.Background())
if err != nil {
log.Fatalf("failed bidirectional streaming mode: %v", err)
}
go func() {
for i := 0; i < 5; i++ {
req := &pb.BidirectionalStreamingRequest{
Message: "Bidirectional streaming message " + strconv.Itoa(i+1),
}
err := stream.Send(req)
if err != nil {
log.Fatalf("failed to send bidirectional streaming request: %v", err)
}
}
stream.CloseSend()
}()
for {
response, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("failed to receive bidirectional streaming response: %v", err)
}
fmt.Println("Bidirectional streaming response:", response.Message)
}
}
2.4.3 server
package main
import (
"context"
"fmt"
"log"
"net"
"strconv"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
pb "path/to/your/proto/file"
)
type modeServiceServer struct{}
func (s *modeServiceServer) UnaryMode(ctx context.Context, req *pb.UnaryRequest) (*pb.UnaryResponse, error) {
message := req.Message
response := &pb.UnaryResponse{Message: "Unary response: " + message}
return response, nil
}
func (s *modeServiceServer) ServerStreamingMode(req *pb.ServerStreamingRequest, stream pb.ModeService_ServerStreamingModeServer) error {
message := req.Message
count := req.Count
for i := 0; i < int(count); i++ {
response := &pb.ServerStreamingResponse{Message: "Server streaming response " + strconv.Itoa(i+1) + ": " + message}
if err := stream.Send(response); err != nil {
return err
}
}
return nil
}
func (s *modeServiceServer) ClientStreamingMode(stream pb.ModeService_ClientStreamingModeServer) error {
var messages []string
for {
request, err := stream.Recv()
if err != nil {
return err
}
messages = append(messages, request.Message)
response := &pb.ClientStreamingResponse{Message: "Client streaming received: " + request.Message}
if err := stream.SendAndClose(response); err != nil {
return err
}
}
return nil
}
func (s *modeServiceServer) BidirectionalStreamingMode(stream pb.ModeService_BidirectionalStreamingModeServer) error {
for {
request, err := stream.Recv()
if err != nil {
return err
}
message := request.Message
response := &pb.BidirectionalStreamingResponse{Message: "Bidirectional streaming received: " + message}
if err := stream.Send(response); err != nil {
return err
}
}
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
server := grpc.NewServer()
pb.RegisterModeServiceServer(server, &modeServiceServer{})
fmt.Println("Server started")
err = server.Serve(lis)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
2.5 gRPC的优缺点
2.5.1 gRPC优点
-
高性能:gRPC使用Protocol Buffers作为数据序列化格式,并通过HTTP/2协议进行通信,实现了低延迟、高吞吐量的网络传输,具有较高的性能表现。
-
跨语言支持:gRPC支持多种编程语言,包括C++, Java, Python, Go等,使得不同语言的客户端和服务端可以进行跨平台、跨语言的通信。
-
代码自动生成:gRPC通过protobuf来定义接口和数据类型,并提供了代码生成工具,能够根据proto文件自动生成对应的客户端和服务端代码,大大减少了编码的工作量。
-
可扩展性:gRPC支持基于HTTP/2的双向流式传输,可以同时处理多个请求和响应,提供了更灵活的通信模式,满足各种场景的需求。
-
高度可靠:gRPC使用HTTP/2协议作为传输层,具备服务端推送、流量控制、头部压缩等特性,能够提供可靠的数据传输。
2.5.2 gRPC缺点
-
学习成本较高:gRPC使用Protocol Buffers来定义接口和数据类型,对于不熟悉Protocol Buffers的开发者来说,需要学习新的语法和编程方式。
-
部分语言支持不完善:尽管gRPC支持多种编程语言,但是在某些语言上的支持不如其他语言完善,可能存在某些功能上的限制或者不兼容的问题。
-
难以调试:由于gRPC使用了二进制的数据传输格式和HTTP/2协议,相对于传统的文本格式和HTTP/1.x协议,调试和查看网络通信过程和数据内容相对困难一些。
-
部署复杂:gRPC基于HTTP/2,需要支持HTTP/2的服务器和客户端,需要进行额外的配置和部署工作。
3. gRPC 和 Protobuf 之间的关系
gRPC使用Protobuf作为默认的数据序列化和传输格式,以便在客户端和服务器之间进行通信。gRPC使用Protobuf定义服务接口和消息格式。通过在Protobuf文件中定义服务接口,可以指定服务的方法、请求参数和响应参数等信息。然后,使用Protobuf编译器生成相应的代码,这些代码在客户端和服务器端之间用于进行通信。 因此,gRPC和Protobuf密切合作,通过使用Protobuf来定义数据格式和服务接口,gRPC能够实现高效、可靠的跨网络的通信。同时,Protobuf的紧凑性和可扩展性也为gRPC提供了高效传输和灵活的数据交换能力。