go语言rpc,grpc介绍

目录

  • rpc
    • RPC调用
    • net/rpc
      • RPC over HTTP 和 RESTful
        • server
        • client
      • RPC over TCP 和 RESTful
        • server
        • client
    • 序列化/反序列化协议
      • json序列化
        • server
        • client
    • python调用rpc
    • RPC原理
  • rpc框架比较
  • grpc
    • why gRpc
    • gRPC与Protobuf介绍
    • 安装gRPC和Protobuf
      • 检查
    • gRPC的开发方式
      • 编写proto代码
      • 编写Server端Go代码
      • 编写Client端Go代码
  • gRPC跨语言调用
    • python
    • 生成Python代码
  • 流式RPC
    • 单向流式server
    • 单向流式client
    • 双向流式
    • metadata
      • 元数据中存储二进制数据
      • 从请求上下文中获取元数据
      • 发送和接收元数据-客户端
        • 发送metadata
        • 接收metadata
        • 普通调用
        • 流式调用
      • 发送和接收元数据-服务器端
        • 接收metadata
        • 发送metadata
          • 普通调用
          • 流式调用
        • 普通RPC调用metadata示例
          • client端的metadata操作
          • server端metadata操作
      • 流式RPC调用metadata示例
        • client端的metadata操作
        • server端的metadata操作
    • 错误处理
      • gRPC code
      • gRPC Status
      • 创建错误
      • 为错误添加其他详细信息
      • 代码示例
        • 服务端
        • 客户端
    • 加密或认证
      • 无加密认证
      • 使用服务器身份验证 SSL/TLS
        • 生成证书
          • 生成私钥
          • 生成自签名的证书
        • 建立安全连接
        • 案例
          • server
          • client
      • 自定义token校验
    • 拦截器(中间件)
      • 客户端端拦截器
        • 普通拦截器/一元拦截器
        • 流拦截器
      • server端拦截器
        • 普通拦截器/一元拦截器
        • 流拦截器
      • 拦截器示例
        • 客户端拦截器定义
          • 一元拦截器
          • 流式拦截器
        • 服务端拦截器定义
          • 一元拦截器
          • 流拦截器
        • 注册拦截器
          • 客户端注册拦截器
          • 服务端注册拦截器
        • go-grpc-middleware
  • Q&A
    • 对于go grpc不需要注册中心也可以使用;那么grpc是如何实现服务注册功能的?
    • etcd和k8s比较,哪个做注册中心好,差异是什么,优劣是什么?

rpc

  1. 远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。
    该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。

  2. 它允许像调用本地服务一样调用远程服务。

  3. RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。

go语言rpc,grpc介绍_第1张图片
4. 从上图可以看出, RPC 本身是 client-server模型,也是一种 request-response 协议。

RPC调用

  1. 使用 RPC 的目的是让我们调用远程方法像调用本地方法一样无差别。并且RESTful API通常是基于HTTP协议,传输数据采用JSON等文本协议,相较于RPC 直接使用TCP协议,传输数据多采用二进制协议来说,RPC通常相比RESTful API性能会更好。

  2. RESTful API多用于前后端之间的数据传输,而目前微服务架构下各个微服务之间多采用RPC调用。

net/rpc

  1. RPC 的消息传输可以通过 TCP、UDP 或者 HTTP等,所以有时候我们称之为 RPC over TCP、 RPC over HTTP(RPC 通过HTTP 传输消息的时候和 RESTful的架构是类似的,但是也有不同)。

  2. Go语言的 rpc 包提供对通过网络或其他 i/o 连接导出的对象方法的访问,服务器注册一个对象,并把它作为服务对外可见(服务名称就是类型名称)。注册后,对象的导出方法将支持远程访问。服务器可以注册不同类型的多个对象(服务) ,但是不支持注册同一类型的多个对象。

RPC over HTTP 和 RESTful

  1. RPC 的客户端和服务器端是紧耦合的,客户端需要知道调用的过程的名字,过程的参数以及它们的类型、顺序等。一旦服务器更改了过程的实现,客户端的实现很容易出问题。RESTful基于 http的语义操作资源,参数的顺序一般没有关系,也很容易的通过代理转换链接和资源位置,从这一点上来说,RESTful 更灵活。

  2. 它们操作的对象不一样。 RPC 操作的是方法和过程,它要操作的是方法对象。RESTful 操作的是资源(resource),而不是方法。

  3. RESTful执行的是对资源的操作,增加、查找、修改和删除等,主要是CURD,所以如果要实现一个特定目的的操作,比如为名字姓张的学生的数学成绩都加上10这样的操作,RESTful的API设计起来就不是那么直观或者有意义。在这种情况下, RPC的实现更有意义,它可以实现一个Student.Increment(Name, Score) 的方法供客户端调用。

go语言rpc,grpc介绍_第2张图片

server

package main

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

type Args struct {
	X, Y int
}

// ServiceA 自定义一个结构体类型
type ServiceA struct{}

// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
	*reply = args.X + args.Y
	return nil
}

func main() {
	service := new(ServiceA)
	rpc.Register(service) // 注册RPC服务
	rpc.HandleHTTP()      // 基于HTTP协议
	l, e := net.Listen("tcp", ":9091")
	if e != nil {
		log.Fatal("listen error:", e)
	}
	http.Serve(l, nil)
}

client

package main

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

