go微服务gRPC

Posted 胡毛毛_三月

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go微服务gRPC相关的知识,希望对你有一定的参考价值。

gRPC教程

RPC算是近些年比较火热的概念了,随着微服务架构的兴起,RPC的应用越来越广泛。本文介绍了RPC和gRPC的相关概念,并且通过详细的代码示例介绍了gRPC的基本使用。

gRPC是什么

gRPC是一种现代化开源的高性能RPC框架,能够运行于任意环境之中。最初由谷歌进行开发。它使用HTTP/2作为传输协议。

快速了解HTTP/2就戳HTTP/2相比HTTP/1.x有哪些重大改进?

在gRPC里,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。与许多RPC系统一样,gRPC是基于定义一个服务,指定一个可以远程调用的带有参数和返回类型的的方法。在服务端程序中实现这个接口并且运行gRPC服务处理客户端调用。在客户端,有一个stub提供和服务端相同的方法。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6hAz9z0t-1668140783539)(https://www.liwenzhou.com/images/Go/grpc/grpc.svg)]

为什么要用gRPC

使用gRPC, 我们可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,反过来,它们可以应用在各种场景中,从Google的服务器到你自己的平板电脑—— gRPC帮你解决了不同语言及环境间通信的复杂性。使用protocol buffers还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。总之一句话,使用gRPC能让我们更容易编写跨语言的分布式代码。

IDL(Interface description language)是指接口描述语言,是用来描述软件组件接口的一种计算机语言,是跨平台开发的基础。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Go写成。

安装gRPC

安装gRPC

在你的项目目录下执行以下命令,获取 gRPC 作为项目依赖。

go get google.golang.org/grpc@latest

安装Protocol Buffers v3

安装用于生成gRPC服务代码的协议编译器,最简单的方法是从下面的链接:https://github.com/google/protobuf/releases下载适合你平台的预编译好的二进制文件(protoc-<version>-<platform>.zip)。

例如,我使用 Intel 芯片的 Mac 系统则下载 protoc-3.20.1-osx-x86_64.zip 文件,解压之后得到如下内容。

其中:

  • bin 目录下的 protoc 是可执行文件。
  • include 目录下的是 google 定义的.proto文件,我们import "google/protobuf/timestamp.proto"就是从此处导入。

我们需要将下载得到的可执行文件protoc所在的 bin 目录加到我们电脑的环境变量中。

安装插件

因为本文我们是使用Go语言做开发,接下来执行下面的命令安装protoc的Go插件:

安装go语言插件:

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28

该插件会根据.proto文件生成一个后缀为.pb.go的文件,包含所有.proto文件中定义的类型及其序列化方法。

安装grpc插件:

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

该插件会生成一个后缀为_grpc.pb.go的文件,其中包含:

  • 一种接口类型(或存根) ,供客户端调用的服务方法。
  • 服务器要实现的接口类型。

上述命令会默认将插件安装到$GOPATH/bin,为了protoc编译器能找到这些插件,请确保你的$GOPATH/bin在环境变量中。

protocol-buffers 官方Go教程

检查

依次执行以下命令检查一下是否开发环境都准备完毕。

  1. 确认 protoc 安装完成。

    ❯ protoc --version
    libprotoc 3.20.1
    
  2. 确认 protoc-gen-go 安装完成。

    ❯ protoc-gen-go --version
    protoc-gen-go v1.28.0
    

    如果这里提示protoc-gen-go不是可执行的程序,请确保你的 GOPATH 下的 bin 目录在你电脑的环境变量中。

  3. 确认 protoc-gen-go-grpc 安装完成。

    ❯ protoc-gen-go-grpc --version
    protoc-gen-go-grpc 1.2.0
    

    如果这里提示protoc-gen-go-grpc不是可执行的程序,请确保你的 GOPATH 下的 bin 目录在你电脑的环境变量中。

gRPC的开发方式

把大象放进冰箱分几步?

  1. 把冰箱门打开。
  2. 把大象放进去。
  3. 把冰箱门带上。

gRPC开发同样分三步:

编写.proto文件定义服务

像许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。默认情况下,gRPC 使用 protocol buffers作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。可以根据需要使用其他的IDL代替。

例如,下面使用 protocol buffers 定义了一个HelloService服务。

service HelloService 
  rpc SayHello (HelloRequest) returns (HelloResponse);


message HelloRequest 
  string greeting = 1;


message HelloResponse 
  string reply = 1;

在gRPC中你可以定义四种类型的服务方法。

  • 普通 rpc,客户端向服务器发送一个请求,然后得到一个响应,就像普通的函数调用一样。
  rpc SayHello(HelloRequest) returns (HelloResponse);
  • 服务器流式 rpc,其中客户端向服务器发送请求,并获得一个流来读取一系列消息。客户端从返回的流中读取,直到没有更多的消息。gRPC 保证在单个 RPC 调用中的消息是有序的。
  rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
  • 客户端流式 rpc,其中客户端写入一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端完成了消息的写入,它就等待服务器读取消息并返回响应。同样,gRPC 保证在单个 RPC 调用中对消息进行排序。
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  • 双向流式 rpc,其中双方使用读写流发送一系列消息。这两个流独立运行,因此客户端和服务器可以按照自己喜欢的顺序读写: 例如,服务器可以等待接收所有客户端消息后再写响应,或者可以交替读取消息然后写入消息,或者其他读写组合。每个流中的消息是有序的。

生成指定语言的代码

.proto 文件中的定义好服务之后,gRPC 提供了生成客户端和服务器端代码的 protocol buffers 编译器插件。

我们使用这些插件可以根据需要生成JavaGoC++Python等语言的代码。我们通常会在客户端调用这些 API,并在服务器端实现相应的 API。

  • 在服务器端,服务器实现服务声明的方法,并运行一个 gRPC 服务器来处理客户端发来的调用请求。gRPC 底层会对传入的请求进行解码,执行被调用的服务方法,并对服务响应进行编码。
  • 在客户端,客户端有一个称为存根(stub)的本地对象,它实现了与服务相同的方法。然后,客户端可以在本地对象上调用这些方法,将调用的参数包装在适当的 protocol buffers 消息类型中—— gRPC 在向服务器发送请求并返回服务器的 protocol buffers 响应之后进行处理。

编写业务逻辑代码

gRPC 帮我们解决了 RPC 中的服务调用、数据传输以及消息编解码,我们剩下的工作就是要编写业务逻辑代码。

在服务端编写业务代码实现具体的服务方法,在客户端按需调用这些方法。

gRPC入门示例

编写proto代码

Protocol Buffers是一种与语言无关,平台无关的可扩展机制,用于序列化结构化数据。使用Protocol Buffers可以一次定义结构化的数据,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。

关于Protocol Buffers的教程可以查看Protocol Buffers V3中文指南,本文后续内容默认读者熟悉Protocol Buffers

syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本

option go_package = "xx";  // 指定生成的Go代码在你项目中的导入路径

package pb; // 包名


// 定义服务
service Greeter 
    // SayHello 方法
    rpc SayHello (HelloRequest) returns (HelloResponse) 


// 请求消息
message HelloRequest 
    string name = 1;


// 响应消息
message HelloResponse 
    string reply = 1;

编写Server端Go代码

我们新建一个hello_server项目,在项目根目录下执行go mod init hello_server

再新建一个pb文件夹,将上面的 proto 文件保存为hello.proto,将go_package按如下方式修改。

// ...

option go_package = "hello_server/pb";

// ...

此时,项目的目录结构为:

hello_server
├── go.mod
├── go.sum
├── main.go
└── pb
    └── hello.proto

在项目根目录下执行以下命令,根据hello.proto生成 go 源码文件。

protoc --go_out=. --go_opt=paths=source_relative \\
--go-grpc_out=. --go-grpc_opt=paths=source_relative \\
pb/hello.proto

注意 如果你的终端不支持\\符(例如某些同学的Windows),那么你就复制粘贴下面不带\\的命令执行。

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

生成后的go源码文件会保存在pb文件夹下。

hello_server
├── go.mod
├── go.sum
├── main.go
└── pb
    ├── hello.pb.go
    ├── hello.proto
    └── hello_grpc.pb.go

将下面的内容添加到hello_server/main.go中。

package main

import (
	"context"
	"fmt"
	"hello_server/pb"
	"net"

	"google.golang.org/grpc"
)

// hello server

type server struct 
	pb.UnimplementedGreeterServer


func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) 
	return &pb.HelloResponseReply: "Hello " + in.Name, nil


