目录

Go gRPC

gRPC 是一个现代的开源高性能远程过程调用(RPC - Remote Procedure Call)框架,可以在任何环境中运行。它可以通过对负载平衡、跟踪、健康检查和身份验证的可插拔支持,有效地连接数据中心内和跨数据中心的服务。它也适用于分布式计算的最后一英里,将设备、移动应用程序和浏览器连接到后端服务。

gRPC 的特性

  • 简单服务定义:使用 Protocol Buffers 定义你的服务,这是一个强大的二进制序列化工具集和语言;
  • 快速启动并扩展:只需一行代码即可安装运行时和开发环境,还可以通过该框架扩展到每秒数百万个 RPCs
  • 跨语言和平台工作:使用多种语言和平台自动为你的服务生成惯用的客户端和服务器桩;
  • 双向流和集成身份验证:双向流式传输和完全集成的可插拔身份验证,以及基于 HTTP/2 的传输;

gRPC 与 RPC 的关系

  • RPC 是一种协议,gRPC 是基于 RPC 协议实现的一种框架;
  • gRPC 解决了 RPC 的三大问题:
    • 协议约定;
    • 传输协议;
    • 服务发现;

gRPC 适用场景

gRPC 环境配置

安装 protobuf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ brew install autoconf automake libtool
$ brew install protobuf --HEAD
$ git clone https://github.com/protocolbuffers/protobuf.git
$ cd protobuf
$ git submodule update --init --recursive
$ ./autogen.sh
$ ./configure
$ make
$ make check
$ sudo make install

安装 Go 的协议编译器插件

1
2
$ go install github.com/golang/protobuf/protoc-gen-go@v1.5.2
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0

更新 PATH 路径

1
$ export PATH="$PATH:$(go env GOPATH)/bin"

实例

实例一

定义服务

1
2
3
$ mkdir greeter
$ cd greeter
$ mkdir hello

hello 目录下创建 hello.proto 文件并复制以下内容:

syntax = "proto3";

option go_package = "greeter.io/greeter/examples/hello/hello";
option java_multiple_files = true;
option java_package = "io.greeter.examples.hello";
option java_outer_classname = "HelloProto";

package hello;

// The greeting service definition.
service Greeter {
  // Sends a greeting.
  rpc hello(HelloRequest) returns (HelloResponse) {}
}

// The request message containing the user's name.
message HelloRequest { string name = 1; }

// The response message containing the greetings.
message HelloResponse { string message = 1; }

完成后在 greeter 目录下执行以下命令:

1
2
3
$ protoc --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  hello/hello.proto

命令完成后在 hello 目录下会生成 hello_grpc.pb.gohello.pb.go 文件。

编写服务端

greeter/cmd/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
40
package main

import (
	"context"
	"flag"
	"fmt"
	"greeter/hello"
	"log"
	"net"

	"google.golang.org/grpc"
)

var (
	port = flag.Int("port", 50051, "The server port")
)

type server struct {
	hello.UnimplementedGreeterServer
}

func (s *server) Hello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloResponse, error) {
	log.Printf("Received: %v", in.GetName())
	return &hello.HelloResponse{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()
	hello.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)
	}
}

启动 grpc server 服务:

1
2
$ go run cmd/server/main.go
2022/06/21 13:49:19 Server listening at [::]:50051

编写客户端

greeter/cmd/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
42
package main

import (
	"context"
	"flag"
	"greeter/hello"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	defaultName = "World, Hello Go! 🎉"
)

var (
	addr = flag.String("addr", "localhost:50051", "The address to connect to")
	name = flag.String("name", defaultName, "The name to greet")
)