func main() {
	// 建立HTTP连接
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	// 同步调用
	args := &Args{10, 20}
	var reply int
	err = client.Call("ServiceA.Add", args, &reply)
	if err != nil {
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

	// 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}

go语言rpc,grpc介绍_第3张图片

RPC over TCP 和 RESTful

  1. 如果直接使用socket实现 RPC,除了上面的不同外,可以获得性能上的优势。

  2. RPC over TCP可以通过长连接减少连接的建立所产生的花费,在调用次数非常巨大的时候(这是目前互联网公司经常遇到的情况,大并发的情况下),这个花费影响是非常巨大的。 RESTful 也可以通过 keep-alive 实现长连接, 但是它最大的一个问题是它的request-response模型是阻塞的 (http1.0和 http1.1, http 2.0没这个问题),发送一个请求后只有等到response返回才能发送第二个请求 (有些http server实现了pipeling的功能,但不是标配), RPC的实现没有这个限制。

server

package main

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

func main() {
	service := new(ServiceA)
	rpc.Register(service) // 注册RPC服务
	l, e := net.Listen("tcp", ":9091")
	if e != nil {
		log.Fatal("listen error:", e)
	}
	for {
		conn, _ := l.Accept()
		rpc.ServeConn(conn)
	}
}

client

package main

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

func main() {
	// 建立TCP连接
	client, err := rpc.Dial("tcp", "127.0.0.1:9091")
	if err != nil {
		log.Fatal("dialing:", err)
	}

	// 同步调用
	args := &Args{10, 20}
	var reply int
	err = client.Call("ServiceA.Add", args, &reply)
	if err != nil {
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

	// 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}

序列化/反序列化协议

rpc 包默认使用的是 gob 协议对传输数据进行序列化/反序列化,比较有局限性。

json序列化

server

package main

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

func main() {
	service := new(ServiceA)
	rpc.Register(service) // 注册RPC服务
	l, e := net.Listen("tcp", ":9091")
	if e != nil {
		log.Fatal("listen error:", e)
	}
	for {
		conn, _ := l.Accept()
		// 使用JSON协议
		rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

client

package main

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

func main() {
	// 建立TCP连接
	conn, err := net.Dial("tcp", "127.0.0.1:9091")
	if err != nil {
		log.Fatal("dialing:", err)
	}
	// 使用JSON协议
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	// 同步调用
	args := &Args{10, 20}
	var reply int
	err = client.Call("ServiceA.Add", args, &reply)
	if err != nil {
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

	// 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}

python调用rpc

import socket
import json

request = {
    "id": 0,
    "params": [{"x":10, "y":20}],  # 参数要对应上Args结构体
    "method": "ServiceA.Add"
}

client = socket.create_connection(("127.0.0.1", 9091),5)
client.sendall(json.dumps(request).encode())

rsp = client.recv(1024)
rsp = json.loads(rsp.decode())
print(rsp)

RPC原理

RPC 让远程调用就像本地调用一样,其调用过程可拆解为以下步骤:
go语言rpc,grpc介绍_第4张图片
① 服务调用方(client)以本地调用方式调用服务;

② client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;

③ client stub找到服务地址,并将消息发送到服务端;

④ server 端接收到消息;

⑤ server stub收到消息后进行解码;

⑥ server stub根据解码结果调用本地的服务;

⑦ 本地服务执行并将结果返回给server stub;

⑧ server stub将返回结果打包成能够进行网络传输的消息体;

⑨ 按地址将消息发送至调用方;

⑩ client 端接收到消息;

⑪ client stub收到消息并进行解码;

⑫ 调用方得到最终结果。

使用RPC框架的目标是只需要关心第1步和最后1步,中间的其他步骤统统封装起来,让使用者无需关心。各式RPC框架(grpc、thrift等)就是为了让RPC调用更方便。

rpc框架比较

  1. Dubbo 是阿里巴巴公司开源的一个Java高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。不过,略有遗憾的是,据说在淘宝内部,dubbo由于跟淘宝另一个类似的框架HSF(非开源)有竞争关系,导致dubbo团队已经解散。

  2. Motan是新浪微博开源的一个Java 框架。它诞生的比较晚,起于2013年,2016年5月开源。Motan 在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。

  3. rpcx是Go语言生态圈的Dubbo, 比Dubbo更轻量,实现了Dubbo的许多特性,借助于Go语言优秀的并发特性和简洁语法,可以使用较少的代码实现分布式的RPC服务。

  4. gRPC是Google开发的高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。本身它不是分布式的,所以要实现上面的框架的功能需要进一步的开发。

  5. thrift是Apache的一个跨语言的高性能的服务框架,也得到了广泛的应用。

  Dubbo Montan rpcx gRPC Thrift
开发语言 Java Java Go 跨语言 跨语言
分布式(服务治理) × ×
多序列化框架支持
 
√ 
(当前支持Hessian2、Json,可扩展)

 
× 
(只支持protobuf)
× (thrift格式)
多种注册中心 × ×
管理中心 × ×
跨编程语言 × × (支持php client和C server) ×

grpc

  1. gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统。

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

why gRpc

  1. 使用gRPC, 可以一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端。
  2. 使用protocol buffers还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。

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

gRPC与Protobuf介绍

  1. 微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题。
  2. gRPC可以实现微服务,将大的项目拆分为多个小且独立的业务模块,也就是服务,各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers,这是
    google开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如JSON)
  3. 可以用proto files创建gRPC服务,用message类型来定义方法参数和返回类型。

go语言rpc,grpc介绍_第5张图片

安装gRPC和Protobuf

  1. 安装grpc: go get google.golang.org/grpc

  2. 安装Protocol Buffers v3:下载适合平台的预编译好的二进制文件,将下载得到的可执行文件protoc所在的 bin 目录加到电脑的环境变量中。

    1. 适用Windows 64位protoc-3.20.1-win64.zip
    2. 适用于Mac Intel 64位protoc-3.20.1-osx-x86_64.zip
    3. 适用于Mac ARM 64位protoc-3.20.1-osx-aarch_64.zip
    4. 适用于Linux 64位protoc-3.20.1-linux-x86_64.zip
  3. 安装go语言protobuf插件:go install google.golang.org/protobuf/cmd/[email protected]该插件会根据.proto文件生成一个后缀为.pb.go的文件,包含所有.proto文件中定义的类型及其序列化方法。

  4. 安装grpc插件:go install google.golang.org/grpc/cmd/[email protected],该插件会生成一个后缀为_grpc.pb.go的文件,其中包含:一种接口类型(或存根) ,供客户端调用的服务方法,服务器要实现的接口类型。

检查

❯ protoc --version
libprotoc 3.20.1

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

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

gRPC的开发方式

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

编写proto代码

Protocol Buffers是一种与语言无关,平台无关的可扩展机制,用于序列化结构化数据。使用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代码

修改proto文件

// ...

option go_package = "hello_server/pb";

// ...

目录结构:

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

生成pb文件:

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

目录结构:

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

server:

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.HelloResponse{Reply: "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
	}
}

编写Client端Go代码

修改proto文件:

// ...

option go_package = "hello_client/pb";

// ...

生成pb文件:

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

client:

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.HelloRequest{Name: *name})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetReply())
}

gRPC跨语言调用

python

# grpc
python -m pip install grpcio
# tool
python -m pip install grpcio-tools

生成Python代码

  1. 新建一个py_client目录,将hello.proto文件保存到py_client/pb/目录下。
  2. 在py_client目录下执行以下命令,生成python源码文件。
python3 -m grpc_tools.protoc -Ipb --python_out=. --grpc_python_out=. pb/hello.proto
py_client
├── client.py
├── hello_pb2.py
├── hello_pb2_grpc.py
└── pb
    └── hello.proto

-----------------------------------------
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()

流式RPC

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

单向流式server

客户端发出一个RPC请求,服务端与客户端之间建立一个单向的流,服务端可以向流中写入多个响应消息,最后主动关闭流;而客户端需要监听这个流,不断获取响应直到流关闭。

  1. 定义服务(修改.proto文件后,需要重新使用 protocol buffers编译器生成客户端和服务端代码。)
// 服务端返回流式数据
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
  1. 实现 LotsOfReplies 方法。
// LotsOfReplies 返回使用多种语言打招呼
func (s *server) LotsOfReplies(in *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error {
	words := []string{
		"你好",
		"hello",
		"fuck you",
		"안녕하세요",
	}

	for _, word := range words {
		data := &pb.HelloResponse{
			Reply: word + in.GetName(),
		}
		// 使用Send方法返回多个数据
		if err := stream.Send(data); err != nil {
			return err
		}
	}
	return nil
}
  1. 客户端调用LotsOfReplies 并将收到的数据依次打印出来
func runLotsOfReplies(c pb.GreeterClient) {
	// server端流式RPC
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{Name: *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())
	}
}

单向流式client

  1. 定义服务
// 客户端发送流式数据
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  1. 服务端实现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()
	}
}  
  1. 客户端调用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{"", "i", ""}
	for _, name := range names {
		// 发送流式数据
		err := stream.Send(&pb.HelloRequest{Name: 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())
}

双向流式

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

  1. 定义服务
// 双向流式数据
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
  1. 服务端实现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.HelloResponse{Reply: reply}); err != nil {
			return err
		}
	}
}
  1. 客户端调用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.HelloRequest{Name: cmd}); err != nil {
			log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)
		}
	}
	stream.CloseSend()
	<-waitc
}

