GRPC入门实战

  • A+
所属分类:Go

相关链接:
gRPC官网:https://grpc.io/

RPC框架一般用于后端服务数据通信比较大很频繁的场景。通信少时RPC和HTTP都可以用,怎么简单怎么来,满足需要就可。GRPC面向移动应用,当然它是通用的,后端也可以用,比较强大,支持双向通信。

先来个简单的rpc示例代码,再来个简单的grpc示例代码。

一、RPC

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许调用不同进程空间的程序。RPC 的客户端和服务器可以在一台机器上,也可以在不同的机器上。程序员使用时,就像调用本地程序一样,无需关注内部的实现细节。

RPC 即远程过程调用,很简单的概念,就像调用本地服务(方法)一样调用服务器的服务(方法) 。

  • Go支持RPC

在Go中,标准库提供的net/rpc包实现了RPC协议需要的相关细节,开发者可以很方便的使用该包编写RPC的服务端和客户端程序。net/rpc包允许 RPC客户端 程序通过网络或者其他IO连接调用一个远程对象的公开方法(该方法必须是外部可访问即首字母大写),在RPC服务端,可将一个对象注册为可访问的服务,之后该对象的公开方法就能够以远程的方式提供访问。

1、GO语言实现rpc服务端

package main

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/rpc"
)

func main() {
   
	fmt.Println("rpc服务端启动......")
	arith := new(Arith)
	rpc.Register(arith)
	rpc.HandleHTTP()
	if err := http.ListenAndServe(":1234", nil); err != nil {
   
		log.Fatal("serve error:", err)
	}
}

type Args struct {
   
	A, B int
}
type Quotient struct {
   
	Quo, Rem int
}

type Arith int

func (t *Arith) Multiply(args *Args, reply *int) error {
   
	*reply = args.A * args.B
	return nil
}

func (t *Arith) Divide(args *Args, quo *Quotient) error {
   
	if args.B == 0 {
   
		return errors.New("divide by 0")
	}
	quo.Quo = args.A / args.B
	quo.Rem = args.A % args.B
	return nil
}

2、Go语言实现rpc客户端

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main() {
   
	fmt.Println("rpc客户端启动......")
	client, err := rpc.DialHTTP("tcp", ":1234")
	if err != nil {
   
		log.Fatal("dialing:", err)
	}
	args := &Args{
   7, 8}
	var reply int
	err = client.Call("Arith.Multiply", args, &reply)
	if err != nil {
   
		log.Fatal("Multiply error:", err)
	}
	fmt.Printf("Multiply: %d*%d=%d\n", args.A, args.B, reply)

	args = &Args{
   15, 6}
	var quo Quotient
	err = client.Call("Arith.Divide", args, &quo)
	if err != nil {
   
		log.Fatal("Divide error:", err)
	}
	fmt.Printf("Divide: %d/%d=%d...%d\n", args.A, args.B, quo.Quo, quo.Rem)
}

type Args struct {
   
	A, B int
}

type Quotient struct {
   
	Quo, Rem int
}

二、GRPC

gRPC是谷歌开源的一个高性能、开源、通用的RPC框架。基于HTTP/2协议标准设计开发,默认采用Protocol Buffers数据序列化协议Protocol Buffers基本语法,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库,面向移动和 HTTP2 设计。【基于HTTP2】

在服务端,服务端实现这个接口并运行一个 gRPC 服务器来处理客户端调用。
在客户端,客户端有一个存根(在某些语言中仅称为客户端),它提供与服务器相同的方法。

gRPC 客户端和服务器可以在各种环境中运行和相互通信,并且可以用任何 gRPC 支持的语言编写【主流语言都支持】

GRPC入门实战

1、protobuf

ProtoBuf 【高效的二进制协议】具有强大的IDL(interface description language,接口描述语言)和相关工具集(主要是protoc)。用户写好.proto描述文件后,protoc可以将其编译成众多语言的接口代码。

  • 数据交互格式: protobuf
  • 通信方式: 最底层为TCP或Unix Socket协议,在此之上是HTTP/2协议的实现
  • 核心库: 在HTTP/2协议之上又构建了针对Go语言的gRPC核心库
  • Stub: 应用程序Application 通过 gRPC插件 生产的 Stub代码 和 gRPC核心库 通信,也可以直接和gRPC核心库通信。
    GRPC入门实战

2、gRPC 支持 4 类服务方法

a、简单 RPC(Simple RPC)

客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。即标准RPC通信。

rpc SayHello(HelloRequest) returns (HelloResponse){}

b、服务端数据流式 RPC (Server-side streaming RPC)

客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。典型的例子是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。

rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}

c、客户端数据流式 RPC(Client-side streaming RPC)

客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。典型的例子是物联网终端向服务器报送数据。

rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}

d、双向数据流式 RPC(Bidirectional streaming RPC)

两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。典型的例子就是聊天应用。

rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}

3、准备工作

a、安装go

下载安装包:https://golang.google.cn/dl/

解压安装包到安装目录,配置安装目录到环境变量【这一步如果不会,去百度】

验证:

C:\Users\DeskTop>go version
go version go1.17 windows/amd64

b、安装 protobuf

protobuf 是一个跨平台和跨语言的数据结构存储和传输的便利工具。

官方文档地址:https://developers.google.com/protocol-buffers/
GitHub 地址:https://github.com/protocolbuffers/protobuf
Releases 下载地址:https://github.com/protocolbuffers/protobuf/releases