func main() {
	flag.Parse()

	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := hello.NewGreeterClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	r, err := c.Hello(ctx, &hello.HelloRequest{Name: *name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

启动 grpc client 服务:

1
2
$ go run cmd/client/main.go --name="World, Hello Go 🎉"
2022/06/21 13:51:36 Greeting: Hello World, Hello Go 🎉

最终的代码目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.
├── cmd
│   ├── client
│   │   └── main.go
│   └── server
│       └── main.go
├── go.mod
├── go.sum
└── hello
    ├── hello.pb.go
    ├── hello.proto
    └── hello_grpc.pb.go

实例二

定义服务

1
2
3
$ mkdir routeguide
$ cd routeguide
$ mkdir route

route 目录下创建 route.proto 文件并复制以下内容:

syntax = "proto3";

option go_package = "routeguide/route";
option java_multiple_files = true;
option java_package = "io.routeguide.route";
option java_outer_classname = "RouteProto";

package route;

// Interface exported by the server.
service Route {
  // A simple RPC.
  //
  // Obtains the feature at a given position.
  //
  // A feature with an empty name is returned if there's no feature
  // at the give position.
  rpc GetFeature(Point) returns (Feature) {}

  // A server-to-client streaming RPC.
  //
  // Obtains the features available within the given Rectangle. Results
  // are streamed rather than returned at once (e.g. in a response message
  // with a repeated field), as the rectangle may cover a large area and
  // contain a huge number of features.
  rpc GetFeatures(Rectangle) returns (stream Feature) {}

  // A client-to-server streaming RPC.
  //
  // Accepts a stream of Points on a route being traversed, returning
  // a RouteSummary when traversal is completed.
  rpc RecordRoute(stream Point) returns (RouteSummary) {}

  // A Bidirectional streaming RPC.
  //
  // Accepts a stream of RouteNotes sent whil a route is being traversed,
  // while receiving other RouteNotes (e.g. from other users).
  rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}

// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest interger).
// Latitude should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
  // One corner of the rectangle.
  Point lo = 1;

  // The other corner of the rectangle.
  Point hi = 2;
}

// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
  // The name of the feature.
  string name = 1;

  // The point where the feature is detected.
  Point location = 2;
}

// A RouteNote is a message sent while at a given point.
message RouteNote {
  // The location from which the message is sent.
  Point location = 1;

  // The message to be sent.
  string message = 2;
}

// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number
// of detected features, and the total distance covered as the
// cumulative sum of the distance between eath point.
message RouteSummary {
  // The number of points received.
  int32 point_count = 1;

  // The number of known features passed while traversing the route.
  int32 feature_count = 2;

  // The distance covered in metres.
  int32 distance = 3;

  // The duration of the traversal in seconds.
  int32 elapsed_time = 4;
}

完成后在 routeguide 目录下执行以下命令:

1
2
3
$ protoc --go_out=. --go_opt=paths=source_relative \
  --go-grpc_out=. --go-grpc_opt=paths=source_relative \
  route/route.proto

编写服务端

routeguide/cmd/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
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
// Package main implements a simple gRPC server that demonstrates how to use gRPC-Go libraries
// to perform unary, client streaming, server streaming and full duplex RPCs.
//
// It implements the route service whose definition can be found in routeguide/route.proto.
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"math"
	"net"
	"sync"
	"time"

	"github.com/golang/protobuf/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"

	"routeguide/data"
	pb "routeguide/route"
)

var (
	tls      = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
	keyfile  = flag.String("keyfile", "", "The TLS key file")
	certfile = flag.String("certfile", "", "The TLS certificate file")
	jsonfile = flag.String("jsonfile", "", "A JSON file containing a list of features")
	port     = flag.Int("port", 50051, "The server port")
)

type routeServer struct {
	pb.UnimplementedRouteServer

	// read-only after initialized.
	savedFeatures []*pb.Feature

	routeNotes map[string][]*pb.RouteNote

	// protects routeNotes.
	mu sync.Mutex
}

// GetFeature returns the feature at the given point.
func (s *routeServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
	for _, feature := range s.savedFeatures {
		if proto.Equal(feature.Location, point) {
			return feature, nil
		}
	}

	// No feature was found, return an unnamed feature
	return &pb.Feature{Location: point}, nil
}

// GetFeatures lists all features contained within the given bounding Rectangle.
func (s *routeServer) GetFeatures(rect *pb.Rectangle, stream pb.Route_GetFeaturesServer) error {
	for _, feature := range s.savedFeatures {
		if inRange(feature.Location, rect) {
			if err := stream.Send(feature); err != nil {
				return err
			}
		}
	}
	return nil
}