metadata

元数据(metadata)是指在处理RPC请求和响应过程中需要但又不属于具体业务(例如身份验证详细信息)的信息,采用键值对列表的形式,其中键是string类型,值通常是[]string类型,但也可以是二进制数据。gRPC中的 metadata 类似于我们在 HTTP headers中的键值对,元数据可以包含认证token、请求标识和监控标签等。

metadata中的键是大小写不敏感的,由字母、数字和特殊字符-、_、.组成并且不能以grpc-开头(gRPC保留自用),二进制值的键名必须以-bin结尾。

元数据对 gRPC 本身是不可见的,我们通常是在应用程序代码或中间件中处理元数据,我们不需要在.proto文件中指定元数据。

如何访问元数据取决于具体使用的编程语言。 在Go语言中我们是用google.golang.org/grpc/metadata这个库来操作metadata。

metadata 类型定义如下:

type MD map[string][]string

元数据可以像普通map一样读取。注意,这个 map 的值类型是[]string,因此用户可以使用一个键附加多个值。

创建新的metadata:

md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
或,具有相同键的值将合并到一个列表中:
md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1"的值将会是 []string{"val1", "val1-2"}
    "key2", "val2",
)

元数据中存储二进制数据

在元数据中,键始终是字符串。但是值可以是字符串或二进制数据。要在元数据中存储二进制数据值,只需在密钥中添加“-bin”后缀。在创建元数据时,将对带有“-bin”后缀键的值进行编码:

md := metadata.Pairs(
    "key", "string value",
    "key-bin", string([]byte{96, 102}), // 二进制数据在发送前会进行(base64) 编码
                                        // 收到后会进行解码
)

从请求上下文中获取元数据

可以使用 FromIncomingContext 可以从RPC请求的上下文中获取元数据:

func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

发送和接收元数据-客户端

发送metadata

有两种方法可以将元数据发送到服务端。推荐的方法是使用 AppendToOutgoingContext 将 kv 对附加到context。无论context中是否已经有元数据都可以使用这个方法。如果先前没有元数据,则添加元数据; 如果context中已经存在元数据,则将 kv 对合并进去。

// 创建带有metadata的context
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")

// 添加一些 metadata 到 context (e.g. in an interceptor)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")

// 发起普通RPC请求
response, err := client.SomeRPC(ctx, someRequest)

// 或者发起流式RPC请求
stream, err := client.SomeStreamingRPC(ctx)

或者,可以使用 NewOutgoingContext 将元数据附加到context。但是,这将替换context中的任何已有的元数据,因此必须注意保留现有元数据(如果需要的话)。这个方法比使用 AppendToOutgoingContext 要慢。这方面的一个例子如下:

// 创建带有metadata的context
md := metadata.Pairs("k1", "v1", "k1", "v2", "k2", "v3")
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 添加一些metadata到context (e.g. in an interceptor)
send, _ := metadata.FromOutgoingContext(ctx)
newMD := metadata.Pairs("k3", "v3")
ctx = metadata.NewOutgoingContext(ctx, metadata.Join(send, newMD))

// 发起普通RPC请求
response, err := client.SomeRPC(ctx, someRequest)

// 或者发起流式RPC请求
stream, err := client.SomeStreamingRPC(ctx)

接收metadata

客户端可以接收的元数据包括header和trailer。

trailer可以用于服务器希望在处理请求后给客户端发送任何内容,例如在流式RPC中只有等所有结果都流到客户端后才能计算出负载信息,这时候就不能使用headers(header在数据之前,trailer在数据之后)

http trailer.

普通调用

可以使用 CallOption 中的 Header 和 Trailer 函数来获取普通RPC调用发送的header和trailer:

var header, trailer metadata.MD // 声明存储header和trailer的变量
r, err := client.SomeRPC(
    ctx,
    someRequest,
    grpc.Header(&header),    // 将会接收header
    grpc.Trailer(&trailer),  // 将会接收trailer
)

// do something with header and trailer

流式调用