解压安装包到安装目录,配置安装目录到环境变量【这一步如果不会,去百度】

验证:

C:\Users\DeskTop>protoc --version
libprotoc 3.17.3
# Ensure compiler version is 3+

c、安装 Go protobuf 插件

go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go

d、安装 grpc-go

go get -u google.golang.org/grpc
  • 编写服务端 .proto 文件
  • 生成服务端 .pb.go 文件并同步给客户端
  • 编写服务端提供接口的代码
  • 编写客户端调用接口的代码

目录结构

├─ hello  -- 代码根目录
│  ├─ go_client
│     ├── main.go
│     ├── proto
│         ├── hello
│            ├── hello.pb.go
│  ├─ go_server
│     ├── main.go
│     ├── controller
│         ├── hello_controller
│            ├── hello_server.go
│     ├── proto
│         ├── hello
│            ├── hello.pb.go
│            ├── hello.proto

这样创建目录是为了 go_client 和 go_server 后期可以拆成两个项目。

4、Go 实现 gRPC 的服务端

编写服务端 hello.proto 文件

syntax = "proto3"; // 指定 proto 版本

package hello;     // 指定包名
option go_package = "./;hello";  // 指定生成 .pb.go文件的包名
// 定义 Hello 服务
service Hello {
   

// 定义 SayHello 方法
rpc SayHello(HelloRequest) returns (HelloResponse) {
   }

// 定义 LotsOfReplies 方法
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
   }
}

// HelloRequest 请求结构
message HelloRequest {
   
string name = 1;
}

// HelloResponse 响应结构
message HelloResponse {
   
string message = 1;
}

了解更多 Protobuf 语法,请查看:

https://developers.google.com/protocol-buffers/

生成服务端 .pb.go

protoc -I . --go_out=plugins=grpc:. ./hello.proto
# 该命令在hello.proto文件所在目录下执行,会生成hello.pb.go文件

同时将生成的 hello.pb.go 复制到客户端一份。

查看更多命令参数,执行 protoc,查看 OPTION。

  • 编写服务端提供接口的代码
// hello_server.go
package hello_controller

import (
	"fmt"
	"golang.org/x/net/context"
	"hello/go_server/proto/hello"
)

type HelloController struct{
   }

func (h *HelloController) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloResponse, error) {
   
	return &hello.HelloResponse{
   Message : fmt.Sprintf("%s", in.Name)}, nil
}

func (h *HelloController) LotsOfReplies(in *hello.HelloRequest, stream hello.Hello_LotsOfRepliesServer)  error {
   
	for i := 0; i < 10; i++ {
   
		stream.Send(&hello.HelloResponse{
   Message : fmt.Sprintf("%s %s %d", in.Name, "Reply", i)})
	}
	return nil
}
  • 服务端启动函数
// main.go
package main

import (
	"log"
	"net"
	"hello/go_server/proto/hello"
	"hello/go_server/controller/hello_controller"
	"google.golang.org/grpc"
)

const (
	Address = "0.0.0.0:9090"
)

func main() {
   
	listen, err := net.Listen("tcp", Address)
	if err != nil {
   
		log.Fatalf("Failed to listen: %v", err)
	}

	s := grpc.NewServer()

	// 服务注册
	hello.RegisterHelloServer(s, &hello_controller.HelloController{
   })

	log.Println("Listen on " + Address)

	if err := s.Serve(listen); err != nil {
   
		log.Fatalf("Failed to serve: %v", err)
	}
}

运行:

go run main.go

2019/07/28 17:51:20 Listen on 0.0.0.0:9090

5、Go 实现 gRPC 的客户端

  • 编写客户端请求接口的代码
package main

import (
	"hello/go_client/proto/hello"
	"io"
	"log"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

const (
	// gRPC 服务地址
	Address = "0.0.0.0:9090"
)

func main() {
   
	conn, err := grpc.Dial(Address, grpc.WithInsecure())
	if err != nil {
   
		log.Fatalln(err)
	}
	defer conn.Close()

	// 初始化客户端
	c := hello.NewHelloClient(conn)

	// 调用 SayHello 方法
	res, err := c.SayHello(context.Background(), &hello.HelloRequest{
   Name: "Hello World"})

	if err != nil {
   
		log.Fatalln(err)
	}

	log.Println(res.Message)

	// 调用 LotsOfReplies 方法
	stream, err := c.LotsOfReplies(context.Background(),&hello.HelloRequest{
   Name: "Hello World"})
	if err != nil {
   
		log.Fatalln(err)
	}

	for {
   
		res, err := stream.Recv()
		if err == io.EOF {
   
			break
		}

		if err != nil {
   
			log.Printf("stream.Recv: %v", err)
		}

		log.Printf("%s", res.Message)
	}
}

运行:

go run main.go

2019/07/28 17:58:13 Hello World
2019/07/28 17:58:13 Hello World Reply 0
2019/07/28 17:58:13 Hello World Reply 1
2019/07/28 17:58:13 Hello World Reply 2
2019/07/28 17:58:13 Hello World Reply 3
2019/07/28 17:58:13 Hello World Reply 4
2019/07/28 17:58:13 Hello World Reply 5
2019/07/28 17:58:13 Hello World Reply 6
2019/07/28 17:58:13 Hello World Reply 7
2019/07/28 17:58:13 Hello World Reply 8
2019/07/28 17:58:13 Hello World Reply 9
w3cjava