Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器

Golang-gRPC学习笔记

  • 入门
    • RPC简介
    • gRPC简介
    • gRPC服务方法
      • 1.一元RPC(简单RPC)
      • 2.服务端流RPC
      • 3.客户端流RPC
      • 4.双向流RPC
    • protobuf简介
  • 安装
    • protobuf
      • MacOS
    • grpc-go
    • golang protobuf
  • 实战
    • 一元RPC
      • 编写proto文件,定义接口
      • 生成go rpc代码
      • 编写服务端
      • 编写客户端
      • 运行展示
    • 流式RPC
      • 接口定义
      • 服务端
      • 客户端
      • 运行效果:
  • 进阶
    • 设置超时
    • 反射功能reflection
      • 启动反射服务
      • 安装grpcurl
      • 查询
    • 认证
      • tls
        • keys
        • 客户端配置
        • 服务端配置
      • 自定义认证(token)
        • 客户端
        • 服务端
    • 拦截器interceptor
      • 客户端
      • 服务端
    • 还有很多其他的特征,请参考:https://github.com/grpc/grpc-go/tree/master/examples
  • 参考资料:

入门

RPC简介

RPC(Remote Procedure Call Protocol),远程调用协议。调用远程的函数实现功能。
广泛应用于分布式,微服务,解耦,跨语言通信等。RPC的目标就是把远程调用的过程透明化。
RPC把函数调用参数和返回值进行序列化,然后通过网络服务发送和接收。
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第1张图片

gRPC简介

gRPC是google的开源的高可用的通用的RPC框架
gRPC使用protobuf作为接口定义语言和基础消息交换格式
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第2张图片

gRPC服务方法

1.一元RPC(简单RPC)

客户端发送一个单独的请求,并且接收服务端对这个请求的单独回复,类似普通的函数调用
rpc SayHello(HelloRequest) returns (HelloResponse);

2.服务端流RPC

客户端发送一个单独的请求,服务端返回流式数据,客户端读取流式数据直到EOF,gRPC保证每个调用中的信息排序
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

3.客户端流RPC

客户端写入流式数据,写入完成后等待服务端读取并返回单独的结果,gRPC保证每个调用中的信息排序
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

4.双向流RPC

双方都使用读写流发送一系列消息。 这两个流是独立运行的,因此客户端和服务器可以按照自己需要的顺序进行读写,例如,服务器可以在写响应之前等待接收所有客户端消息,或者可以先读取一条消息再写入一条消息,或其他一些读写组合。 gRPC保证每个调用中的信息排序
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

protobuf简介

google开源的成熟的语言无关,平台无关的可扩展的结构化数据序列化机制,类似xml,但是更小更快更简单。
可以通过一次定义数据结构,通过生成的源代码轻松编写和读取各种数据流以及使用多种语言的结构化数据。
protobuf需要在原型文件定义数据结构,其原型文件的扩展名是.proto

安装

protobuf

MacOS

brew的安装参考:https://brew.sh/
brew的安装命令:/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
使用brew安装protobuf:brew install protobuf
验证安装:

$ protoc --version
libprotoc 3.14.0

grpc-go

这里的话,先建立一个go mod项目,在项目下进行操作,项目名myproto:
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第3张图片
然后在项目目录里运行go get -u google.golang.org/grpc来安装grpc-go
这一步是在go安装grpc库,也就是让go支持grpc

$ go get -u google.golang.org/grpc
go: google.golang.org/grpc upgrade => v1.35.0
go: google.golang.org/protobuf upgrade => v1.25.0
go: github.com/golang/protobuf upgrade => v1.4.3
go: golang.org/x/text upgrade => v0.3.5
go: golang.org/x/sys upgrade => v0.0.0-20210124154548-22da62e12c0c
go: golang.org/x/net upgrade => v0.0.0-20210119194325-5f4716e94777
go: google.golang.org/genproto upgrade => v0.0.0-20210203152818-3206188e46ba
go: downloading golang.org/x/net v0.0.0-20210119194325-5f4716e94777
go: downloading google.golang.org/genproto v0.0.0-20210203152818-3206188e46ba
go: downloading golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
go: downloading golang.org/x/text v0.3.5

golang protobuf

这一步是安装protoc-gen-go,来让protoc支持生成go代码
在项目目录下运行go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

