Golang 5分钟理解gRPC客户端源码

1,471 阅读4分钟

源码架构分析图

gRpc-Client.png

建立 gRPC 客户端连接

func main() {
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "HelloWorld"})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

client 端连接的建立主要包括以下三步

  1. 创建一个客户端连接 conn
  2. 通过一个 conn 创建一个客户端
  3. 发起 rpc 调用

创建一个客户端连接 conn

import "google.golang.org/grpc"

conn, err := grpc.Dial("host:port", grpc.WithInsecure())
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
    return DialContext(context.Background(), target, opts...)
}

实例化一个 ClientConn 的结构体

//ClientConn表示到概念端点的虚拟连接,以执行RPC。
//ClientConn可以根据配置、负载等自由地拥有零个或多个到端点的实际连接。
//它还可以自由地确定要使用的实际端点,并可以在每个RPC中更改它,从而允许客户端负载平衡。
//ClientConn封装了一系列功能,包括名称解析、TCP连接建立(带重试和退避)和TLS握手。
//它还通过重新解析名称并重新连接来处理已建立连接上的错误。
type ClientConn struct {
   ctx    context.Context
   cancel context.CancelFunc

   target       string
   parsedTarget resolver.Target
   authority    string
   dopts        dialOptions
   csMgr        *connectivityStateManager

   balancerBuildOpts balancer.BuildOptions
   blockingpicker    *pickerWrapper

   safeConfigSelector iresolver.SafeConfigSelector

   mu              sync.RWMutex
   resolverWrapper *ccResolverWrapper
   sc              *ServiceConfig
   conns           map[*addrConn]struct{}
   
   mkp             keepalive.ClientParameters
   curBalancerName string
   balancerWrapper *ccBalancerWrapper
   //...
}

connectivityStateManager

type connectivityStateManager struct {
	mu         sync.Mutex
	state      connectivity.State
	notifyChan chan struct{}
	channelzID int64
}

连接的状态管理器,每个连接具有 IDLECONNECTINGREADYTRANSIENT_FAILURESHUTDOW NInvalid-State”这几种状态。

pickerWrapper

type pickerWrapper struct {
   mu         sync.Mutex
   done       bool
   blockingCh chan struct{}
   picker     balancer.Picker
}

pickerWrapper 是对 balancer.Picker 的一层封装,balancer.Picker 其实是一个负载均衡器,它里面只有一个 Pick 方法,它返回一个 PickResult 结构,包含 SubConn 连接。

type Picker interface {
    Pick(info PickInfo) (PickResult, error)
}
// PickResult包含与为RPC选择的连接相关的信息。
type PickResult struct {
   SubConn SubConn
   Done func(DoneInfo)
}

client 发起一个rpc 调用之前,需要通过 balancer 去找到一个 serveraddressbalancerPicker 类返回一个 SubConnSubConn 里面包含了多个 serveraddress,假如返回的 SubConnREADY 状态,grpc 会发送 RPC 请求,否则则会阻塞,等待 UpdateState 这个方法更新连接的状态并且通过 picker 获取一个新的 SubConn 连接。

DialContext

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
   cc := &ClientConn{
      target:            target,
      csMgr:             &connectivityStateManager{},
      conns:             make(map[*addrConn]struct{}),
      dopts:             defaultDialOptions(),
      blockingpicker:    newPickerWrapper(),
      czData:            new(channelzData),
      firstResolveEvent: grpcsync.NewEvent(),
   }
   //...
   cc.balancerBuildOpts = balancer.BuildOptions{
      DialCreds:        credsClone,
      CredsBundle:      cc.dopts.copts.CredsBundle,
      Dialer:           cc.dopts.copts.Dialer,
      Authority:        cc.authority,
      CustomUserAgent:  cc.dopts.copts.UserAgent,
      ChannelzParentID: cc.channelzID,
      Target:           cc.parsedTarget,
   }

   // Build the resolver.
   rWrapper, err := newCCResolverWrapper(cc, resolverBuilder)
   if err != nil {
      return nil, fmt.Errorf("failed to build resolver: %v", err)
   }
   //...
   return cc, nil
}

