微服务,gRPC 一文全解(五)

前言

RPC

是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。
微服务常用更高效的rpc(远程过程调用协议)通信。

RPC优点:
  • 提高开发效率,开发人员可以把更多精力放在具体的接口实现,而不必考虑数据的底层传输问题。
  • 大多数rpc框架都是很多优秀开发人员的智慧结晶,它们的功能实现和执行效率都很优秀。
  • client端和server端必须遵循统一的接口规范,避免产生client和server之间接口或数据局结构不匹配的情况。

gRPC:

  • gRPC是一个高性能、通用的开源RPC框架,其由Google
    2015年主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol
    Buffers)序列化协议开发,且支持众多开发语言。gRPC提供了一种简单的方法来精确地定义服务和为iOS、Android和后台支持服务自动生成可靠性很强的客户端功能库。客户端充分利用高级流和链接功能,从而有助于节省带宽、降低的TCP链接次数、节省CPU使用、电池寿命。
  • 最新的Google API支持gRPC
  • 支持 C, C++, Node.js, Python, Ruby, Objective-C,PHP and C#
  • 当前版本Alpha
  • 协议 BSD

ProtoBuf

  • 其由Google 2001年设计,2008年开源。
  • Google内部的服务几乎都是用的PB协议
  • 久经考验、充分验证、良好实现
  • 使用ProtoBuf: Google、Hadoop、ActiveMQ、Netty
  • 当前版本v3.0.0-alpha-3
  • 协议 BSD
    proto文件的具体写法可以参考官网文档:https://developers.google.cn/protocol-buffers/docs/gotutorial

gRPC原生例子

proto文件

创建book.proto文件,并输入

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

package book; // 包名

// 包含id的一个请求消息
message BookRequest {
    int32 id = 1;
}

// 包含名称的响应消息
message BookResponse {
    string name = 1;
}

// 定义根据id获取名称的服务,对应go中的接口
service BookFun {
    rpc GetBookInfoByID (BookRequest) returns (BookResponse) {}
}

将.proto文件放到book文件夹下,并执行如下命令,生成book.pb.go文件,

protoc --go_out=plugins=grpc:. book.proto

服务端

获取grpc开发工具

go get google.golang.org/grpc
type BookEntity struct {
}
//实现GetBookInfoByID接口
func (b *BookEntity) GetBookInfoByID(ctx context.Context,requ *book.BookRequest) (*book.BookResponse, error){
	resp := new(book.BookResponse)
	switch requ.Id {
	case 1:
		resp.Name = "西游记"
	default:
		resp.Name = "金瓶梅"
	}
	return resp,nil
}

func main(){
	ls, _ := net.Listen("tcp", ":666")
	gs := grpc.NewServer()
	//将接口实现注册到grpc实例中
	book.RegisterBookFunServer(gs,new(BookEntity))
	gs.Serve(ls)
}

客户端

func main(){
	conn,err := grpc.Dial(":666",grpc.WithInsecure() )
	if err!=nil{
		panic(err)
	}
	defer conn.Close()
	//创建一个BookFun的客户端实例
	client := book.NewBookFunClient(conn)
	//发送请求
	resp,err := client.GetBookInfoByID(context.Background(),&book.BookRequest{Id:1})

	if err!=nil{
		fmt.Println(err)
	}
	fmt.Println(resp)
}

gRPC+gokit简单栗子

服务端

1.创建一个book.proto文件

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

package book; // 包名

// 包含id的一个请求消息
message BookRequest {
    int32 id = 1;
}

// 包含名称的响应消息
message BookResponse {
    string name = 1;
}

// 定义根据id获取名称的服务
service BookFun {
    rpc GetBookInfoByID (BookRequest) returns (BookResponse) {}
}

2.在文件目录下运行如下命令,生成book.pb.go, 需要提前安装protobuf并配置环境变量

protoc --go_out=plugins=grpc:. book.proto

3.在service文件夹下创建go文件 并创建接口和实现方法

type BookInter interface {
	GetBookInfoByID(int32)string
}
type Book struct {
}
func(b * Book)GetBookInfoByID(id int32)string{
	switch id {
	case 1:
		return "西游记"
	case 2:
		return "三国演义"
	case 3:
		return "水浒传"
	case 4:
		return "红楼梦"
	case 5:
		return "金瓶梅"
	}
	return "未知id"
}

4.在transport文件夹下实现endpoint的编解码方法

func DecodeBook(ctx context.Context,inter interface{}) (request interface{}, err error){
	return inter,nil
}

func EncodeBook(ctx context.Context,inter interface{}) (response interface{}, err error){
	return inter,nil
}

