grpc了解

grpc是由google开发的一款语言中立、平台中立、开源的RPC系统

在grpc中客户端应用可以像调用本地对象一样直接调用另一台不同机器上服务端应用的方法,使得很容易创建分布式应用和服务。与许多RPC系统类似,grpc也是定义一个服务,指定能够被远程调用的方法,在服务端实现该接口,并允许grpc服务器来处理客户端调用。客户端拥有像服务端一样方法的stub。

grpc了解_第1张图片

grpc允许定义四种服务方法

  • 单项RPC,即客户端发送一个请求给服务端,然后从服务端获取一个应答,就像一次普通的函数调用
  • 服务端流式RPC,客户端发送一个请求给服务端,可获取一个数据流给客户端读取消息
  • 客户端流式RPC,即用客户端提供的一个数据流写入并发送一系列消息给服务端,一旦客户端完成消息写入,就等待服务端读取这些消息并应答
  • 双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。

RPC是什么?rpc与http协议的关系是什么?

在分布式计算中,远程过程调用(RPC)是当计算机程序导致一个过程(子例程)在不同的地址空间(通常在共享网络上的另一台计算机上)执行时,它被编码为普通的(本地)过程调用,而无需程序员显式编码远程交互的细节。也就是说,无论子例程在执行程序的本地还是远程,程序员编写的代码本质上是相同的。这是一种客户端-服务器交互形式(调用者是客户端,执行者是服务器),通常通过请求-响应消息传递系统实现。在面向对象的程序设计范型中,rpc被表示为远程方法调用(RMI)。RPC模型意味着一定程度的位置透明性,即无论调用过程是本地的还是远程的,它们在很大程度上是相同的,但通常它们是不相同的,因此可以将本地调用与远程调用区分开来。远程调用通常比本地调用慢几个数量级,也不可靠,因此区分它们很重要。

rpc是进程间通信(IPC)的一种形式,不同的进程有不同的地址空间:如果在同一台主机上,它们有不同的虚拟地址空间,即使物理地址空间是相同的;如果它们在不同的主机上,则物理地址空间是不同的。许多不同(通常不兼容)的技术被用于实现该概念。

客户端rpc调用过程

  1. 客户端调用客户端stub。该调用是一个本地过程调用,参数按常规方式推入栈中。
  2. 客户端stub将参数封装到消息中,并进行系统调用以发送消息。打包参数称为marshalling。
  3. 客户端的本地操作系统将消息从客户端机器发送到服务器机器。
  4. 服务器机器上的本地操作系统将传入的数据包传递给服务器stub。
  5. 服务器stub从消息中解包参数。参数解包称为unmarshaling。
  6. 最后,服务器stub调用服务器过程。回复按相反的方向跟踪相同的步骤。

从上面可以得知其实rpc与http并不是完全并行的概念,rpc其实也可以基于http实现。就比如grpc其实就是基于http2实现的

使用

以下以一个简单的获取产品的场景来演示grpc的使用

使用protoc buffer定义服务

protoBuffer主要编写相应的数据结构以及接口方法定义

syntax="proto3";
package pd;

option go_package="/pd";

// 因为rpc方法参数不能为空,所以定义一个空message
message empty {
}

// 产品
message Prod {
	string id = 1;
	string name = 2;
}

// 因为所有参数都只能为message类型,所以对于prodId还是要定义为message
message ProdId {
	int64 val = 1;
}

产品服务定义如下

syntax="proto3";
package pd;

option go_package="/pd";

import "pd/common.proto";

service ProdInfo{
  rpc addProd(Prod) returns (ProdId);
  rpc getProd(ProdId) returns (Prod);
  // stream就是前文介绍的流
  rpc listProds(empty) returns (stream Prod);
  rpc listProdsByIds(stream ProdId) returns (stream Prod);
}

当我们运行protoc命令之后,就会为我们自动生成pb.go文件

# go install google.golang.org/protobuf/cmd/[email protected]
# go install google.golang.org/grpc/cmd/[email protected]
# export PATH="$PATH:$(go env GOPATH)/bin"

