概述
什么是gRPC
gRPC是一个高性能、开源的、通用的远程过程调用 (RPC,Remote Procedure Call) 框架,由Google主导开发。gRPC用于在服务端和客户端之间进行通信,支持多种流行的编程语言。gRPC主要使用Protocol Buffers作为其接口定义语言,来描述服务接口和消息类型。
要实现gRPC服务,需要完成以下步骤:
1)定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。
2)在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。
3)在客户端拥有一个像服务端一样的方法的存根,即Stub
### gRPC与HTTP相比的优缺点
优点:
- 性能:gRPC基于HTTP/2协议,因此可以利用其多路复用、二进制帧、头部压缩等优点,从而提高性能。
- 通信协议:gRPC使用Protocol Buffers(Protobuf),这是一种比JSON更紧凑、更快速的二进制格式,同时还可以生成API接口和数据结构代码,而HTTP通常使用JSON或XML作为数据格式,这在某些情况下可能会更加冗长且效率较低。
- 流:gRPC支持四种类型的流,包括单向流和双向流,而HTTP/1.1只支持请求/响应模式。
- 双向认证和负载均衡:gRPC支持TLS和双向认证,并且原生支持各种类型的负载均衡。
- 语言独立:gRPC支持多种语言,包括C、Java、Python、Go、Ruby、C#等,这使得在不同的系统和语言之间进行通信变得更加容易。
缺点:
- 浏览器兼容性:由于gRPC基于HTTP/2,而不是所有的浏览器都支持HTTP/2,因此它在浏览器兼容性上可能存在问题。
- 可读性差:gRPC使用二进制格式,对于调试和测试来说,可读性比HTTP+JSON差。
gRPC常用的场景
- 微服务:gRPC是构建微服务的理想选择,因为它可以轻松处理服务间的通信和数据传输。由于其性能优异,可以用来构建高性能的微服务架构。
- 低延迟、高并发的系统:gRPC的高性能特性使其非常适合需要低延迟和高并发的系统。
- 实时应用:gRPC的流特性使得它非常适合实时应用,比如在线游戏、聊天服务、物联网设备通信等。
- 多语言环境:由于gRPC支持多种编程语言,因此非常适合多语言环境的使用,可以方便地在不同语言编写的服务之间进行通信。
实现gRPC
安装gRPC
Go安装gRPC
-
安装gRPC
go get -u google.golang.org/grpc
-
安装Protocol Buffers v3
brew install protobuf
-
安装Go的protobuf插件
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
Python安装gRPC
- 安装gRPC
pip install grpcio
- 安装Protobuf
pip install protobuf
- 安装gRPC的Protobuf插件
pip install grpcio-tools
定义.proto文件
什么是Protocol Buffers
Protocol Buffers (Protobuf) 是 Google 开发的一种用于结构化数据序列化的协议,类似于 XML、JSON 和 YAML,但相比之下,它更小、更快、更简单。可以用于数据存储、通信协议、数据交换等领域。
proto文件介绍
下面通过一个例子说明编写一个.proto文件,主要格式如下:
syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本
package getWords; // 声明包名,该文件的参数都在这个命名空间中的,避免与其他proto文件有相同参数名而发生冲突
option go_package="./getWords;getWords"; // 以(;)进行分隔,前面是生成文件的路径,后面是就是生成go代码时的package名
// 定义服务,带有接口getPersionalInformation
service Greeter {
rpc getPersionalInformation (PersonRequest) returns (PersonReply) {}
}
// 定义请求消息
message PersonRequest {
int32 id = 1;
}
// 定义响应消息
message PersonReply {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType { // 枚举类型
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4; // 重复字段
}
该文件的字段声明是:有一个PersonReply的消息类型,它有四个字段,其中第一个字段是name,类型为字符串,第二个字段为id,类型是int32,第三个字段是email,类型是字符串,第四个字段是phones,它是一个重复字段,且类型是PhoneNumber。PhoneNumber也是一个消息类型,它有两个字段,一个是字符串类型number,一个是PhoneType类型的type。PhoneType是一个枚举类型,它有三个值,MOBILE、HOME和WORK。如果用json表示一条消息的话,如下:
{
"name": "John",
"id": 1,
"email": "xx@xx.com"
"phones": [{"number": "123", "type": "MOBILE"}, {"number": "345", "type": "HOME"}]
}
proto字段类型
想要完成自己的.proto文件的定义,主要在于字段的声明,常用的字段类型如下:
- 字符串:
string
- 整型:
int32
,int64
,uint32
,uint64
,sint32
,sint64
,fixed32
,fixed64
,sfixed32
,sfixed64
- 浮点型:
float
,double
- 布尔值:
bool
- 枚举类型:
enum
- 字节序列:
bytes
- 嵌套消息类型
- 重复字段:
repeated
值得注意的是,在 Protobuf 3 中所有字段都是可选的,字段是否有默认值取决于字段的类型。同时 Protobuf 3 移除了required和optional标签。
HelloWorld.proto
我们使用源代码中的HelloWorld示例
- 初始化项目:
go mod init helloWorld
- 创建proto文件
./helloWorld
├── go.mod
└── helloworld
├── helloworld.proto
syntax = "proto3";
option go_package = "google.golang.org/grpc/examples/helloworld/helloworld";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
使用Go实现gRPC服务端
-
生成grpc代码
protoc --go_out=./ --go-grpc_out=./ ./helloworld/helloworld.proto
./helloWorld
├── go.mod
└── helloworld
├── helloworld.pb.go
├── helloworld.proto
└── helloworld_grpc.pb.go -
编写服务端代码
// Package main implements a server for Greeter service. package main import ( "context" "flag" "fmt" "log" "net" "google.golang.org/grpc" pb "helloWorld/helloworld" ) var ( port = flag.Int("port", 50051, "The server port") ) // server is used to implement helloworld.GreeterServer. type server struct { pb.UnimplementedGreeterServer } // SayHello implements helloworld.GreeterServer func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
./helloWorld
├── go.mod
├── go.sum
├── helloworld
│ ├── helloworld.pb.go
│ ├── helloworld.proto
│ └── helloworld_grpc.pb.go
└── server
└── main.go -
运行server
go mod tidy
go run server/main.go
使用Python实现gRPC客户端
-
生成grpc代码
python -m grpc_tools.protoc -I. --python_out=./ --grpc_python_out=./ ./helloworld/helloworld.proto
./helloWorld
└── helloworld
├── helloworld.proto
├── helloworld_pb2.py
└── helloworld_pb2_grpc.py -
编写客户端代码
from __future__ import print_function import logging import grpc from helloworld import helloworld_pb2 from helloworld import helloworld_pb2_grpc def run(): # NOTE(gRPC Python Team): .close() is possible on a channel and should be # used in circumstances in which the with statement does not fit the needs # of the code. print("Will try to greet world ...") with grpc.insecure_channel("localhost:50051") as channel: stub = helloworld_pb2_grpc.GreeterStub(channel) response = stub.SayHello(helloworld_pb2.HelloRequest(name="you")) print("Greeter client received: " + response.message) if __name__ == "__main__": logging.basicConfig() run()
./helloWorld
├── client
│ └── client.py
└── helloworld
├── init.py
├── helloworld.proto
├── helloworld_pb2.py
└── helloworld_pb2_grpc.py -
运行client
python client/client.py
获得输出:
Will try to greet world …
Greeter client received: Hello you