5.在epoint文件夹下, 声明创建endpoint的方法,该方法调用业务逻辑

func GetGrpcEndpointForGetBookIDS(inter service.BookInter)endpoint.Endpoint{
	return func(ctx context.Context, request interface{}) (response interface{}, err error){
		bookresp := new(book.BookResponse)
		bookresp.Name =  inter.GetBookInfoByID(request.(*book.BookRequest).Id)
		return bookresp,nil
	}
}

6.声明一个结构体,并实现proto中的接口,在实现方法中调用endpoint的ServeGRPC方法。
kitgrpc 是 “github.com/go-kit/kit/transport/grpc”

//调用该方法将GetBookInfoByID方法的实现注册到gRPC服务中
func InitGRPCRouter(gs * grpc.Server)  {
	book.RegisterBookFunServer(gs,NewGrpcBook())
}
//创建实现接口的函数,此处实现了endpoint的创建,并赋值给结构体中的参数
func NewGrpcBook() book.BookFunServer{
	b :=  &GrpcBook{}
	b.Handler = kitgrpc.NewServer(epoint.GetGrpcEndpointForGetBookIDS(new(service.Book)),
		transport.DecodeBook,transport.EncodeBook)
	return b
}

type GrpcBook struct {
	Handler kitgrpc.Handler
}
//实现proto中的接口
func (g * GrpcBook)GetBookInfoByID(ctx context.Context,request *book.BookRequest)(*book.BookResponse,error){
	_,res ,err := g.Handler.ServeGRPC(ctx,request)
	return res.(*book.BookResponse),err
}

7.创建GRPC实例,并注册方法

func main(){
	ls, _ := net.Listen("tcp", ":666")
	gs := grpc.NewServer()

	router.InitGRPCRouter(gs)

	gs.Serve(ls)
}

客户端

1.声明解压缩方法,创建一个endpioint实例
import kitgrpc “github.com/go-kit/kit/transport/grpc”

//创建一个grpc客户端
//第二个参数是接口名称 .proto文件中的 BookFun 
//第三个参数是方法名称 .proto文件中的 GetBookInfoByID 
func GetEndpoint(cc *grpc.ClientConn, serviceName string, 	method string,) *kitgrpc.Client{
	//参数grpcReply是grpc返回值,实际上是取类型
	return  kitgrpc.NewClient(cc,serviceName,method,EncodeRequestBook,DecodeResponseBook,book.BookResponse{})
}
//编码
func EncodeRequestBook (ctx context.Context,inter interface{}) (request interface{}, err error){
	return inter,nil
}
//解码
func DecodeResponseBook(ctx context.Context,inter interface{}) (response interface{}, err error){
	return inter,nil
}

2.创建grpc实例并调用

func main(){
	conn,err := grpc.Dial(":666",grpc.WithInsecure() )
	if err!=nil{
		panic(err)
	}
	defer conn.Close()
	//创建endpoint,并指明grpc调用的接口名和方法名
	client := GetEndpoint(conn,"book.BookFun", "GetBookInfoByID")
	resp,err:= client.Endpoint()(context.Background(),&book.BookRequest{Id:1})

	if err!=nil{
		fmt.Println(err)
	}
	fmt.Println(resp)
}

测试

将服务运行起来,然后运行客户端,截图如下
在这里插入图片描述

拦截器

拦截器在作用于每一个 RPC 调用,通常用来做日志,认证,metric 等等

interfactor 分为两种

  • unary interceptor 拦截 unary(一元) RPC 调用
  • stream interceptor 处理 stream RPC

服务端

UnaryServerInterceptor

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

unary会根据消息进行拦截,todo后,可以调用invoker方法继续执行,并可以在方法后添加一些滞后操作

err := invoker(ctx, method, req, reply, cc, opts...)

StreamServerInteceptor源码

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

stream会根据流进行拦截,
通过调用streamer 可以获得 ClientStream, 包装ClientStream 并重载他的 RecvMsg 和 ·SendMsg 方法,即可做一些拦截处理了最后将包装好的 ClientStream 返回给客户
例子如下:(偷来的,原文

type wrappedStream struct{
	grpc.ClientStream
}
func (w *wrappedStream) RecvMsg(m interface{})error{
	log.Printf("Receive a message (Type: %T)", m)
	return w.ClientStream.RecvMsg(m)
}
func (w *wrappedStream)SendMsg(m interface{})error{
	log.Printf("Send a message (Type: %T)", m)
	return w.ClientStream.SendMsg(m)
}

func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption)(grpc.ClientStream, error){
	// do soming

	// return ClientStream
	s , err := streamer(ctx, desc, cc, method, opts...)
	if err != nil{
		return nil, err
		}
	return &wrappedStream{s}, nil
}

