Skip to content

开始使用

 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文件:

1
2
3
4
5
6
syntax = "proto3";

import "google/protobuf/wrappers.proto";

package ecommerce;
...
1
2
3
4
# 在项目根目录执行如下命令
$ 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())
}

运行

在项目根目录下

首先运行

1
2
$ go run server/*.go
2021/10/03 08:43:21 Starting gRPC listener on port:50051

启动服务端

然后运行 go run client/main.go

1
2
3
$ 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

可以看到客户端与服务端通信成功

官方示例

1
2
3
4
5
$ 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;
}
1
2
3
4
# 在项目根目录执行如下命令
$ 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

启动客户端与服务端通信:

1
2
$ 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;
}
1
2
3
4
# 在项目根目录执行如下命令
$ 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
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 结尾增加如下内容:

1
2
3
4
5
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())

运行服务端:

1
$ go run greeter_server/main.go

运行客户端:

1
2
3
4
$ 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