$ go get -u github.com/golang/protobuf/{
     proto,protoc-gen-go} 
go: found github.com/golang/protobuf/protoc-gen-go in github.com/golang/protobuf v1.4.3
go: found github.com/golang/protobuf/proto in github.com/golang/protobuf v1.4.3
go: google.golang.org/protobuf upgrade => v1.25.0

这个时候检查一下项目的go.mod文件,应该是这样的:
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第4张图片
至此环境配置完成。

实战

一元RPC

这里我们用两个数加和函数功能做例子。

编写proto文件,定义接口

在项目根目录新建目录及文件UnaryRPC/proto/unary.proto
文件内容:

//指定使用proto3(proto2,3有很多不同,不可混写)
syntax = "proto3";
//指定生成的go_package,简单来说就是生成的go代码使用什么包,因为我们想用proto这个文件夹来装这个代码,所以package proto
option go_package = ".;proto";

//定义rpc服务
//此处rpc服务的定义,一定要从服务端的角度考虑,即接受请求,处理请求并返回响应的一端
//请求接受一个SumReq(num1, num2)
//响应回发一条sum = num1 + num2
service Sum{
  rpc Sum(SumReq)returns(SumRes){}
}

message SumReq {
  int32 num1 = 1;
  int32 num2 = 2;
}

message SumRes {
  int32 sum = 1;
}

生成go rpc代码

进入命令行,切到unary.proto文件所在的目录,执行:protoc --go_out=plugins=grpc:. unary.proto
这条命令含义是在当前目录编译unary.proto,插件使用grpc
执行完毕可以在同目录看到一个unary.pb.go
来看一下生成文件的内容,重点关注:
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第5张图片
我们在应用这个proto的时候:

  • 客户端调用SumClient接口的Sum方法,传入SumReq,接收SumRes
  • 服务端实现SumServer接口的Sum方法,接收SumReq,返回SumRes

编写服务端

文件:UnaryRPC/server/server.go
内容,有疑问看注释,再有疑问留评论:

package main

import (
	"context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"log"
	"net"

	// import UnaryRPC 生成的proto
	"myproto/UnaryRPC/proto"
)

// 实现SumServer接口
type SumServer struct{
     }

func (*SumServer) Sum(ctx context.Context, req *proto.SumReq) (*proto.SumRes, error) {
     
	// 暂时不对ctx进行处理
	return &proto.SumRes{
     Sum: req.Num1 + req.Num2}, nil
}

func main() {
     
	// 定义rpc server监听的端口
	lis, err := net.Listen("tcp", ":6012")
	if err != nil {
     
		log.Fatalf("failed to listen: %v", err)
	}

	// 实例化一个新的服务端对象
	s := grpc.NewServer()
	// 向服务端对象注册SumServer服务
	proto.RegisterSumServer(s, &SumServer{
     })
	// 注册服务端反射服务
	reflection.Register(s)

	// 启动服务
	s.Serve(lis)

	// 可配合ctx实现服务端的动态终止
	//s.Stop()
}

编写客户端

文件:UnaryRPC/client/client.go
内容,依旧是看注释:

package main

import (
	"context"
	"google.golang.org/grpc"
	"log"
	"myproto/UnaryRPC/proto"
	"time"
)

func main() {
     
	// 向服务端建立连接
	grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
	if err != nil {
     
		log.Fatalln(err)
	}

	// 实例化一个SumClient对象
	client := proto.NewSumClient(grpcConn)

	// 设置超时
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 调用Sum函数
	res, err := client.Sum(ctx, &proto.SumReq{
     
		Num1: 54,
		Num2: 35,
	})
	if err != nil {
     
		log.Fatalln(err)
	}

	// 输出结果
	log.Println("the sum is", res.Sum)
}

运行展示

目前的目录结构
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第6张图片

  • 服务端
    Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第7张图片
  • 客户端
    Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第8张图片

流式RPC

接口定义

文件:StreamingRPC/proto/stream.proto
内容:

syntax = "proto3";
option go_package = ".;proto";

//三个流式rpc
//GetStream服务器返回流
//PutStream客户端上传流
//DiStream双向流
service Stream{
  rpc GetStream(StreamReq)returns(stream StreamRes){}
  rpc PutStream(stream StreamReq)returns(StreamRes){}
  rpc BiStream(stream StreamReq)returns(stream StreamRes){}
}