流式调用包括:

  1. 客户端流式
  2. 服务端流式
  3. 双向流式

使用接口 ClientStream 中的 Header 和 Trailer 函数,可以从返回的流中接收 Header 和 Trailer:

stream, err := client.SomeStreamingRPC(ctx)

// 接收 header
header, err := stream.Header()

// 接收 trailer
trailer := stream.Trailer()

发送和接收元数据-服务器端

接收metadata

要读取客户端发送的元数据,服务器需要从 RPC 上下文检索它。如果是普通RPC调用,则可以使用 RPC 处理程序的上下文。对于流调用,服务器需要从流中获取上下文。

普通调用

func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

流式调用

func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    md, ok := metadata.FromIncomingContext(stream.Context()) // get context from stream
    // do something with metadata
}

发送metadata

普通调用

在普通调用中,服务器可以调用 grpc 模块中的 SendHeader 和 SetTrailer 函数向客户端发送header和trailer。这两个函数将context作为第一个参数。它应该是 RPC 处理程序的上下文或从中派生的上下文:

func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    // 创建和发送 header
    header := metadata.Pairs("header-key", "val")
    grpc.SendHeader(ctx, header)
    // 创建和发送 trailer
    trailer := metadata.Pairs("trailer-key", "val")
    grpc.SetTrailer(ctx, trailer)
}
流式调用

对于流式调用,可以使用接口 ServerStream 中的 SendHeader 和 SetTrailer 函数发送header和trailer:

func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    // 创建和发送 header
    header := metadata.Pairs("header-key", "val")
    stream.SendHeader(header)
    // 创建和发送 trailer
    trailer := metadata.Pairs("trailer-key", "val")
    stream.SetTrailer(trailer)
}

普通RPC调用metadata示例

client端的metadata操作

下面的代码片段演示了client端如何设置和获取metadata。

// unaryCallWithMetadata 普通RPC调用客户端metadata操作
func unaryCallWithMetadata(c pb.GreeterClient, name string) {
	fmt.Println("--- UnarySayHello client---")
	// 创建metadata
	md := metadata.Pairs(
		"token", "app-test-q1mi",
		"request_id", "1234567",
	)
	// 基于metadata创建context.
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	// RPC调用
	var header, trailer metadata.MD
	r, err := c.SayHello(
		ctx,
		&pb.HelloRequest{Name: name},
		grpc.Header(&header),   // 接收服务端发来的header
		grpc.Trailer(&trailer), // 接收服务端发来的trailer
	)
	if err != nil {
		log.Printf("failed to call SayHello: %v", err)
		return
	}
	// 从header中取location
	if t, ok := header["location"]; ok {
		fmt.Printf("location from header:\n")
		for i, e := range t {
			fmt.Printf(" %d. %s\n", i, e)
		}
	} else {
		log.Printf("location expected but doesn't exist in header")
		return
	}
   // 获取响应结果
	fmt.Printf("got response: %s\n", r.Reply)
	// 从trailer中取timestamp
	if t, ok := trailer["timestamp"]; ok {
		fmt.Printf("timestamp from trailer:\n")
		for i, e := range t {
			fmt.Printf(" %d. %s\n", i, e)
		}
	} else {
		log.Printf("timestamp expected but doesn't exist in trailer")
	}
}
server端metadata操作

下面的代码片段演示了server端如何设置和获取metadata。

// UnarySayHello 普通RPC调用服务端metadata操作
func (s *server) UnarySayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	// 通过defer中设置trailer.
	defer func() {
		trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
		grpc.SetTrailer(ctx, trailer)
	}()

	// 从客户端请求上下文中读取metadata.
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Errorf(codes.DataLoss, "UnarySayHello: failed to get metadata")
	}
	if t, ok := md["token"]; ok {
		fmt.Printf("token from metadata:\n")
		if len(t) < 1 || t[0] != "app-test-q1mi" {
			return nil, status.Error(codes.Unauthenticated, "认证失败")
		}
	}

	// 创建和发送header.
	header := metadata.New(map[string]string{"location": "BeiJing"})
	grpc.SendHeader(ctx, header)

	fmt.Printf("request received: %v, say hello...\n", in)

	return &pb.HelloResponse{Reply: in.Name}, nil
}

流式RPC调用metadata示例

这里以双向流式RPC为例演示客户端和服务端如何进行metadata操作。

client端的metadata操作

下面的代码片段演示了client端在服务端流式RPC模式下如何设置和获取metadata。

// bidirectionalWithMetadata 流式RPC调用客户端metadata操作
func bidirectionalWithMetadata(c pb.GreeterClient, name string) {
	// 创建metadata和context.
	md := metadata.Pairs("token", "app-test-q1mi")
	ctx := metadata.NewOutgoingContext(context.Background(), md)

	// 使用带有metadata的context执行RPC调用.
	stream, err := c.BidiHello(ctx)
	if err != nil {
		log.Fatalf("failed to call BidiHello: %v\n", err)
	}

	go func() {
		// 当header到达时读取header.
		header, err := stream.Header()
		if err != nil {
			log.Fatalf("failed to get header from stream: %v", err)
		}
		// 从返回响应的header中读取数据.
		if l, ok := header["location"]; ok {
			fmt.Printf("location from header:\n")
			for i, e := range l {
				fmt.Printf(" %d. %s\n", i, e)
			}
		} else {
			log.Println("location expected but doesn't exist in header")
			return
		}

		// 发送所有的请求数据到server.
		for i := 0; i < 5; i++ {
			if err := stream.Send(&pb.HelloRequest{Name: name}); err != nil {
				log.Fatalf("failed to send streaming: %v\n", err)
			}
		}
		stream.CloseSend()
	}()

	// 读取所有的响应.
	var rpcStatus error
	fmt.Printf("got response:\n")
	for {
		r, err := stream.Recv()
		if err != nil {
			rpcStatus = err
			break
		}
		fmt.Printf(" - %s\n", r.Reply)
	}
	if rpcStatus != io.EOF {
		log.Printf("failed to finish server streaming: %v", rpcStatus)
		return
	}

	// 当RPC结束时读取trailer
	trailer := stream.Trailer()
	// 从返回响应的trailer中读取metadata.
	if t, ok := trailer["timestamp"]; ok {
		fmt.Printf("timestamp from trailer:\n")
		for i, e := range t {
			fmt.Printf(" %d. %s\n", i, e)
		}
	} else {
		log.Printf("timestamp expected but doesn't exist in trailer")
	}
}

