grpc拦截器实践

介绍

grpc提供了拦截器,可以使用拦截器开发grpc中间件,实现各种中间功能。 比如: 日志采集,认证 等

pb文件

$ protoc -I ./ pb/*.proto --plugin=protoc-gen-go=protoc-gen-go --go_out=plugins=grpc:pb
syntax = "proto3";

option go_package = "./echo";

package echo;

// Request
message EchoRequest {
  string message = 1;
}

// Response
message EchoResponse {
  string message = 1;
}

// Echo is the echo service.
service Echo {
  // UnaryEcho is unary echo.
  rpc UnaryEcho(EchoRequest) returns (EchoResponse) {}

  // ServerStreamingEcho is server side streaming.
  rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {}

  // ClientStreamingEcho is client side streaming.
  rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {}

  // BidirectionalStreamingEcho is bidi streaming.
  rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {}

}

Server端

核心方法: grpc.NewServer(grpc.UnaryInterceptor(unaryInterceptor), grpc.StreamInterceptor(streamInterceptor))

unaryInterceptor

func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
	log.Println("[Server] 请求参数 = ", req)
	log.Println("[Server] 请求方法 = ", info.FullMethod)
	md, _ := metadata.FromIncomingContext(ctx)
	log.Println("[Server] context 上下文参数 = ", md)

	m, err := handler(ctx, req)
	log.Println("[Server] handler = ", m, err)
	if err != nil {
		log.Println("RPC failed with error %v", err)
	}
	return m, err
}

streamInterceptor

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

	log.Println("[Server] 发起了请求, streamInterceptor. ")
	md, _ := metadata.FromIncomingContext(ss.Context())
	log.Println("[Server] md = ", md)

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

// wrappedStream wraps around the embedded grpc.ServerStream, and intercepts the RecvMsg and
// SendMsg method call.
type wrappedStream struct {
	grpc.ServerStream
}

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

Client端

核心方法: conn, err := grpc.Dial("127.0.0.1:18881", grpc.WithInsecure(), grpc.WithUnaryInterceptor(unaryInterceptor), grpc.WithStreamInterceptor(streamInterceptor))

setCtx 设置上下文参数

// setCtx set context
func setCtx(kv map[string]string, grpcConn *grpc.ClientConn) context.Context {
	if grpcConn == nil {
		return nil
	}
	value := make([]string,0)
	for k, v := range kv {
		value = append(value, k)
		value = append(value, v)
	}
	return metadata.NewOutgoingContext(context.Background(), metadata.Pairs(value...))
}

unaryInterceptor

func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	log.Println("[Clinet] 请求方法 = ", method)
	log.Println("[Clinet] 请求参数 = ", req)
	log.Println("[Clinet] reply = ", reply)
	log.Println("[Clinet] 请求状态 = ",  cc.GetState())
     log.Println("[Clinet] 服务地址 = ", cc, cc.Target())

	// 给 ctx 设置参数
	ctx = setCtx(map[string]string{"a":"a", "b":"b", "c":"c"}, cc)

    // 请求时间记录  start, end
	start := time.Now()
	err := invoker(ctx, method, req, reply, cc, opts...)
	end := time.Now()

	// 打印日志
	log.Printf("RPC: %s, start time: %s, end time: %s, err: %v", method, start.Format("Basic"), end.Format(time.RFC3339), err)
	return err
}

cc.GetState()

为了向 gRPC API(即应用程序代码)的用户隐藏所有这些活动的详细信息,同时公开有关通道状态的有意义的信息,我们使用具有五个状态的状态机,定义如下:

		CONNECTING:通道正在尝试建立连接,并且正在等待名称解析、TCP 连接建立或 TLS 握手中涉及的步骤之一取得进展。这可以用作创建通道时的初始状态。

		READY:通道已通过 TLS 握手(或等效)和协议级(HTTP/2 等)握手成功建立了连接,并且所有后续通信尝试都已成功(或在没有任何已知故障的情况下处于挂起状态)。

		TRANSIENT_FAILURE:出现了一些暂时性故障(例如 TCP 3 次握手超时或套接字错误)。该状态的通道最终会切换到 CONNECTING 状态并尝试再次建立连接。由于重试是通过指数退避完成的,因此无法连接的通道一开始将在此状态下花费很少的时间,但随着尝试反复失败,通道将在此状态下花费越来越多的时间。对于许多非致命故障(例如,由于服务器尚不可用,TCP 连接尝试超时),通道可能会在此状态下花费越来越多的时间。

		IDLE:这是通道甚至没有尝试创建连接的状态,因为缺少新的或挂起的 RPC。在这种状态下可以创建新的 RPC。任何在通道上启动 RPC 的尝试都会将通道推出此状态以进行连接。如果在指定的 IDLE_TIMEOUT 内通道上没有 RPC 活动,即在此期间没有新的或未决(活动)的 RPC,则准备好或正在连接的通道将切换到 IDLE。此外,当没有活动或挂起的 RPC 时接收 GOAWAY 的通道也应该切换到 IDLE 以避免在尝试脱落连接的服务器上的连接过载。我们将使用 300 秒(5 分钟)的默认 IDLE_TIMEOUT。

		SHUTDOWN:此频道已开始关闭。任何新的 RPC 都应该立即失败。挂起的 RPC 可能会继续运行,直到应用程序取消它们。通道可能会进入此状态,因为应用程序明确请求关闭,或者在尝试连接通信期间发生不可恢复的错误。(截至 2015612 日,不存在归类为不可恢复的已知错误(连接或通信时)。)进入此状态的通道永远不会离开此状态。
	

以下方法可以获取客户端地址

// 获取客户端地址
func getClietIP(ctx context.Context) (string, error) {
	pr, ok := peer.FromContext(ctx)
	if !ok {
		return "", fmt.Errorf("invoke FromContext() failed")
	}
	if pr.Addr == net.Addr(nil) {
		return "", fmt.Errorf("peer.Addr is nil")
	}
	return pr.Addr.String(), nil
}

完整实例代码地址:

https://github.com/mangenotwork/man/tree/master/core/grpc_1

你可能感兴趣的:(golang,实例,grpc,golang)