# --go-grpc_out 为grpc插件,可以通过上述命令安装
protoc -I ../  --go-grpc_out=../  ../pd/prod.proto
protoc -I ../  --go_out=../  ../pd/common.proto

实现服务

以下是实现过程

import (
	"context"
	"io"
	"learn/grpc-demo/pd"
)

type prodServer struct {
	pd.UnsafeProdInfoServer
	prodMap map[int64]*pd.Prod
	index   int64
}

func (s *prodServer) GetProd(ctx context.Context, id *pd.ProdId) (*pd.Prod, error) {
	return s.prodMap[id.Val], nil
}

func (s *prodServer) ListProds(empty *pd.Empty, server pd.ProdInfo_ListProdsServer) error {
	for _, prod := range s.prodMap {
		if err := server.Send(prod); err != nil {
			return err
		}
	}

	return nil
}

func (s *prodServer) ListProdsByIds(server pd.ProdInfo_ListProdsByIdsServer) error {
	for {
		in, err := server.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		pid := in.Val
		if prod, ok := s.prodMap[pid]; ok {
			if err = server.Send(prod); err != nil {
				return err
			}
		}
	}
}

func (s *prodServer) AddProd(ctx context.Context, p *pd.Prod) (resp *pd.ProdId, err error) {
	s.prodMap[s.index] = p
	s.index++
	return &pd.ProdId{Val: s.index - 1}, nil
}

值得注意的是出现了一个很奇怪的pd.UnsafeProdInfoServer,在新版protoc-gen-grpc-go编译器中,Server实现必须向前兼容

当然也可以通过以下命令不生成向前兼容service

protoc --go-grpc_out=require_unimplemented_servers=false:.

编写服务端和客户端

服务端监听

	conn,err:=net.Listen("tcp",":8888")
	if err != nil {
		t.Fatalf("fail to listen for %s",err)
	}

	s :=grpc.NewServer()
	pd.RegisterProdInfoServer(s,&prodServer{
		prodMap: make(map[int64]*pd.Prod),
		index: 0,
	})

	err=s.Serve(conn)
	if err != nil {
		t.Fatal("fail to serve")
	}

客户端请求,并可以通过grpc.WithInsecure()取消认证

	conn,err:=grpc.Dial(":8888",grpc.WithInsecure())
	if err != nil {
		t.Fatal("fail to conn server")
	}
	defer conn.Close()

	c:=pd.NewProdInfoClient(conn)
	pid,err:=c.AddProd(context.Background(),&pd.Prod{
		Id:   "x",
		Name: "we",
	})
	if err != nil {
		t.Fatal("add prod failed")
	}

	t.Logf("add prod %d",pid.Val)

认证

gRPC被设计为与各种身份验证机制一起工作,使其可以轻松安全地使用gRPC与其他系统进行通信。

gRPC还提供了一个简单的身份验证API,允许在创建通道或进行调用时提供所有必要的身份验证信息作为凭证。

凭证可以是通道凭证,附加到通道,如SSL。也可以是调用凭证,附加到调用

// SSL/TLS
// server side
creds := credentials.NewTLS(&tls.Config{})
so := grpc.Creds(creds)
s := grpc.NewServer(so)

// client side
creds := credentials.NewTLS(&tls.Config{})
conn, err := grpc.Dial(":8888", grpc.WithTransportCredentials(creds))

Stream

stream的存在是由于简单rpc有数据包过大造成的瞬时压力以及接收数据包时,需要所有数据包都接收成功之后才能回调响应,进行业务处理

func TestProdClient_ListProds(t *testing.T) {
	conn, err := grpc.Dial(":8888", grpc.WithInsecure())
	if err != nil {
		t.Fatal("fail to conn server")
	}
	defer conn.Close()
	c := pd.NewProdInfoClient(conn)

	stream, err := c.ListProds(context.Background(), &pd.Empty{})
	if err != nil {
		t.Fatalf("fail to list prods for %s", err)
	}

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

		if err != nil {
			t.Fatalf("recv prod failed for %s", err)
		}
		t.Logf("get prod %s", prod.Name)
	}
}

