RPC 是指远程过程调用,允许一台计算机上的程序调用另一台计算机上的程序,就像对本地函数进行调用一样方便。
gRPC 是由 google 开源的一个高性能、通用的 RPC 框架,基于 HTTP/2 协议标准,同时支持大多数流行的编程语言。
gRPC 官方地址:
gRPC 默认使用 protobuf 协议进行数据序列化,protobuf 是 Google 开源的一套数据结构,适合高性能的数据传输场景;
protobuf 与 JSON,XML 相比,具有以下优点:
protobuf 数据类型与 golang 的对应关系如下:
.proto Type | go Type | .proto Type | go Type |
---|---|---|---|
double | float64 | fixed32 | uint32 |
float | float32 | fixed64 | uint64 |
int32 | int32 | sfixed32 | int32 |
int64 | int64 | sfixed64 | int64 |
uint32 | uint32 | bool | bool |
uint64 | uint64 | string | string |
sint32 | int32 | bytes | []byte |
sint64 | int64 |
gPRC 调用模型如下:
protoc 是 protobuf 的编译器,用来编译 .proto
文件(转化成对应语言的代码文件),其下载地址是 https://github.com/protocolbuffers/protobuf/releases 。
注:我这里使用的是 Linux 系统,下载的是 proto3 版本。
下载到 protobuf-all-3.20.3.tar.gz
包后,使用下面命令进行解压,编译,安装:
> tar -xzf protobuf-all-3.20.3.tar.gz # 解压
> cd ./protobuf-3.20.3
> ./configure
> make # 编译,此过程可能会比较慢
> make install # 安装,需要 root 权限
> ldconfig # 系统重新加载动态链接库,需要 root 权限
检查是否安装成功:
> protoc --version
————————————————————
libprotoc 3.20.3
针对不同的语言,还需要安装运行时的 protoc 插件,Go 语言对应的是 protoc-gen-go,当使用 protoc 时,会自动调用 protoc-gen-go。
安装方法如下:
# 需要在 go 项目目录中执行
go get -u google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
查看 go 版本:
> go version
go version go1.19 linux/amd64
创建一个 go 项目:
> mkdir hello-protobuf
> cd ./hello-protobuf
> go mod init github.com/hello-protobuf # 使用 Go Module 管理项目,生成 go.mod 文件
下载 protoc-gen-go:
> go get github.com/golang/protobuf/protoc-gen-go
如果无法下载,则可以更换代理地址:
go env -w GOPROXY=https://goproxy.cn
安装 protoc-gen-go:
> go install github.com/golang/protobuf/protoc-gen-go
最后,将 protoc-gen-go 放在系统目录中:
> cp protoc-gen-go /usr/local/go/bin/
注意:protoc-gen-go 在
$GOPATH/bin
目录中
例如我们现在要传输用户信息,包括 name 和 age 两个字段。
创建 user.proto
文件如下:
syntax = "proto3";
// 这是一行注释
option go_package="./helloworld";
package helloworld;
message User {
string name = 1;
int32 age = 2;
}
其中:
.pb.go
文件的路径,一般跟 package 包名一致。//
开头。;
结尾。执行如下命令,生成 .pd.go
文件:
> protoc --go_out=. user.proto
--go_out
用于指定输出目录,它会加载 protoc-gen-go 程序。创建 main.go 文件:
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
"github.com/hello-protobuf/helloworld"
)
func main() {
user := &helloworld.User{
Name: "lucy",
Age: 18,
}
// 序列化
s_user, err := proto.Marshal(user)
if err != nil {
panic(err)
}
// 反序列化
newUser := &helloworld.User{}
err = proto.Unmarshal(s_user, newUser)
if err != nil {
panic(err)
}
fmt.Println(newUser)
}
执行 go 程序:
> go run main.go
——————————————————————
name:"lucy" age:18
来看下现在的目录结构:
> tree hello-protobuf/
————————————————————————
hello-protobuf/
├── go.mod
├── go.sum
├── helloworld
│ └── user.pb.go
├── main.go
└── user.proto
例如下面定义:
message User {
string name = 1;
int32 age = 2;
optional string password = 3;
repeated string addres = 4;
}
生成的 protobuf 结构如下:
type User struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
Password *string `protobuf:"bytes,3,opt,name=password,proto3,oneof" json:"password,omitempty"`
Addres []string `protobuf:"bytes,4,rep,name=addres,proto3" json:"addres,omitempty"`
}
oneof 表示只设置多个字段中的一个字段。
示例:
message User {
oneof sex {
string man = 1;
string woman = 2;
}
}
编译后生成的类型如下:
type User struct {
// Types that are assignable to Sex:
//
// *User_Man
// *User_Woman
Sex isUser_Sex `protobuf_oneof:"sex"`
}
枚举类型,用于限定传入的字段值必须是预定义中的值。
enum 类型:
enum Sex {
man = 0; // 必须从 0 开始
woman = 1;
}
message User {
Sex sex = 1;
}
map 类型:
message Hello {
map<string, string> names = 1;
}
message 可以嵌套,称为嵌套类型,嵌套类型可以定义比较复杂的结构体。
嵌套有两种模式:
内部嵌套示例:
message Hello {
// World 在 Hello 内部
message World {
string name = 1;
}
World world = 1;
}
Hello.World
外部嵌套示例:
// World 在 Hello 外部
message World {
string name = 1;
}
message Hello {
World world = 1;
}
要想将消息类型用在 RPC 这种,需要使用 service 关键字来定义 RPC 接口。
示例:
// The greeter 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;
}
这里的 gRPC Golang 版示例来自官网:
下载示例代码:
> git clone -b v1.49.0 --depth 1 https://github.com/grpc/grpc-go.git
> cd grpc-go/examples/helloworld
下文中对官方示例做了一些简化。
> mkdir hello-grpc
> cd ./hello-grpc
> go mod init github.com/hello-grpc # 使用 Go Module 管理项目,生成 go.mod 文件
syntax = "proto3";
option go_package = "./helloworld";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
编译 proto 文件:
> protoc --go_out=plugins=grpc:. helloworld.proto
看下当前的目录结构:
> tree hello-grpc
————————————————————————————
hello-grpc
├── go.mod
├── go.sum
├── helloworld
│ └── helloworld.pb.go
└── helloworld.proto
package main
import (
"context"
"log"
"net"
pb "github.com/hello-grpc/helloworld"
"google.golang.org/grpc"
)
// 定义一个类型
type GreeterServerImp struct {
}
// 实现 GreeterServer 接口中的 SayHello 方法
func (s *GreeterServerImp) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("GreeterServerImp Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func main() {
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &GreeterServerImp{})
lis, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
package main
import (
"context"
"log"
"time"
pb "github.com/hello-grpc/helloworld"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "world"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
运行服务端程序:
> go run grcp_server.go
运行客户端程序:
> go run grcp_client.go
—————————————————————————————————————————
2022/10/07 17:42:03 Greeting: Hello world
(本节完。)