// RecordRoute records a route composited of a sequence of points.
//
// It gets a stream of points, and responds with statistics about the "trip":
// number of points,  number of known features visited, total distance traveled, and
// total time spent.
func (s *routeServer) RecordRoute(stream pb.Route_RecordRouteServer) error {
	var pointCount, featureCount, distance int32
	var lastPoint *pb.Point
	stime := time.Now()
	for {
		point, err := stream.Recv()
		if err == io.EOF {
			etime := time.Now()
			return stream.SendAndClose(&pb.RouteSummary{
				PointCount:   pointCount,
				FeatureCount: featureCount,
				Distance:     distance,
				ElapsedTime:  int32(etime.Sub(stime).Seconds()),
			})
		}

		if err != nil {
			return err
		}
		pointCount++

		for _, feature := range s.savedFeatures {
			if proto.Equal(feature.Location, point) {
				featureCount++
			}
		}

		if lastPoint != nil {
			distance += calcDistance(lastPoint, point)
		}
		lastPoint = point
	}
}

// RouteChat receives a stream of message/location pairs, and responds with
// a stream of all previous messages at each of those locations.
func (s *routeServer) RouteChat(stream pb.Route_RouteChatServer) error {
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		key := serialize(in.Location)

		s.mu.Lock()
		s.routeNotes[key] = append(s.routeNotes[key], in)

		// Note: this copy prevents blocking other clients while serving this one.
		// We don't need to do a deep copy, because elements in the slice are
		// insert-only and never modified.
		rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
		copy(rn, s.routeNotes[key])
		s.mu.Unlock()

		for _, note := range rn {
			if err := stream.Send(note); err != nil {
				return err
			}
		}
	}
}

// loadFeatures loads features from a JSON file.
func (s *routeServer) loadFeatures(path string) {
	var data []byte
	if path == "" {
		path = "./data/data.json"
	}

	data, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatalf("failed to load default features: %v", err)
	}

	if err := json.Unmarshal(data, &s.savedFeatures); err != nil {
		log.Fatalf("failed to load default features: %v", err)
	}
}

// calcDistance calculates the distance between two points using the "haversine" formula.
// The formula is based on http://mathforum.org/library/drmath/view/51879.html.
func calcDistance(p1 *pb.Point, p2 *pb.Point) int32 {
	const CordFactor float64 = 1e7
	const R = float64(6371000) // earth radius in metres

	lat1 := toRadians(float64(p1.Latitude) / CordFactor)
	lat2 := toRadians(float64(p2.Latitude) / CordFactor)
	lng1 := toRadians(float64(p1.Longitude) / CordFactor)
	lng2 := toRadians(float64(p2.Longitude) / CordFactor)

	dlat := lat2 - lat1
	dlng := lng2 - lng1

	a := math.Sin(dlat/2)*math.Sin(dlat/2) +
		math.Cos(lat1)*math.Cos(lat2)*
			math.Sin(dlng/2)*math.Sin(dlng/2)
	c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))

	return int32(R * c)
}

func toRadians(num float64) float64 {
	return num * math.Pi / float64(180)
}

func inRange(point *pb.Point, rect *pb.Rectangle) bool {
	left := math.Min(float64(rect.Lo.Longitude), float64(rect.Hi.Longitude))
	right := math.Max(float64(rect.Lo.Longitude), float64(rect.Hi.Longitude))
	top := math.Min(float64(rect.Lo.Latitude), float64(rect.Hi.Latitude))
	btm := math.Max(float64(rect.Lo.Latitude), float64(rect.Hi.Latitude))

	if float64(point.Longitude) >= left &&
		float64(point.Longitude) <= right &&
		float64(point.Latitude) >= btm &&
		float64(point.Latitude) <= top {
		return true
	}
	return false
}

func serialize(point *pb.Point) string {
	return fmt.Sprintf("%d %d", point.Latitude, point.Longitude)
}