message StreamReq {
  string data = 1;
}

message StreamRes {
  string data = 1;
}

服务端

文件:StreamingRPC/server/server.go

package main

import (
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"log"
	"myproto/StreamingRPC/proto"
	"net"
	"strings"
	"sync"
	"time"
)

type server struct {
     
}

//服务端下载单向流
func (*server) GetStream(req *proto.StreamReq, getServer proto.Stream_GetStreamServer) error {
     
	log.Println("GetServer Start.")
	i := 0
	for i < 3 {
     
		i++
		getServer.Send(&proto.StreamRes{
     Data: req.Data + ":" + fmt.Sprintf("%v", time.Now().Unix())})
		log.Println("Get Res Send.")
		time.Sleep(1 * time.Second)
	}
	log.Println("GetServer Start.")
	return nil
}

//客户端上传单向流
func (*server) PutStream(putServer proto.Stream_PutStreamServer) error {
     
	log.Println("PutServer Start.")
	var cliStr strings.Builder
	for {
     
		if putReq, err := putServer.Recv(); err == nil {
     
			log.Println("Put Req: " + putReq.Data)
			cliStr.WriteString(putReq.Data)
		} else {
     
			putServer.SendAndClose(&proto.StreamRes{
     Data: "Finish. Your Data is: " + cliStr.String()})
			break
		}
	}
	log.Println("PutServer Done.")
	return nil
}

//双向流
func (*server) BiStream(biServer proto.Stream_BiStreamServer) error {
     
	log.Println("BiServer Start.")
	wg := sync.WaitGroup{
     }
	wg.Add(2)
	go func() {
     
		for {
     
			biReq, err := biServer.Recv()
			if err != nil {
     
				break
			} else {
     
				log.Println("Bi Req: " + biReq.Data)
			}
		}
		wg.Done()
	}()

	go func() {
     
		for {
     
			err := biServer.Send(&proto.StreamRes{
     Data: "ok"})
			if err != nil {
     
				break
			} else {
     
				log.Println("Bi Res: ok")
				time.Sleep(time.Second)
			}
		}
		wg.Done()
	}()

	wg.Wait()
	log.Println("BiServer Done.")
	return nil
}

func main() {
     
	//监听端口
	lis, err := net.Listen("tcp", ":6012")
	if err != nil {
     
		log.Fatalf("failed to listen: %v", err)
	}
	//创建一个grpc 服务器
	s := grpc.NewServer()
	//注册事件
	proto.RegisterStreamServer(s, &server{
     })
	// 注册服务端反射服务
	reflection.Register(s)
	//处理链接
	s.Serve(lis)
}

客户端

文件:StreamingRPC/client/client.go
内容:

package main

import (
	"context"
	"google.golang.org/grpc"
	"log"
	"myproto/StreamingRPC/proto"
	"strconv"
	"time"
)

func main() {
     
	//新建grpc连接
	grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithInsecure())
	if err != nil {
     
		log.Fatalln(err)
	}
	defer grpcConn.Close()

	//通过连接 生成一个client对象。
	c := proto.NewStreamClient(grpcConn)

	//设置超时
	//ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	//defer cancel()
	ctx := context.Background()

	//调用服务端推送流,获取服务端流数据
	log.Println("GetStream:")
	getClient, err := c.GetStream(ctx, &proto.StreamReq{
     Data: "Get Time"})
	if err != nil {
     
		log.Fatalln(err)
		return
	}
	for {
     
		aa, err := getClient.Recv()
		if err != nil {
     
			log.Println("Get Done. EOF reached.")
			break
		}
		log.Println("Get Res Data: " + aa.Data)
	}

	//客户端推送流
	log.Println("PutStream:")
	putClient, err := c.PutStream(ctx)
	if err != nil {
     
		log.Fatalln(err)
		return
	}
	i := 1
	for i < 4 {
     
		i++
		var putData = proto.StreamReq{
     Data: "Put " + strconv.Itoa(i) + " "}
		log.Println("Put Req Data: " + putData.Data)
		putClient.Send(&putData)
		time.Sleep(time.Second)
	}
	putRes, err := putClient.CloseAndRecv()
	if err != nil {
     
		log.Fatalln(err)
	}
	log.Printf("Put Done. Res is %v", putRes.Data)

	//双向流
	log.Println("BiStream:")
	//设置结束等待
	done := make(chan struct{
     })
	biClient, err := c.BiStream(ctx)
	if err != nil {
     
		log.Fatalln(err)
		return
	}
	go func() {
     
		for {
     
			biRes, err := biClient.Recv()
			if err != nil {
     
				return
			} else {
     
				log.Println("Bi Res Data: " + biRes.Data)
			}
		}
	}()

	go func() {
     
		i := 1
		for i < 4 {
     
			i++
			biReq := proto.StreamReq{
     Data: "send " + strconv.Itoa(i) + " "}
			log.Println("Bi Req Data: " + biReq.Data)
			biClient.Send(&biReq)
			time.Sleep(time.Second)
		}
		done <- struct{
     }{
     }
	}()

	<-done
	log.Println("All Done.")
}