Metadata

metadata是关于特定RPC调用信息(例如身份验证细节),以key-value形式存在,其中key是字符串,值通常也是字符串,不过也可以是二进制数据

简单理解可以说是http header中的key-value对

在golang中metadata就是一个map

type MD map[string][]string

创建

metadata创建有两种

  • New

    md := metadata.New(map[string]string{"k":"v"})
    
  • Pair(相同key自动合并)

    md := metadata.Pairs (
      "k1":"v1",
      "k2":"v1",
      "k1":"v2",
    )
    
    // 为了储存二进制数据,可以在key后面加一个-bin。具有-bin的key对应value在创建metadata时会被编码(base64),收到时会被解码
    
    md := metadata.Pairs(
        "key", "string value",
        "key-bin", string([]byte{96, 102}),
    )
    

发送过程

发送过程如下

  1. 创建context
  2. 创建并写入元数据到metadata
  3. 将metadata关联到ctx
    • AppendToOutgoingContext: 添加metadata到context,不会覆盖
    • NewOutgoingContext: 将新创建的metadata添加到context中,会覆盖掉原来的metadata
  4. 通过RPC发送metadata

客户端发送代码

client := pb.NewGreeterClient(conn)
// 生成 metadata 数据
md := metadata.Pairs("k", "v")
ctx := metadata.NewOutgoingContext(context.Background(), md)

resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "hello, world"})
if err == nil {
        fmt.Printf("Reply is %s\n", resp.Message)
} else {
        fmt.Printf("call server error:%s\n", err)
}

服务端发送代码

// unary call
// create and send header
header := metadata.Pairs("header-key", "val")
grpc.SendHeader(ctx, header)
// create and set trailer
trailer := metadata.Pairs("trailer-key", "val")
grpc.SetTrailer(ctx, trailer)

// stream call
header := metadata.Pairs("header-key", "val")
stream.SendHeader(header)
// create and set trailer
trailer := metadata.Pairs("trailer-key", "val")
stream.SetTrailer(trailer)

接收过程

客户端接收metadata是通过grpc.Header()和grpc.Trailer()

// unary call
var header, trailer metadata.MD // variable to store header and trailer
r, err := client.SomeRPCMethod(
    ctx,
    someRequest,
    grpc.Header(&header),    // will retrieve header
    grpc.Trailer(&trailer),  // will retrieve trailer
)

// stream call
stream, err := client.SomeStreamingRPC(ctx)
// retrieve header
header, err := stream.Header()
// retrieve trailer
trailer := stream.Trailer()

服务端则是使用FromIncomingContext接收metadata

// 服务端尝试接收 metadata 数据,通过 FromIncomingContext 接收
md, ok := metadata.FromIncomingContext(ctx)

t, ok := md["k"]

trailer是什么?

在上述metadata中出现了并不是很熟悉的trailer。

header和trailer的接收时机不同。在一元模式中,header和trailer是一起到达客户端的,此时客户端从header或者trailer中获取 metadata 是一样的,但是在流模式中,header是先到达,然后接收多个stream内容,最后才获取到trailer,获取的时机是不一样的,所以 grpc 提供了两种方式让我们发送 metadata

在持久连接中,可能无法提前知道主体的长度,Content-Length也就无法确定。此时可以利用分块编码传输解决,并且分块还可以利用压缩提高效率。此时Transfer-Encodingchunkedtrailer: Content-MD5配合chunked在分块数据末尾补充header信息

HTTP/1.1 200 OK<CR><LF>
Content-Type: text/plain<CR><LF>
Transfer-Encoding: chunked<CR><LF>
Trailer: Content-MD5<CR><LF>
<CR><LF>


11<CR><LF>
123456789012345678901234567<CR><LF>
  
12<CR><LF>
1234567890123456789012345678<CR><LF>
...
0<CR><LF>
<CR><LF>