func main() 
	// 监听本地的8972端口
	lis, err := net.Listen("tcp", ":8972")
	if err != nil 
		fmt.Printf("failed to listen: %v", err)
		return
	
	s := grpc.NewServer()                  // 创建gRPC服务器
	pb.RegisterGreeterServer(s, &server) // 在gRPC服务端注册服务
	// 启动服务
	err = s.Serve(lis)
	if err != nil 
		fmt.Printf("failed to serve: %v", err)
		return
	

编译并执行 http_server

go build
./server

编写Client端Go代码

我们新建一个hello_client项目,在项目根目录下执行go mod init hello_client

再新建一个pb文件夹,将上面的 proto 文件保存为hello.proto,将go_package按如下方式修改。

// ...

option go_package = "hello_client/pb";

// ...

在项目根目录下执行以下命令,根据hello.protohttp_client项目下生成 go 源码文件。

protoc --go_out=. --go_opt=paths=source_relative \\
--go-grpc_out=. --go-grpc_opt=paths=source_relative \\
pb/hello.proto

注意 如果你的终端不支持\\符(例如某些同学的Windows),那么你就复制粘贴下面不带\\的命令执行。

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

此时,项目的目录结构为:

http_client
├── go.mod
├── go.sum
├── main.go
└── pb
    ├── hello.pb.go
    ├── hello.proto
    └── hello_grpc.pb.go