server端的metadata操作

下面的代码片段演示了server端在服务端流式RPC模式下设置和操作metadata。

// BidirectionalStreamingSayHello 流式RPC调用客户端metadata操作
func (s *server) BidirectionalStreamingSayHello(stream pb.Greeter_BidiHelloServer) error {
	// 在defer中创建trailer记录函数的返回时间.
	defer func() {
		trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
		stream.SetTrailer(trailer)
	}()

	// 从client读取metadata.
	md, ok := metadata.FromIncomingContext(stream.Context())
	if !ok {
		return status.Errorf(codes.DataLoss, "BidirectionalStreamingSayHello: failed to get metadata")
	}

	if t, ok := md["token"]; ok {
		fmt.Printf("token from metadata:\n")
		for i, e := range t {
			fmt.Printf(" %d. %s\n", i, e)
		}
	}

	// 创建和发送header.
	header := metadata.New(map[string]string{"location": "X2Q"})
	stream.SendHeader(header)

	// 读取请求数据发送响应数据.
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		fmt.Printf("request received %v, sending reply\n", in)
		if err := stream.Send(&pb.HelloResponse{Reply: in.Name}); err != nil {
			return err
		}
	}
}

错误处理

gRPC code

类似于HTTP定义了一套响应状态码,gRPC也定义有一些状态码。Go语言中此状态码由codes定义,本质上是一个uint32。

type Code uint32
使用时需导入google.golang.org/grpc/codes包。

import "google.golang.org/grpc/codes"
目前已经定义的状态码有如下几种。

Code 含义
OK 0 请求成功
Canceled 1 操作已取消
Unknown 2 未知错误。如果从另一个地址空间接收到的状态值属 于在该地址空间中未知的错误空间,则可以返回此错误的示例。 没有返回足够的错误信息的API引发的错误也可能会转换为此错误
InvalidArgument 3 表示客户端指定的参数无效。 请注意,这与 FailedPrecondition 不同。 它表示无论系统状态如何都有问题的参数(例如,格式错误的文件名)。
DeadlineExceeded 4 表示操作在完成之前已过期。对于改变系统状态的操作,即使操作成功完成,也可能会返回此错误。 例如,来自服务器的成功响应可能已延迟足够长的时间以使截止日期到期。
NotFound 5 表示未找到某些请求的实体(例如,文件或目录)。
AlreadyExists 6 创建实体的尝试失败,因为实体已经存在。
PermissionDenied 7 表示调用者没有权限执行指定的操作。 它不能用于拒绝由耗尽某些资源引起的(使用 ResourceExhausted )。 如果无法识别调用者,也不能使用它(使用 Unauthenticated )。
ResourceExhausted 8 表示某些资源已耗尽,可能是每个用户的配额,或者整个文件系统空间不足
FailedPrecondition 9 指示操作被拒绝,因为系统未处于操作执行所需的状态。 例如,要删除的目录可能是非空的,rmdir 操作应用于非目录等。
Aborted 10 表示操作被中止,通常是由于并发问题,如排序器检查失败、事务中止等。
OutOfRange 11 表示尝试超出有效范围的操作。
Unimplemented 12 表示此服务中未实施或不支持/启用操作。
Internal 13 意味着底层系统预期的一些不变量已被破坏。 如果你看到这个错误,则说明问题很严重。
Unavailable 14 表示服务当前不可用。这很可能是暂时的情况,可以通过回退重试来纠正。 请注意,重试非幂等操作并不总是安全的。
DataLoss 15 表示不可恢复的数据丢失或损坏
Unauthenticated 16 表示请求没有用于操作的有效身份验证凭据
_maxCode 17 -

gRPC Status

Go语言使用的gRPC Status 定义在google.golang.org/grpc/status,使用时需导入。

import "google.golang.org/grpc/status"
RPC服务的方法应该返回 nil 或来自status.Status类型的错误。客户端可以直接访问错误。

创建错误

当遇到错误时,gRPC服务的方法函数应该创建一个 status.Status。通常我们会使用 status.New函数并传入适当的status.Code和错误描述来生成一个status.Status。调用status.Err方法便能将一个status.Status转为error类型。也存在一个简单的status.Error方法直接生成error。下面是两种方式的比较。

// 创建status.Status
st := status.New(codes.NotFound, "some description")
err := st.Err()  // 转为error类型

// vs.

err := status.Error(codes.NotFound, "some description")

为错误添加其他详细信息

在某些情况下,可能需要为服务器端的特定错误添加详细信息。status.WithDetails就是为此而存在的,它可以添加任意多个proto.Message,我们可以使用google.golang.org/genproto/googleapis/rpc/errdetails中的定义或自定义的错误详情。

st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
ds, _ := st.WithDetails(
	// proto.Message
)
return nil, ds.Err()

然后,客户端可以通过首先将普通error类型转换回status.Status,然后使用status.Details来读取这些详细信息。

s := status.Convert(err)
for _, d := range s.Details() {
	// ...
}

代码示例

我们现在要为hello服务设置访问限制,每个name只能调用一次SayHello方法,超过此限制就返回一个请求超过限制的错误。

服务端

使用map存储每个name的请求次数,超过1次则返回错误,并且记录错误详情。

package main

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

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// grpc server

type server struct {
	pb.UnimplementedGreeterServer
	mu    sync.Mutex     // count的并发锁
	count map[string]int // 记录每个name的请求次数
}

// SayHello 是我们需要实现的方法
// 这个方法是我们对外提供的服务
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.count[in.Name]++ // 记录用户的请求次数
	// 超过1次就返回错误
	if s.count[in.Name] > 1 {
		st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
		ds, err := st.WithDetails(
			&errdetails.QuotaFailure{
				Violations: []*errdetails.QuotaFailure_Violation{{
					Subject:     fmt.Sprintf("name:%s", in.Name),
					Description: "限制每个name调用一次",
				}},
			},
		)
		if err != nil {
			return nil, st.Err()
		}
		return nil, ds.Err()
	}
	// 正常返回响应
	reply := "hello " + in.GetName()
	return &pb.HelloResponse{Reply: reply}, nil
}

