开始使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | $ mkdir product_info
$ cd product_info
$ go mod init example.com/product_info
$ mkdir ecommerce server client
$ tree
.
├── client
├── ecommerce
├── go.mod
└── server
3 directories, 1 file
$ go get -u google.golang.org/grpc
$ go get -u github.com/gofrs/uuid
|
创建服务定义
在开发 gRPC 应用程序时,要先定义服务接口,其中包含允许远程调用的方法、方法参数以及调用这些方法所使用的消息格式等。
这些服务定义都以 protocol buffers 定义的形式进行记录,也就是 gRPC 中所使用的接口定义语言。
在确定了服务的业务功能之后,就可以定义服务接口来满足业务需要了。
在本示例中,可以看到 ProductInfo 服务有两个远程方法,即 addProduct(Product) 和 getProduct(ProductID),并且这两个方法都会接受或返回两个消息类型(Product 和 ProductID)
接下来以 protocol buffers 定义的形式声明这些服务定义。
protocol buffers 可以定义消息类型和服务类型,其中消息包含字段,每个字段由其类型和唯一索引值定义;
服务则包含方法,每个方法由其类型、输入参数和输出参数进行定义。
新建文件 ecommerce/ecommerce.proto
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | syntax = "proto3"; // 服务定义首先要指定所使用的 protocol buffers 版本(proto3)
option go_package = "example.com/product_info/ecommerce";
package ecommerce; // 为了避免协议消息类型之间的命名冲突,这里使用了包名,它也会用于生成代码
service ProductInfo { //服务接口定义
rpc addProduct(Product) returns (ProductID); //用于添加商品的远程方法,它会返回商品 ID 作为响应
rpc getProduct(ProductID) returns (Product); // 基于商品 ID 获取商品的远程方法
}
message Product { // Product 消息类型(格式)的定义
string id = 1; // 用来保存商品 ID 的字段(名-值对),使用唯一的数字来标识二进制消息格式中的各个字段
string name = 2;
string description = 3;
float price = 4;
}
message ProductID { // ProductID 消息类型(格式)的定义
string value = 1;
}
|
在 protocol buffers 定义中,可以指定包名(option go_package
),这样做能够避免在不同的项目间出现命名冲突。
当使用这个包属性生成服务或客户端代码时,除非明确指明了不同的包名,否则将为对应的编程语言生成相同的包。
当然,该语言需要支持包的概念。
还有一个过程需要注意,那就是从其他 proto 文件中进行导入。
如果需要使用其他 proto 文件中定义的消息类型,那么可以将它们导入本例的 protocol buffers 定义中。
如果要使用 wrappers.proto 文件中的 StringValue 类型(google.protobuf.StringValue
),就可以按照如下方式在定义中导入google/protobuf/wrappers.proto
文件:
| syntax = "proto3";
import "google/protobuf/wrappers.proto";
package ecommerce;
...
|
| # 在项目根目录执行如下命令
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
ecommerce/ecommerce.proto
|
会生成两个文件:
ecommerce/ecommerce.pb.go
: which contains all the protocol buffer code to populate, serialize, and retrieve request and response message types.
ecommerce/ecommerce_grpc.pb.go
: which contains the following:
- An interface type (or stub) for clients to call with the methods defined in the
ProductInfo
service.
- An interface type for servers to implement, also with the methods defined in the
ProductInfo
service
在完成服务定义的规范之后,就可以处理 gRPC 服务和客户端的实现了
实现服务端
接下来要实现该 gRPC 服务,它包含了我们在服务定义中所声明的远程方法。
这些方法会通过服务器端暴露出来,gRPC 客户端会连接到服务器端,并调用这些远程方法
新建文件: server/server.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | package main
import (
"context"
pb "example.com/product_info/ecommerce" // 导入刚刚通过 protobuf 编译器所生成的代码所在的包
"github.com/gofrs/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type server struct { // server 结构体是对服务器的抽象。可以通过它将服务方法附加到服务器上
pb.UnimplementedProductInfoServer
productMap map[string]*pb.Product
}
// 这两个方法都有一个 Context 参数。Context 对象包含一些元数据,比如终端用户授权令牌的标识和请求的截止时间。这些元数据会在请求的生命周期内一直存在
// 这两个方法都会返回一个错误以及远程方法的返回值(方法有多种返回类型)。这些错误会传播给消费者,用来进行消费者端的错误处理
// AddProduct 方法以 Product 作为参数并返回一个 ProductID。Product 和 ProductID 结构体定义在 ecommerce.pb.go 文件中,该文件是通过 ecommerce.proto 定义自动生成的
func (s *server) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
out, err := uuid.NewV4()
if err != nil {
return nil, status.Errorf(codes.Internal, "Error while generating Product Id", err)
}
in.Id = out.String()
if s.productMap == nil {
s.productMap = make(map[string]*pb.Product)
}
s.productMap[in.Id] = in
return &pb.ProductID{Value: in.Id}, status.New(codes.OK, "").Err()
}
// GetProduct 方法以 ProductID 作为参数并返回 Product
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
value, exists := s.productMap[in.Value]
if exists {
return value, status.New(codes.OK, "").Err()
}
return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}
|
这样就实现了 ProductInfo 服务的业务逻辑。接下来可以创建简单的服务器,来托管该服务并接受来自客户端的请求
新建 server/main.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | package main
import (
"log"
"net"
pb "example.com/product_info/ecommerce" // 导入通过 protobuf 编译器所生成的代码所在的包
"google.golang.org/grpc"
)
const (
port = ":50051"
)
func main() {
lis, err := net.Listen("tcp", port) // 希望由 gRPC 服务器所绑定的 TCP 监听器在给定的端口(50051)上创建
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer() // 通过调用 gRPC Go API 创建新的 gRPC 服务器实例
pb.RegisterProductInfoServer(s, &server{}) // 通过调用生成的 API,将之前生成的服务注册到新创建的 gRPC 服务器上
log.Printf("Starting gRPC listener on port" + port)
if err := s.Serve(lis); err != nil { // 在指定的端口(50051)上开始监听传入的消息
log.Fatalf("failed to serve: %v", err)
}
}
|
现在,我们已通过 Go 语言为业务场景构建了 gRPC 服务。同时,我们创建了简单的服务器,该服务器将暴露服务方法,并接收来自 gRPC 客户端的消息
实现客户端
新建 client/main.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | package main
import (
"context"
"log"
"time"
pb "example.com/product_info/ecommerce"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure()) // 根据提供的地址(localhost:50051)创建到服务器端的链接。这里创建了一个客户端和服务器端之间的连接,但它目前不安全
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close() // 所有事情都完成后,关闭连接
c := pb.NewProductInfoClient(conn) // 传递连接并创建存根文件。这个实例包含可调用服务器的所有远程方法
name := "Apple iPhone 11"
description := `Meet Apple iPhone 11.All-new
dual-camera system with
Ultra Wide and Night mode.`
price := float32(1000.0)
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // 创建 Context 以传递给远程调用。这里的 Context 对象包含一些元数据,如终端用户的标识、授权令牌以及请求的截止时间,该对象会在请求的生命周期内一直存在
defer cancel()
r, err := c.AddProduct(ctx, &pb.Product{Name: name, Description: description, Price: price}) // 使用商品的详情信息调用 AddProduct 方法。如果操作成功完成,就会返回一个商品 ID,否则将返回一个错误
if err != nil {
log.Fatalf("Could not add product: %v", err)
}
log.Printf("Product ID: %s added sucessfully", r.Value)
product, err := c.GetProduct(ctx, &pb.ProductID{Value: r.Value}) // 使用商品 ID 来调用 GetProduct 方法。如果操作成功完成,将返回商品详情,否则会返回一个错误
if err != nil {
log.Fatalf("Could not get product: %v", err)
}
log.Println("Product:", product.String())
}
|
运行
在项目根目录下
首先运行
| $ go run server/*.go
2021/10/03 08:43:21 Starting gRPC listener on port:50051
|
启动服务端
然后运行 go run client/main.go
| $ go run client/main.go
2021/10/03 08:43:29 Product ID: 4ccd2719-e5fb-4b33-b382-2910abb0e75c added sucessfully
2021/10/03 08:43:29 Product: id:"4ccd2719-e5fb-4b33-b382-2910abb0e75c" name:"Apple iPhone 11" description:"Meet Apple iPhone 11.All-new\n\tdual-camera system with\n\t\tUltra Wide and Night mode." price:1000
|
可以看到客户端与服务端通信成功
官方示例
| $ mkdir helloworld
$ cd helloworld
$ go mod init example.com/helloworld
$ mkdir helloworld greeter_server greeter_client
$ go get -u google.golang.org/grpc
|
新建文件 helloworld/helloworld.proto
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | syntax = "proto3";
option go_package = "example.com/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;
}
|
| # 在项目根目录执行如下命令
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
|
新建 greeter_server/main.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 | // Package main implements a server for Greeter service.
package main
import (
"context"
"log"
"net"
pb "example.com/helloworld/helloworld"
"google.golang.org/grpc"
)
const (
port = ":50051"
)
// 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() {
lis, err := net.Listen("tcp", 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)
}
}
|
新建greeter_client/main.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 | // Package main implements a client for Greeter service.
package main
import (
"context"
"log"
"os"
"time"
pb "example.com/helloworld/helloworld"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}
|
启动服务端服务: go run greeter_server/main.go
启动客户端与服务端通信:
| $ go run greeter_client/main.go
2021/10/03 11:37:14 Greeting: Hello world
|
更新服务
helloworld/helloworld.proto
中添加新的方法 SayHelloAgain
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | syntax = "proto3";
option go_package = "example.com/helloworld/helloworld";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends another greeting
rpc SayHelloAgain (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;
}
|
| # 在项目根目录执行如下命令
$ protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto
|
更新服务端:
greeter_server/main.go
添加如下内容:
| func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}
|
更新客户端 greeter_client/main.go
结尾增加如下内容:
| r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
|
运行服务端:
| $ go run greeter_server/main.go
|
运行客户端:
| $ go run greeter_client/main.go Alice
2021/10/03 13:11:20 Greeting: Hello Alice
2021/10/03 13:11:20 Greeting: Hello again Alice
|