func newServer() *routeServer {
	s := &routeServer{routeNotes: make(map[string][]*pb.RouteNote)}
	s.loadFeatures(*jsonfile)
	return s
}

func main() {
	flag.Parse()

	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	var opts []grpc.ServerOption
	if *tls {
		if *certfile == "" {
			*certfile = data.Path("./x509/server_cert.pem")
		}
		if *keyfile == "" {
			*keyfile = data.Path("./x509/server_key.pem")
		}
		creds, err := credentials.NewServerTLSFromFile(*certfile, *keyfile)
		if err != nil {
			log.Fatalf("failed to generate credentials %v", err)
		}
		opts = []grpc.ServerOption{grpc.Creds(creds)}
	}

	srv := grpc.NewServer(opts...)
	log.Printf("Server listening at %v", lis.Addr())
	pb.RegisterRouteServer(srv, newServer())
	srv.Serve(lis)
}

启动 grpc server 服务:

1
2
$ go run cmd/server/main.go
2022/06/25 13:53:08 Server listening at 127.0.0.1:50051

编写客户端

routeguide/cmd/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
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
package main

import (
	"context"
	"flag"
	"io"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"

	"routeguide/data"
	pb "routeguide/route"
)

var (
	tls      = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP")
	certfile = flag.String("certfile", "", "The file containing the CA root cert file")
	srvAddr  = flag.String("addr", "localhost:50051", "The server address in the format of host:port")
	srvHost  = flag.String("host", "example.io", "The server name used to verify the hostname returned by the TLS handshake")
)

// printFeature gets the feature for the given point.
func printFeature(client pb.RouteClient, point *pb.Point) {
	log.Printf("Getting feature for point (%d, %d)", point.Latitude, point.Longitude)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	feature, err := client.GetFeature(ctx, point)
	if err != nil {
		log.Fatalf("client.GetFeature failed: %v", err)
	}
	log.Println(feature)
}

// printFeatures lists all the features within the given bounding Rectangle.
func printFeatures(client pb.RouteClient, rect *pb.Rectangle) {
	log.Printf("Looking for features within %v", rect)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	stream, err := client.GetFeatures(ctx, rect)
	if err != nil {
		log.Fatalf("client.GetFeatures failed: %v", err)
	}

	for {
		feature, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Fatalf("client.GetFeatures failed: %v", err)
		}
		log.Printf("Feature: name: %q, point:(%v, %v)",
			feature.GetName(),
			feature.GetLocation().GetLatitude(),
			feature.GetLocation().GetLongitude(),
		)
	}
}

func main() {
	flag.Parse()
	var opts []grpc.DialOption
	if *tls {
		if *certfile == "" {
			*certfile = data.Path("./x509/ca_cert.pem")
		}
		creds, err := credentials.NewServerTLSFromFile(*certfile, *srvHost)
		if err != nil {
			log.Fatalf("failed to generate credentials %v", err)
		}
		opts = append(opts, grpc.WithTransportCredentials(creds))
	} else {
		opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
	}

	conn, err := grpc.Dial(*srvAddr, opts...)
	if err != nil {
		log.Fatalf("grpc.Dial failed: %v", err)
	}

	defer conn.Close()
	client := pb.NewRouteClient(conn)

	// Looking for a valid feature
	printFeature(client, &pb.Point{Latitude: 409146138, Longitude: -746188906})

	// Feature missing.
	printFeature(client, &pb.Point{Latitude: 0, Longitude: 0})

	// Looking for features between 40, -75 and 42, -73.
	printFeatures(client, &pb.Rectangle{
		Lo: &pb.Point{Latitude: 400000000, Longitude: -750000000},
		Hi: &pb.Point{Latitude: 420000000, Longitude: -730000000},
	})
}

启动 grpc client 服务:

1
2
3
4
5
$ go run cmd/client/main.go
2022/06/25 13:54:45 Getting feature for point (409146138, -746188906)
2022/06/25 13:54:45 name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:{latitude:409146138 longitude:-746188906}
2022/06/25 13:54:45 Getting feature for point (0, 0)
2022/06/25 13:54:45 location:{}