func main() {
	// 启动服务
	l, err := net.Listen("tcp", ":8972")
	if err != nil {
		fmt.Printf("failed to listen, err:%v\n", err)
		return
	}
	s := grpc.NewServer() // 创建grpc服务
	// 注册服务,注意初始化count
	pb.RegisterGreeterServer(s, &server{count: make(map[string]int)})
	// 启动服务
	err = s.Serve(l)
	if err != nil {
		fmt.Printf("failed to serve,err:%v\n", err)
		return
	}
}

客户端

当服务端返回错误时,尝试从错误中获取detail信息。

package main

import (
	"context"
	"flag"
	"fmt"
	"google.golang.org/grpc/status"
	"hello_client/pb"
	"log"
	"time"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// grpc 客户端
// 调用server端的 SayHello 方法

var name = flag.String("name", "七米", "通过-name告诉server你是谁")

func main() {
	flag.Parse() // 解析命令行参数

	// 连接server
	conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("grpc.Dial failed,err:%v", err)
		return
	}
	defer conn.Close()
	// 创建客户端
	c := pb.NewGreeterClient(conn) // 使用生成的Go代码
	// 调用RPC方法
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
	if err != nil {
		s := status.Convert(err)        // 将err转为status
		for _, d := range s.Details() { // 获取details
			switch info := d.(type) {
			case *errdetails.QuotaFailure:
				fmt.Printf("Quota failure: %s\n", info)
			default:
				fmt.Printf("Unexpected type: %s\n", info)
			}
		}
		fmt.Printf("c.SayHello failed, err:%v\n", err)
		return
	}
	// 拿到了RPC响应
	log.Printf("resp:%v\n", resp.GetReply())
}

加密或认证

无加密认证

在上面的示例中,我们都没有为我们的 gRPC 配置加密或认证,属于不安全的连接(insecure connection)。

Client端:

conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewGreeterClient(conn)

Server端:

s := grpc.NewServer()
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)

使用服务器身份验证 SSL/TLS

gRPC 内置支持 SSL/TLS,可以通过 SSL/TLS 证书建立安全连接,对传输的数据进行加密处理。

这里我们演示如何使用自签名证书进行server端加密。

生成证书

OpenSSL官网:
官方下载地址: https://www.openssl.org/source/,
windows:http://slproweb.com/products/Win32OpenSSL.html
go语言rpc,grpc介绍_第6张图片
设置环境变量,例如工具安装在C:\OpenSSL-Win64,则将C:\OpenSSL-Win64\bin;复制到Path中。

生成私钥

执行下面的命令生成私钥文件——server.key。

openssl ecparam -genkey -name secp384r1 -out server.key

这里生成的是ECC私钥,当然你也可以使用RSA。

生成自签名的证书

Go1.15之后x509弃用Common Name改用SANs。

当出现如下错误时,需要提供SANs信息。

transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0

为了在证书中添加SANs信息,我们将下面自定义配置保存到server.cnf文件中。

[ req ]
default_bits       = 4096
default_md		= sha256
distinguished_name = req_distinguished_name
req_extensions     = req_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = BEIJING
localityName                = Locality Name (eg, city)
localityName_default        = BEIJING
organizationName            = Organization Name (eg, company)
organizationName_default    = DEV
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = liwenzhou.com

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = localhost
DNS.2   = liwenzhou.com
IP      = 127.0.0.1

执行下面的命令生成自签名证书——server.crt。

openssl req -nodes -new -x509 -sha256 -days 3650 -config server.cnf -extensions 'req_ext' -key server.key -out server.crt

建立安全连接

Server端使用credentials.NewServerTLSFromFile函数分别加载证书server.cert和秘钥server.key。

creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)

而client端使用上一步生成的证书文件——server.cert建立安全连接。

creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewGreeterClient(conn)
// ...

除了这种自签名证书的方式外,生产环境对外通信时通常需要使用受信任的CA证书。

案例

server
package main
import (
	"fmt"
	"net"
	pb "github.com/jergoo/go-grpc-example/proto/hello"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes" // grpc 响应状态码
	"google.golang.org/grpc/credentials" // grpc认证包
	"google.golang.org/grpc/grpclog"
	"google.golang.org/grpc/metadata" // grpc metadata包
)

const (
	// Address gRPC服务地址
	Address = "127.0.0.1:50052"
)
// 定义helloService并实现约定的接口
type helloService struct{}
// HelloService Hello服务
var HelloService = helloService{}
// SayHello 实现Hello服务接口
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.He
lloResponse, error) {
	resp := new(pb.HelloResponse)
	resp.Message = fmt.Sprintf("Hello %s.", in.Name)
	return resp, nil
}
func main() {
	listen, err := net.Listen("tcp", Address)
	if err != nil {
		grpclog.Fatalf("Failed to listen: %v", err)
	}
	var opts []grpc.ServerOption
	// TLS认证
	creds, err := credentials.NewServerTLSFromFile("../../keys/server.pem",
	"../../keys/server.key")
	if err != nil {
		grpclog.Fatalf("Failed to generate credentials %v", err)
	}
	opts = append(opts, grpc.Creds(creds))
	// 注册interceptor
	opts = append(opts, grpc.UnaryInterceptor(interceptor))
	// 实例化grpc Server
	s := grpc.NewServer(opts...)
	// 注册HelloService
	pb.RegisterHelloServer(s, HelloService)
	grpclog.Println("Listen on " + Address + " with TLS + Token + Interceptor")
	s.Serve(listen)
}