运行效果:

客户端:
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第9张图片
服务端:
Golang-gRPC学习笔记,streamRPC,reflection,认证,拦截器_第10张图片

进阶

设置超时

注意设置合理超时

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

反射功能reflection

可以用Go语言实现的grpcurl工具来查询服务列表或调用grpc方法

启动反射服务

在服务端注册反射服务

s := grpc.NewServer()
pb.RegisterYourOwnServer(s, &server{
     })
//Register reflection service on gRPC server. 
reflection.Register(s)

安装grpcurl

命令:

go get github.com/fullstorydev/grpcurl
go install github.com/fullstorydev/grpcurl/cmd/grpcurl
$ go get github.com/fullstorydev/grpcurl
go: downloading github.com/fullstorydev/grpcurl v1.8.0
go: github.com/fullstorydev/grpcurl upgrade => v1.8.0
go: downloading github.com/jhump/protoreflect v1.6.1
$ go install github.com/fullstorydev/grpcurl/cmd/grpcurl
go: downloading github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad
go: downloading github.com/google/uuid v1.1.2
go: downloading github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403
go: downloading github.com/census-instrumentation/opencensus-proto v0.2.1
go: downloading github.com/envoyproxy/protoc-gen-validate v0.1.0
go: downloading github.com/google/go-cmp v0.5.0
go: downloading golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
go: downloading golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
go: downloading cloud.google.com/go v0.56.0

查询

  1. 启动server端
  2. 命令行执行
    • grpcurl -plaintext localhost:6012 list
    • grpcurl -plaintext localhost:6012 describe
  3. 效果
$ grpcurl -plaintext localhost:6012 list           
Stream
grpc.reflection.v1alpha.ServerReflection

$ grpcurl -plaintext localhost:6012 describe       
Stream is a service:
service Stream {
     
  rpc BiStream ( stream .StreamReq ) returns ( stream .StreamRes );
  rpc GetStream ( .StreamReq ) returns ( stream .StreamRes );
  rpc PutStream ( stream .StreamReq ) returns ( .StreamRes );
}
grpc.reflection.v1alpha.ServerReflection is a service:
service ServerReflection {
     
  rpc ServerReflectionInfo ( stream .grpc.reflection.v1alpha.ServerReflectionRequest ) returns ( stream .grpc.reflection.v1alpha.ServerReflectionResponse );
}

认证

tls

keys

生成私钥:openssl genrsa -out server.key 2048
生成自签名公钥:openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650

$ openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:CN
State or Province Name (full name) []:BJ
Locality Name (eg, city) []:BJ
Organization Name (eg, company) []:CAS
Organizational Unit Name (eg, section) []:IIE
Common Name (eg, fully qualified host name) []:MT
Email Address []:[email protected]

客户端配置