trailer是一个响应头部,允许在分块发送的消息添加额外的元信息,这些元信息可能随着消息发送动态生成。比如消息的完整性校验,消息的数字签名,或者消息经过处理之后的最终状态等。

通道

gRPC通道提供与指定主机和端口上的gRPC服务器的连接。它用于创建客户端存根。客户端可以指定通道参数来修改gRPC的默认行为,例如打开或关闭消息压缩。信道有状态,包括已连接状态和空闲状态。

Interceptor

在编写HTTP应用程序时,可以使用HTTP中间件将特定于路由的应用程序处理程序包装起来,HTTP中间件允许您在执行应用程序处理程序之前和之后执行一些公共逻辑。我们通常使用中间件来编写横切组件,如授权、日志记录、缓存等。同样的功能可以在gRPC中通过使用一个叫做拦截器的概念来实现。

拦截器可以为客户端和服务端创建,并且两者都支持unary和stream调用

// UnaryClientInterceptor 拦截客户端unary RPC执行。
// 在创建clientConn unary interceptor 可以使用WithUnaryInterceptor()或WithChainUnaryInterceptor()设置为DialOption
// req, reply是相应的请求、响应消息
// cc 调用rpc的ClientConn
// invoker是完成RPC的处理程序,拦截器负责调用它。
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

// UnaryInvoker is called by UnaryClientInterceptor to complete RPCs.
type UnaryInvoker func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error

// StreamClientInterceptor 拦截客户端stram RPC执行
type StreamClientInterceptor func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

// Streamer is called by StreamClientInterceptor to create a ClientStream.
type Streamer func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (ClientStream, error)

// UnaryServerInterceptor 拦截服务端unary RPC执行。
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

// StreamServerInterceptor 拦截服务端stram RPC执行
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

以下写一个简单的log interceptor来展示用法。其中一个stream interceptor输出处理时长,两个unary interceptor输出所在interceptor

func serverLogInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
	start := time.Now()
	err := handler(srv, ss)
	fmt.Printf("handler %p handle rpc req %s with err %v duration %s", handler, srv, err, time.Since(start))
	return err
}

func serverChain1Interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	fmt.Println("chain 1 interceptor")
	return handler(ctx, req)
}

func serverChain2Interceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
	fmt.Println("chain 2 interceptor")
	return handler(ctx, req)
}

func withServerStreamInterceptor() grpc.ServerOption {
	return grpc.StreamInterceptor(serverLogInterceptor)
}

func withServerUnaryChainInterceptor() grpc.ServerOption {
	return grpc.ChainUnaryInterceptor(serverChain1Interceptor, serverChain2Interceptor)
}

func TestRunServerWithStreamIntercept(t *testing.T) {
	conn, err := net.Listen("tcp", ":8888")
	if err != nil {
		t.Fatalf("fail to listen for %s", err)
	}

	s := grpc.NewServer(withServerStreamInterceptor(), withServerUnaryChainInterceptor())
	pd.RegisterProdInfoServer(s, &prodServer{
		prodMap: make(map[int64]*pd.Prod),
		index:   0,
	})

	err = s.Serve(conn)
	if err != nil {
		t.Fatal("fail to serve")
	}
}

Ref

  1. https://doc.oschina.net/grpc?t=58008
  2. https://blog.gopheracademy.com/advent-2017/go-grpc-beyond-basics/
  3. https://grpc.io/docs/guides/auth/
  4. https://shijuvar.medium.com/writing-grpc-interceptors-in-go-bf3e7671fe48
  5. https://stackoverflow.com/questions/65079032/grpc-with-mustembedunimplemented-method
  6. https://en.wikipedia.org/wiki/Remote_procedure_call
  7. https://pandaychen.github.io/2020/02/22/GRPC-METADATA-INTRO/
  8. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Trailer
  9. https://carlmastrangelo.com/blog/why-does-grpc-insist-on-trailers
  10. https://blog.csdn.net/luo1324574369/article/details/115221853

你可能感兴趣的:(distributed,网络,rpc,golang)