http_client/main.go文件中按下面的代码调用http_server提供的 SayHello RPC服务。

package main

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

	"hello_client/pb"

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

// hello_client

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() 
	flag.Parse()
	// 连接到server端,此处禁用安全传输
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil 
		log.Fatalf("did not connect: %v", err)
	
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// 执行RPC调用并打印收到的响应数据
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequestName: *name)
	if err != nil 
		log.Fatalf("could not greet: %v", err)
	
	log.Printf("Greeting: %s", r.GetReply())

保存后将http_client编译并执行:

go build
./hello_client -name=七米

得到以下输出结果,说明RPC调用成功。

2022/05/15 00:31:52 Greeting: Hello 七米

gRPC跨语言调用

接下来,我们演示一下如何使用gRPC实现跨语言的RPC调用。

我们使用Python语言编写Client,然后向上面使用go语言编写的server发送RPC请求。

python下安装 grpc:

python -m pip install grpcio

安装gRPC tools:

python -m pip install grpcio-tools

生成Python代码

新建一个py_client目录,将hello.proto文件保存到py_client/pb/目录下。 在py_client目录下执行以下命令,生成python源码文件。

cd py_cleint
python3 -m grpc_tools.protoc -Ipb --python_out=. --grpc_python_out=. pb/hello.proto

编写Python版RPC客户端

将下面的代码保存到py_client/client.py文件中。

from __future__ import print_function

import logging

import grpc
import hello_pb2
import hello_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.
    with grpc.insecure_channel('127.0.0.1:8972') as channel:
        stub = hello_pb2_grpc.GreeterStub(channel)
        resp = stub.SayHello(hello_pb2.HelloRequest(name='q1mi'))
    print("Greeter client received: " + resp.reply)