// auth 验证Token
func auth(ctx context.Context) error {
	md, ok := metadata.FromContext(ctx)
	if !ok {
		return grpc.Errorf(codes.Unauthenticated, "无Token认证信息")
	}
	var (
		appid string
		appkey string
	)
	if val, ok := md["appid"]; ok {
		appid = val[0]
	}
	if val, ok := md["appkey"]; ok {
		appkey = val[0]
	}
	if appid != "101010" || appkey != "i am key" {
		return grpc.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s,
		appkey=%s", appid, appkey)
	}
	return nil
}
// interceptor 拦截器
func interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInf
o, handler grpc.UnaryHandler) (interface{}, error) {
	err := auth(ctx)
	if err != nil {
	return nil, err
	}
	// 继续处理请求
	return handler(ctx, req)
}
client
package main
import (
"time"
pb "github.com/jergoo/go-grpc-example/proto/hello" // 引入proto包
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials" // 引入grpc认证包
"google.golang.org/grpc/grpclog"
)
const (
// Address gRPC服务地址
Address = "127.0.0.1:50052"
// OpenTLS 是否开启TLS认证
OpenTLS = true
)
// customCredential 自定义认证
type customCredential struct{}
// GetRequestMetadata 实现自定义认证接口
func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string)
(map[string]string, error) {
return map[string]string{
"appid": "101010",
"appkey": "i am key",
}, nil
}
// RequireTransportSecurity 自定义认证是否开启TLS
func (c customCredential) RequireTransportSecurity() bool {
return OpenTLS
}
func main() {
var err error
var opts []grpc.DialOption
if OpenTLS {
// TLS连接
creds, err := credentials.NewClientTLSFromFile("../../keys/server.pem",
"server name")
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
} else {
opts = append(opts, grpc.WithInsecure())
}
// 指定自定义认证
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
// 指定客户端interceptor
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
conn, err := grpc.Dial(Address, opts...)
if err != nil {
grpclog.Fatalln(err)
}
defer conn.Close()
// 初始化客户端
c := pb.NewHelloClient(conn)
// 调用方法
req := &pb.HelloRequest{Name: "gRPC"}
res, err := c.SayHello(context.Background(), req)
if err != nil {
grpclog.Fatalln(err)
}
grpclog.Println(res.Message)
}
// interceptor 客户端拦截器
func interceptor(ctx context.Context, method string, req, reply interface{}, cc
*grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
grpclog.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, re
q, reply, time.Since(start), err)
return err
}

自定义token校验

  1. 客户端实现接口,GetRequestMetadata可以携带一些头信息,RequireTransportSecurity为true则表示结合ssl/tls校验
    go语言rpc,grpc介绍_第7张图片
  2. 链接到server
    go语言rpc,grpc介绍_第8张图片
  3. server端获取元信息进行校验
    go语言rpc,grpc介绍_第9张图片

拦截器(中间件)

gRPC 为在每个 ClientConn/Server 基础上实现和安装拦截器提供了一些简单的 API。 拦截器拦截每个 RPC 调用的执行。用户可以使用拦截器进行日志记录、身份验证/授权、指标收集以及许多其他可以跨 RPC 共享的功能。

在 gRPC 中,拦截器根据拦截的 RPC 调用类型可以分为两类。第一个是普通拦截器(一元拦截器),它拦截普通RPC 调用。另一个是流拦截器,它处理流式 RPC 调用。而客户端和服务端都有自己的普通拦截器和流拦截器类型。因此,在 gRPC 中总共有四种不同类型的拦截器。

客户端端拦截器

普通拦截器/一元拦截器

UnaryClientInterceptor 是客户端一元拦截器的类型,它的函数前面如下:

func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

一元拦截器的实现通常可以分为三个部分: 调用 RPC 方法之前(预处理)、调用 RPC 方法(RPC调用)和调用 RPC 方法之后(调用后)。

  1. 预处理:用户可以通过检查传入的参数(如 RPC 上下文、方法字符串、要发送的请求和 CallOptions 配置)来获得有关当前 RPC 调用的信息。
  2. RPC调用:预处理完成后,可以通过执行invoker执行 RPC 调用。
  3. 调用后:一旦调用者返回应答和错误,用户就可以对 RPC 调用进行后处理。通常,它是关于处理返回的响应和错误的。 若要在 ClientConn 上安装一元拦截器,请使用DialOptionWithUnaryInterceptor的DialOption配置 Dial 。

流拦截器

StreamClientInterceptor是客户端流拦截器的类型。它的函数签名是

func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

流拦截器的实现通常包括预处理和流操作拦截。

  1. 预处理:类似于上面的一元拦截器。
  2. 流操作拦截:流拦截器并没有事后进行 RPC 方法调用和后处理,而是拦截了用户在流上的操作。首先,拦截器调用传入的streamer以获取 ClientStream,然后包装 ClientStream 并用拦截逻辑重载其方法。最后,拦截器将包装好的 ClientStream 返回给用户进行操作。
    若要为 ClientConn 安装流拦截器,请使用WithStreamInterceptor的 DialOption 配置 Dial。

server端拦截器

服务器端拦截器与客户端类似,但提供的信息略有不同。

普通拦截器/一元拦截器

UnaryServerInterceptor是服务端的一元拦截器类型,它的函数签名是

func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

服务端一元拦截器具体实现细节和客户端版本的类似。

若要为服务端安装一元拦截器,请使用 UnaryInterceptor 的ServerOption配置 NewServer。

流拦截器

StreamServerInterceptor是服务端流式拦截器的类型,它的签名如下:

func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

实现细节类似于客户端流拦截器部分。

若要为服务端安装流拦截器,请使用 StreamInterceptor 的ServerOption来配置 NewServer。

拦截器示例

下面将演示一个完整的拦截器示例,我们为一元RPC和流式RPC服务都添加上拦截器。

我们首先定义一个名为valid的校验函数。

// valid 校验认证信息.
func valid(authorization []string) bool {
	if len(authorization) < 1 {
		return false
	}
	token := strings.TrimPrefix(authorization[0], "Bearer ")
	// 执行token认证的逻辑
	// 这里是为了演示方便简单判断token是否与"some-secret-token"相等
	return token == "some-secret-token"
}

客户端拦截器定义

一元拦截器
// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	var credsConfigured bool
	for _, o := range opts {
		_, ok := o.(grpc.PerRPCCredsCallOption)
		if ok {
			credsConfigured = true
			break
		}
	}
	if !credsConfigured {
		opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
			AccessToken: "some-secret-token",
		})))
	}
	start := time.Now()
	err := invoker(ctx, method, req, reply, cc, opts...)
	end := time.Now()
	fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format("Basic"), end.Format(time.RFC3339), err)
	return err
}

其中,grpc.PerRPCCredentials()函数指明每个 RPC 请求使用的凭据,它接收一个credentials.PerRPCCredentials接口类型的参数。credentials.PerRPCCredentials接口的定义如下:

type PerRPCCredentials interface {
	// GetRequestMetadata 获取当前请求的元数据,如果需要则会设置token。
	// 传输层在每个请求上调用,并且数据会被填充到headers或其他context。
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
	// RequireTransportSecurity 指示该 Credentials 的传输是否需要需要 TLS 加密
	RequireTransportSecurity() bool
}

而示例代码中使用的oauth.NewOauthAccess()是内置oauth包提供的一个函数,用来返回包含给定token的PerRPCCredentials。

// NewOauthAccess constructs the PerRPCCredentials using a given token.
func NewOauthAccess(token *oauth2.Token) credentials.PerRPCCredentials {
	return oauthAccess{token: *token}
}

func (oa oauthAccess) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	ri, _ := credentials.RequestInfoFromContext(ctx)
	if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
		return nil, fmt.Errorf("unable to transfer oauthAccess PerRPCCredentials: %v", err)
	}
	return map[string]string{
		"authorization": oa.token.Type() + " " + oa.token.AccessToken,
	}, nil
}

func (oa oauthAccess) RequireTransportSecurity() bool {
	return true
}
流式拦截器

自定义一个ClientStream类型。

type wrappedStream struct {
	grpc.ClientStream
}

wrappedStream重写grpc.ClientStream接口的RecvMsg和SendMsg方法。

func (w *wrappedStream) RecvMsg(m interface{}) error {
	logger("Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
	return w.ClientStream.RecvMsg(m)
}

func (w *wrappedStream) SendMsg(m interface{}) error {
	logger("Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
	return w.ClientStream.SendMsg(m)
}

func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
	return &wrappedStream{s}
}

这里的wrappedStream嵌入了grpc.ClientStream接口类型,然后又重新实现了一遍grpc.ClientStream接口的方法。

下面就定义一个流式拦截器,最后返回上面定义的wrappedStream。

// streamInterceptor 客户端流式拦截器
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
	var credsConfigured bool
	for _, o := range opts {
		_, ok := o.(*grpc.PerRPCCredsCallOption)
		if ok {
			credsConfigured = true
			break
		}
	}
	if !credsConfigured {
		opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
			AccessToken: "some-secret-token",
		})))
	}
	s, err := streamer(ctx, desc, cc, method, opts...)
	if err != nil {
		return nil, err
	}
	return newWrappedStream(s), nil
}

服务端拦截器定义

一元拦截器

服务端定义一个一元拦截器,对从请求元数据中获取的authorization进行校验。

// unaryInterceptor 服务端一元拦截器
func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	// authentication (token verification)
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
	}
	if !valid(md["authorization"]) {
		return nil, status.Errorf(codes.Unauthenticated, "invalid token")
	}
	m, err := handler(ctx, req)
	if err != nil {
		fmt.Printf("RPC failed with error %v\n", err)
	}
	return m, err
}
流拦截器

同样为流RPC也定义一个从元数据中获取认证信息的流式拦截器。

// streamInterceptor 服务端流拦截器
func streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	// authentication (token verification)
	md, ok := metadata.FromIncomingContext(ss.Context())
	if !ok {
		return status.Errorf(codes.InvalidArgument, "missing metadata")
	}
	if !valid(md["authorization"]) {
		return status.Errorf(codes.Unauthenticated, "invalid token")
	}

	err := handler(srv, newWrappedStream(ss))
	if err != nil {
		fmt.Printf("RPC failed with error %v\n", err)
	}
	return err
}

注册拦截器

客户端注册拦截器
conn, err := grpc.Dial("127.0.0.1:8972",
	grpc.WithTransportCredentials(creds),
	grpc.WithUnaryInterceptor(unaryInterceptor),
	grpc.WithStreamInterceptor(streamInterceptor),
)
服务端注册拦截器
s := grpc.NewServer(
	grpc.Creds(creds),
	grpc.UnaryInterceptor(unaryInterceptor),
	grpc.StreamInterceptor(streamInterceptor),
)

go-grpc-middleware

社区中有很多开源的常用的grpc中间件——go-grpc-middleware.

Q&A

对于go grpc不需要注册中心也可以使用;那么grpc是如何实现服务注册功能的?

  1. 对于 gRPC,不需要必须使用注册中心。gRPC 可以直接使用服务的 IP 地址和端口号进行通信。
  2. 但是,对于大型的分布式系统,使用注册中心可以提供更好的服务发现和管理。使用注册中心可以让服务的 IP 地址和端口号动态地注册和注销,从而可以更灵活地管理服务的生命周期。此外,注册中心还可以提供负载均衡和故障转移等功能,以确保服务的高可用性和可靠性。
  3. gRPC 的官方推荐使用的注册中心是 etcd 或 Consul。如果您使用 Kubernetes,也可以使用 Kubernetes 自带的服务发现和负载均衡功能。

etcd和k8s比较,哪个做注册中心好,差异是什么,优劣是什么?

  1. etcd 和 Kubernetes 都可以作为 gRPC 的注册中心,但是它们的设计目标和适用场景略有不同。

  2. etcd 是一个开源的分布式键值存储系统,它提供了高可用性、强一致性和快速的响应能力。etcd 可以作为 gRPC 的注册中心,可以管理服务的注册和发现,并提供服务的负载均衡、故障转移等功能。etcd 的优点是易于部署和管理,同时具有高可用性和可靠性,可以满足大规模分布式系统的需求。但是,etcd 对于运维人员的技术水平要求较高,需要了解其底层实现原理和配置方法。

  3. Kubernetes 是一个开源的容器编排系统,它可以管理容器的部署、扩缩容、服务发现等任务。Kubernetes 中自带了服务发现和负载均衡的功能,可以作为 gRPC 的注册中心来管理服务的注册和发现。Kubernetes 的优点是易于部署和使用,同时具有良好的扩展性和可靠性,可以和容器化的应用无缝集成。但是,Kubernetes 的学习和配置成本较高,需要了解其复杂的 API 对象和控制器模型。

  4. 选择 etcd 还是 Kubernetes 作为 gRPC 的注册中心,取决于具体的应用场景和技术团队的能力。如果您的系统已经使用了 Kubernetes,那么使用 Kubernetes 作为注册中心可能更加方便。如果您需要一个独立的、可靠的分布式存储系统,那么使用 etcd 可能更加适合。

你可能感兴趣的:(微服务,golang,rpc,java)