可以看到通过 dialerresolver 来进行服务发现,这里后续再单独详细讲解。

通过一个 conn 创建一个客户端

c := pb.NewGreeterClient(conn)

这一步非常简单,其实是 pb 文件中生成的代码,就是创建一个 greeterClient 的客户端。

type greeterClient struct {
    cc *grpc.ClientConn
}
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
    return &greeterClient{cc}
}

发起 rpc 调用

前面在创建 Dialer 的时候,我们已经将请求的 target 解析成了 address。之后向指定 address 发起 rpc 请求。

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
   out := new(HelloReply)
   err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
   if err != nil {
      return nil, err
   }
   return out, nil
}

SayHello 方法是通过调用 Invoke 的方法去发起 rpc 调用, Invoke 方法如下:

func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error {
   opts = combine(cc.dopts.callOptions, opts)

   if cc.dopts.unaryInt != nil {
      return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
   }
   return invoke(ctx, method, args, reply, cc, opts...)
}
func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {
   cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
   if err != nil {
      return err
   }
   if err := cs.SendMsg(req); err != nil {
      return err
   }
   return cs.RecvMsg(reply)
}

SendMsg

func (cs *clientStream) SendMsg(m interface{}) (err error) {
	// ...
	op := func(a *csAttempt) error {
		err := a.sendMsg(m, hdr, payload, data)
		m, data = nil, nil
		return err
	}
	// ...
}

sendMsg

func (a *csAttempt) sendMsg(m interface{}, hdr, payld, data []byte) error {
		// ...
    if err := a.t.Write(a.s, hdr, payld, &transport.Options{Last: !cs.desc.ClientStreams}); err != nil {
        if !cs.desc.ClientStreams {
            // For non-client-streaming RPCs, we return nil instead of EOF on error
            // because the generated code requires it.  finish is not called; RecvMsg()
            // will call it with the stream's status independently.
            return nil
        }
        return io.EOF
    }
		// ...  
}

最终是通过 a.t.Write 发出的数据写操作,a.t 是一个 ClientTransport 类型,所以最终是通过 ClientTransport 这个结构体的 Write 方法发送数据

RecvMsg

func (cs *clientStream) RecvMsg(m interface{}) error {
  // ...
	err := cs.withRetry(func(a *csAttempt) error {
		return a.recvMsg(m, recvInfo)
	}, cs.commitAttemptLocked)
  // ...
}

recvMsg


func (a *csAttempt) recvMsg(m interface{}, payInfo *payloadInfo) (err error) {
	// ...
	err = recv(a.p, cs.codec, a.s, a.dc, m, *cs.callInfo.maxReceiveMessageSize, payInfo, a.decomp)
	// ...
	if a.statsHandler != nil {
		a.statsHandler.HandleRPC(cs.ctx, &stats.InPayload{
			Client:   true,
			RecvTime: time.Now(),
			Payload:  m,
			Data:       payInfo.uncompressedBytes,
			WireLength: payInfo.wireLength,
			Length:     len(payInfo.uncompressedBytes),
		})
	}
	// ...
}

recv

func recv(p *parser, c baseCodec, s *transport.Stream, dc Decompressor, m interface{}, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) error {
	d, err := recvAndDecompress(p, s, dc, maxReceiveMessageSize, payInfo, compressor)
	// ...
}

recvAndDecompress

func recvAndDecompress(p *parser, s *transport.Stream, dc Decompressor, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) ([]byte, error) {
	pf, d, err := p.recvMsg(maxReceiveMessageSize)
  // ...
}

recvMsg

func (p *parser) recvMsg(maxReceiveMessageSize int) (pf payloadFormat, msg []byte, err error) {
	if _, err := p.r.Read(p.header[:]); err != nil {
		return 0, nil, err
	}
  // ...  
}

最终还是调用了 p.r.Read 方法,p.r 是一个 io.Reader 接口, s *transport.Stream类型

附带 HTTP2 Client 写逻辑:

gprc-HTTP2-Client.png


欢迎指教,有疑问或者有其它感兴趣的学习方向可以在下方评论,我们将一一为您解答