if __name__ == '__main__':
    logging.basicConfig()
    run()

此时项目的目录结构图如下:

py_client
├── client.py
├── hello_pb2.py
├── hello_pb2_grpc.py
└── pb
    └── hello.proto

Python RPC 调用

执行client.py调用go语言的SayHelloRPC服务。

❯ python3 client.py
Greeter client received: Hello q1mi

这里我们就实现了,使用 Python 代码编写的client去调用Go语言版本的server了。

点击右边的链接查看完整代码:gRPC_demo完整代码

gRPC流式示例

在上面的示例中,客户端发起了一个RPC请求到服务端,服务端进行业务处理并返回响应给客户端,这是gRPC最基本的一种工作方式(Unary RPC)。除此之外,依托于HTTP2,gRPC还支持流式RPC(Streaming RPC)。

服务端流式RPC

客户端发出一个RPC请求,服务端与客户端之间建立一个单向的流,服务端可以向流中写入多个响应消息,最后主动关闭流;而客户端需要监听这个流,不断获取响应直到流关闭。应用场景举例:客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。

我们在此编写一个使用多种语言打招呼的方法,客户端发来一个用户名,服务端分多次返回打招呼的信息。

1.定义服务

// 服务端返回流式数据
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

修改.proto文件后,需要重新使用 protocol buffers编译器生成客户端和服务端代码。

2.服务端需要实现 LotsOfReplies 方法。