添加方法

s := grpc.NewServer(
    grpc.UnaryInterceptor(unaryInterceptor),
    grpc.StreamInterceptor(streamInterceptor)
)

链式拦截器
调用 grpc.NewServer 时,使用ChainUnaryInterceptor 和 ChainStreamInterceptor 可以设置链式的 interceptor

func ChainStreamInterceptor(interceptors ...StreamServerInterceptor) ServerOption
func ChainUnaryInterceptor(interceptors ...UnaryServerInterceptor) ServerOption

第一个 interceptor 是最外层的,最后一个为最内层
使用UnaryInterceptor, StreamInterceptor·添加的interceptor,总是最先执行

客户端

对应服务端

UnaryClientInterceptor

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

StreamClientInterceptor

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

添加方法

conn, err := grpc.Dial(
    grpc.WithUnaryInterceptor(unaryInterepotr),
    grpc.WithStreamInterceptor(streamInterceptor)
)

链式拦截器

func WithChainStreamInterceptor(interceptors ...StreamClientInterceptor) DialOption
func WithChainUnaryInterceptor(interceptors ...UnaryClientInterceptor) DialOption

元数据

grpc能够通过ctx上下文携带元数据
实现方法是通过grpc包下的metadate包实现
“google.golang.org/grpc/metadata”

MD

MD是元数据的基本结构如下,通过操作md实现在ctx上下文中添加提取数据

type MD map[string][]string

创建MD

  • 键不允许是“grpc-”开头,该开头已内部使用
  • 键以“-bin” 为后缀,这时,值会在传输前后以 base64 进行编解码,进行二进制存储
  • 提示:
    如果对metadata进行修改,那么需要用拷贝的副本进行修改 (FromIncomingContext的注释)
    方法是:func (md MD) Copy() MD
func New(m map[string]string) MD
func Pairs(kv ...string) MD
//将若干个md连接到一起
func Join(mds ...MD) MD
//从上下文中提取一个md
//如果对metadata进行修改,那么需要用拷贝的副本进行修改
func FromIncomingContext(ctx context.Context) (md MD, ok bool)
//创建一个md副本  
func (md MD) Copy() MD

md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
md = metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2",
    "key2", "val2",
    "key-bin", string([]byte{96, 102}),
)

发送方法

客户端

//向一个已有的ctx中拼接
func AppendToOutgoingContext(ctx context.Context, kv ...string) context.Context
//根据MD创建一个新的ctx上下文
func NewOutgoingContext(ctx context.Context, md MD) context.Context

服务器

  • server 端会把 metadata 分为 header 和 trailer 发送给 client
  • unary RPC 可以通过 CallOption grpc.SendHeader 和 grpc.SetTriler 来发送 header, trailer metadata
  • stream RPC 则可以直接使用 ServerStream 接口的方法
  • SetTriler 可以被调用多次,并且所有 metadata 会被合并,当 RPC 返回的时候, trailer metadata 会被发送
//unary rpc
func SendHeader(ctx context.Context, md metadata.MD) error
func SetTrailer(ctx context.Context, md metadata.MD) error
//stream rpc
func SendHeader(md metadata.MD) error
func SetTrailer(md metadata.MD) error

接收方法

客户端

  • 和服务器对应采用Header和Trailer方法接收,
    它们返回CallOption,放在grpc调用的方法的最后一个参数 opts …grpc.CallOption上
var header,trailer metadata.MD
bookclient := book.NewBookFunClient(conn)
resp,err:=bookclient.GetBookInfoByID(context.Background(),&book.BookRequest{Id:1}, 
grpc.Header(&header),
grpc.Trailer(&trailer))

stream RPC 同理

stream, err := bookclient.GetBookInfoByID(ctx)

// retrieve header
header, err := stream.Header()
// retrieve trailer
trailer := stream.Trailer()

服务端

  • 服务端调用 FromIncomingContext 即可从 context 中接收 client 发送的 metadata
func FromIncomingContext(ctx context.Context) (md MD, ok bool)

func (g * GrpcBook)GetBookInfoByID(ctx context.Context,request *book.BookRequest)(*book.BookResponse,error){
	//在次提示,如需修改md,请先copy
	md,ok := metadata.FromIncomingContext(ctx)
	//TODO
}

未完待续。。。

猜你喜欢

转载自blog.csdn.net/qq_25490573/article/details/107066387