//初始化证书
//GODEBUG=x509ignoreCN=0
creds, err := credentials.NewClientTLSFromFile("/Users/susu/go/src/myproto/tlsSimpleRPC/keys/server.pem", "MT")
if err != nil {
     
	grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
// 向服务端建立连接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", grpc.WithTransportCredentials(creds))

这里运行可能会报错:code = Unavailable desc = connection error: desc = “transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0”
解决方式是添加环境变量:GODEBUG=x509ignoreCN=0即可

服务端配置

creds, err := credentials.NewServerTLSFromFile("/Users/susu/go/src/myproto/tlsSimpleRPC/keys/server.pem", "/Users/susu/go/src/myproto/tlsSimpleRPC/keys/server.key")
if err != nil {
     
	log.Fatalf("Failed to generate credentials %v", err)
}
// 实例化一个新的服务端对象
s := grpc.NewServer(grpc.Creds(creds))

自定义认证(token)

需要实现PerRPCCredentials接口,实现GetRequestMetadataRequireTransportSecurity两个方法。

客户端

文件:tlsSimpleRPC/tlsClient/client.go

var opts []grpc.DialOption
//初始化证书
//GODEBUG=x509ignoreCN=0
creds, err := credentials.NewClientTLSFromFile("/Users/susu/go/src/myproto/tlsSimpleRPC/keys/server.pem", "MT")
if err != nil {
     
	grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
opts = append(opts, grpc.WithTransportCredentials(creds))
opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))

// 向服务端建立连接
grpcConn, err := grpc.Dial("127.0.0.1"+":6012", opts...)

服务端

这个服务端的实现是把自定义认证的过程放在了服务端接口的具体实现中。
修改SumServer的Sum方法,从metadata获取认证信息。
这样做会导致效率低,因为每次调用rpc都需要认证一次,增加了传输开销。更好的方法是使用拦截器interceptor,实现一次认证多次调用。下面介绍拦截器。
文件:tlsSimpleRPC/tlsServer/server.go

func (*SumServer) Sum(ctx context.Context, req *proto.SumReq) (*proto.SumRes, error) {
     
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
     
		return nil, status.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 nil, status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
	}
	// 暂时不对ctx进行处理
	return &proto.SumRes{
     Sum: req.Num1 + req.Num2}, nil
}

拦截器interceptor

使用方式:在客户端和服务端的opts,也就是grpc.DialOption注册拦截器opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
这样就可以把认证的过程独立出来了。

客户端

文件:tlsSimpleRPC/tlsClient-interceptor/client.go

// 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...)
	log.Printf("method=%s req=%v rep=%v duration=%s error=%v\n", method, req, reply, time.Since(start), err)
	return err
}
func main() {
     
	...
		//添加TLS认证
	opts = append(opts, grpc.WithTransportCredentials(creds))
	//添加自定义认证
	opts = append(opts, grpc.WithPerRPCCredentials(new(customCredential)))
	//添加拦截器interceptor
	opts = append(opts, grpc.WithUnaryInterceptor(interceptor))

	// 向服务端建立连接
	grpcConn, err := grpc.Dial("127.0.0.1"+":6012", opts...)
	...
}

服务端

把之前写在接口实现里的认证过程独立出来成为auth函数
然后在服务端注册拦截器
文件:tlsSimpleRPC/tlsServer-interceptor/server.go

func (*SumServer) Sum(ctx context.Context, req *proto.SumReq) (*proto.SumRes, error) {
     
	// 暂时不对ctx进行处理
	return &proto.SumRes{
     Sum: req.Num1 + req.Num2}, nil
}

// interceptor 拦截器
func interceptor(ctx context.Context, req interface{
     }, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{
     }, error) {
     
	err := auth(ctx)
	if err != nil {
     
		return nil, err
	}
	// 继续处理请求
	return handler(ctx, req)
}
func auth(ctx context.Context) error {
     
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
     
		return status.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 status.Errorf(codes.Unauthenticated, "Token认证信息无效: appid=%s, appkey=%s", appid, appkey)
	}
	return nil
}
func main() {
     
	...
		//注册TLS认证
	opts = append(opts, grpc.Creds(creds))
	//注册interceptor
	opts = append(opts, grpc.UnaryInterceptor(interceptor))
	// 实例化一个新的服务端对象
	s := grpc.NewServer(opts...)
	// 向服务端对象注册SumServer服务
	proto.RegisterSumServer(s, &SumServer{
     })
	...
}

还有很多其他的特征,请参考:https://github.com/grpc/grpc-go/tree/master/examples

参考资料:

gRPC 实操指南(golang)
go语言grpc的stream使用

你可能感兴趣的:(go,grpc,rpc,goland,go语言)