// LotsOfReplies 返回使用多种语言打招呼
func (s *server) LotsOfReplies(in *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error 
	words := []string
		"你好",
		"hello",
		"こんにちは",
		"안녕하세요",
	

	for _, word := range words 
		data := &pb.HelloResponse
			Reply: word + in.GetName(),
		
		// 使用Send方法返回多个数据
		if err := stream.Send(data); err != nil 
			return err
		
	
	return nil

3.客户端调用LotsOfReplies 并将收到的数据依次打印出来。

func runLotsOfReplies(c pb.GreeterClient) 
	// server端流式RPC
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	stream, err := c.LotsOfReplies(ctx, &pb.HelloRequestName: *name)
	if err != nil 
		log.Fatalf("c.LotsOfReplies failed, err: %v", err)
	
	for 
		// 接收服务端返回的流式数据,当收到io.EOF或错误时退出
		res, err := stream.Recv()
		if err == io.EOF 
			break
		
		if err != nil 
			log.Fatalf("c.LotsOfReplies failed, err: %v", err)
		
		log.Printf("got reply: %q\\n", res.GetReply())
	

执行程序后会得到如下输出结果。

2022/05/21 14:36:20 got reply: "你好七米"
2022/05/21 14:36:20 got reply: "hello七米"
2022/05/21 14:36:20 got reply: "こんにちは七米"
2022/05/21 14:36:20 got reply: "안녕하세요七米"

客户端流式RPC

客户端传入多个请求对象,服务端返回一个响应结果。典型的应用场景举例:物联网终端向服务器上报数据、大数据流式计算等。

在这个示例中,我们编写一个多次发送人名,服务端统一返回一个打招呼消息的程序。

1.定义服务

// 客户端发送流式数据
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

修改.proto文件后,需要重新使用 protocol buffers编译器生成客户端和服务端代码。

2.服务端实现LotsOfGreetings方法。

// LotsOfGreetings 接收流式数据
func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error 
	reply := "你好:"
	for 
		// 接收客户端发来的流式数据
		res, err := stream.Recv()
		if err == io.EOF 
			// 最终统一回复
			return stream.SendAndClose(&pb.HelloResponse
				Reply: reply,
			)
		
		if err != nil 
			return err
		
		reply += res.GetName()
	
  

3.客户端调用LotsOfGreetings方法,向服务端发送流式请求数据,接收返回值并打印。

func runLotsOfGreeting(c pb.GreeterClient) 
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	// 客户端流式RPC
	stream, err := c.LotsOfGreetings(ctx)
	if err != nil 
		log.Fatalf("c.LotsOfGreetings failed, err: %v", err)
	
	names := []string"七米", "q1mi", "沙河娜扎"
	for _, name := range names 
		// 发送流式数据
		err := stream.Send(&pb.HelloRequestName: name)
		if err != nil 
			log.Fatalf("c.LotsOfGreetings stream.Send(%v) failed, err: %v", name, err)
		
	
	res, err := stream.CloseAndRecv()
	if err != nil 
		log.Fatalf("c.LotsOfGreetings failed: %v", err)
	
	log.Printf("got reply: %v", res.GetReply())

执行上述函数将得到如下数据结果。

2022/05/21 14:57:31 got reply: 你好:七米q1mi沙河娜扎

双向流式RPC

双向流式RPC即客户端和服务端均为流式的RPC,能发送多个请求对象也能接收到多个响应对象。典型应用示例:聊天应用等。

我们这里还是编写一个客户端和服务端进行人机对话的双向流式RPC示例。

1.定义服务

// 双向流式数据
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

修改.proto文件后,需要重新使用 protocol buffers编译器生成客户端和服务端代码。

2.服务端实现BidiHello方法。

// BidiHello 双向流式打招呼
func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error 
	for 
		// 接收流式请求
		in, err := stream.Recv()
		if err == io.EOF 
			return nil
		
		if err != nil 
			return err
		

		reply := magic(in.GetName()) // 对收到的数据做些处理

		// 返回流式响应
		if err := stream.Send(&pb.HelloResponseReply: reply); err != nil 
			return err
		
	

这里我们还定义了一个处理数据的magic函数,其内容如下。

// magic 一段价值连城的“人工智能”代码
func magic(s string) string 
	s = strings.ReplaceAll(s, "吗", "")
	s = strings.ReplaceAll(s, "吧", "")
	s = strings.ReplaceAll(s, "你", "我")
	s = strings.ReplaceAll(s, "?", "!")
	s = strings.ReplaceAll(s, "?", "!")
	return s

3.客户端调用BidiHello方法,一边从终端获取输入的请求数据发送至服务端,一边从服务端接收流式响应。

func runBidiHello(c pb.GreeterClient) 
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	// 双向流模式
	stream, err := c.BidiHello(ctx)
	if err != nil 
		log.Fatalf("c.BidiHello failed, err: %v", err)
	
	waitc := make(chan struct)
	go func() 
		for 
			// 接收服务端返回的响应
			in, err := stream.Recv()
			if err == io.EOF 
				// read done.
				close(waitc)
				return
			
			if err != nil 
				log.Fatalf("c.BidiHello stream.Recv() failed, err: %v", err)
			
			fmt.Printf("AI:%s\\n", in.GetReply())
		
	()
	// 从标准输入获取用户输入
	reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
	for 
		cmd, _ := reader.ReadString('\\n') // 读到换行
		cmd = strings.TrimSpace(cmd)
		if len(cmd) == 0 
			continue
		
		if strings.ToUpper(cmd) == "QUIT" 
			break
		
		// 将获取到的数据发送至服务端
		if err := stream.Send(&pb.HelloRequestName: cmd); err != nil 
			log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)
		
	
	stream.CloseSend()
	<-waitc

将服务端和客户端的代码都运行起来,就可以实现简单的对话程序了。

以上是关于go微服务gRPC的主要内容,如果未能解决你的问题,请参考以下文章

清晰架构(Clean Architecture)的Go微服务: 依赖注入(Dependency Injection)

go微服务gRPC

go微服务gRPC

go微服务gRPC

Go使用grpc+http打造高性能微服务

利用gRPC,Ballerina和Go建立有效的「微服务」