最近在学习 rpc 相关的知识,如果让我去从头设计一个 rpc,我从使用者的角度出发,究竟需要去做一下什么工作?

第一,RPC 本质上就是一个远程调用,那肯定就需要通过网络来传输数据。虽然传输协议可以有多种选择,但考虑到可靠性的话,我们一般默认采用 TCP 协议。为了屏蔽网络传输的复杂性,我们需要封装一个单独的数据传输模块用来收发二进制数据,这个单独模块我们可以叫做传输模块。

第二,用户请求的时候是基于方法调用,方法出入参数都是对象数据,对象是肯定没法直接在网络中传输的,我们需要提前把它转成可传输的二进制,这就是我们说的序列化过程。但只是把方法调用参数的二进制数据传输到服务提供方是不够的,我们需要在方法调用参数的二进制数据后面增加“断句”符号来分隔出不同的请求,在两个“断句”符号中间放的内容就是我们请求的二进制数据,这个过程我们叫做协议封装。

第三,为了传输性能的进一步提高,我们还可以在协议模块中加入压缩功能,这是因为压缩过程也是对传输的二进制数据进行操作。在实际的网络传输过程中,我们的请求数据包在数据链路层可能会因为太大而被拆分成多个数据包进行传输,为了减少被拆分的次数,从而导致整个传输过程时间太长的问题,这就是数据压缩和解压。

第四,在针对服务提供方不断增多的情况,会出现一个接口对应多个IP+端口的服务地址映射关系,但这多个服务提供者对于我们的调用方来说是透明的,所以在 RPC 里面我们还需要给调用方找到所有的服务提供方,并需要在 RPC 里面维护好接口跟服务提供者地址的关系,这样调用方在发起请求的时候才能快速地找到对应的接收地址,这就是我们常说的服务发现。

第五,作为一个成熟的 rpc 框架,还需要给开发者提供各种各样能够深入介入 rpc 框架内部的接口调用。这里可以看看 grpc 各种各样的 wrapper 封装,在传输,编码,协议,服务发现,日志打印都留有标准统一的对外接口,让开发人员可以自定义开发。

在考虑完以上问题后,我对 grpc 的实现比较好奇,于是搭建了 client 和 service 想从代码的维度上去查看一下 grpc 在 rpc 上是怎么实现的?

client代码

package main

import (
"context"
"fmt"
"log"
"time" "grpc-test/client/service/pbservice" "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) func main() {
// 1. 新建连接,端口是服务端开放的8082端口
// 并且添加grpc.WithInsecure(),不然没有证书会报错
conn, err := grpc.Dial(":8082", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
} // 退出时关闭链接
defer conn.Close() // 2. 调用Product.pb.go中的NewProdServiceClient方法
productServiceClient := pbservice.NewProdServiceClient(conn) // 设置了超时 15s
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(10 * time.Second)))
resp, err := productServiceClient.GetProductStock(ctx, &pbservice.ProductRequest{ProdId: 233})
defer cancel()
if err != nil {
state, ok := status.FromError(err)
if ok && state.Code() == codes.DeadlineExceeded {
log.Fatalln("client.Search err: deadline")
}
log.Fatal("调用gRPC方法错误: ", err)
} fmt.Println("调用gRPC方法成功,ProdStock = ", resp.ProdStock)
}

service代码

package main

import (
"google.golang.org/grpc"
"grpc-test/server/pbservice/product"
"net"
"log"
) func main() {
rpcServer := grpc.NewServer() //将新建的ProdService注册进去,注意这里的 RegisterProdServiceServer 方法是将新生成的 service 中的方法
product.RegisterProdServiceServer(rpcServer, new(product.ProdService)) listener, err := net.Listen("tcp", ":8082")
if err != nil {
log.Fatal("服务监听端口失败", err)
} // 运行rpcServer,传入listener
_ = rpcServer.Serve(listener)
}

接下来我们追踪一下 client 部分的代码:

# 创建连接
conn, err := grpc.Dial(":8082", grpc.WithInsecure())
if err != nil {
log.Fatal(err)
} # 具体的初始化操作在grpc.DialContext方法中。
# DialContext首先初始化空对象ClientConn,然后判断opts …DialOption数据是否存在,如果存在就执行传入的函数并设置特定属性。
// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
return DialContext(context.Background(), target, opts...)
}

在 clientconn.go 中创建ClientConn对象,对应结构体包括的字段

// ClientConn represents a virtual connection to a conceptual endpoint, to
// perform RPCs.
//
// A ClientConn is free to have zero or more actual connections to the endpoint
// based on configuration, load, etc. It is also free to determine which actual
// endpoints to use and may change it every RPC, permitting client-side load
// balancing.
//
// A ClientConn encapsulates a range of functionality including name
// resolution, TCP connection establishment (with retries and backoff) and TLS
// handshakes. It also handles errors on established connections by
// re-resolving the name and reconnecting.
type ClientConn struct {
ctx context.Context
cancel context.CancelFunc target string
parsedTarget resolver.target // 负载均衡选择
authority string
dopts dialOptions // 初始化可设置选项,在每一次请求会带上,看call.go中的combine方法
csMgr *connectivityStateManager // 连接状态维护 balancerBuildOpts balancer.BuildOptions
blockingpicker *pickerWrapper // 负载均衡设置 mu sync.RWMutex
resolverWrapper *ccResolverWrapper // 实现了resolver.ClientConn,位于./resolver/resolver.go中,ClientConn的上层包装器
sc *ServiceConfig
conns map[*addrConn]struct{} // 存放连接的地方
// Keepalive parameter can be updated if a GoAway is received.
mkp keepalive.ClientParameters
curBalancerName string
balancerWrapper *ccBalancerWrapper // 负载均衡器上的包装器
retryThrottler atomic.Value firstResolveEvent *grpcsync.Event channelzID int64 // channelz unique identification number
czData *channelzData
}

当pb自动生成的代码中,调用方法的时候,grpc 会将其请求代理替换成自己封装的网络层上请求,使用连接的 Invoke() 方法

func (c *prodServiceClient) GetProductStock(ctx context.Context, in *ProductRequest, opts ...grpc.CallOption) (*ProductResponse, error) {
out := new(ProductResponse)
err := c.cc.Invoke(ctx, "/service.ProdService/GetProductStock", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}

在 call.go 内部的 invoke() 方法中,grpc 主要做了三件事情:建立获取连接(newClientStream函数),发送数据(SendMSg()函数),接收数据(RecvMsg()函数)

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)
}

上面的还是比较容易看懂的,但是深入到内部传输层的处理的时候会发现 grpc 很多晦涩难懂的逻辑,只能强忍着往下看了

newClientStream分析

具体的流程为:

1、运行传入的opts,在请求前调用 before()

2、设置压缩算法 compressor

3、设置请求的 ctx,包括链路追中 trace 还有编码,压缩等等信息

4、生成 clientStream,负责从流中发送数据

5、发送请求生成 newAttemptLocked 和 withRetry

6、处理 ctx 监听上下文是否关闭

func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) {
// channelz 主要用于保存不同 groutine 公用的 channel 数据读取和写入,避免竞态
if channelz.IsOn() {
cc.incrCallsStarted()
defer func() {
if err != nil {
cc.incrCallsFailed()
}
}()
} // 可以设置 callInfo,在请求的时候掌控 stream流 和 codec编码等等
c := defaultCallInfo() // Provide an opportunity for the first RPC to see the first service config
// provided by the resolver.
if err := cc.waitForResolvedAddrs(ctx); err != nil {
return nil, err
}
mc := cc.GetMethodConfig(method)
if mc.WaitForReady != nil {
c.failFast = !*mc.WaitForReady
} // ... 省略 // 这里根据传入的 opts, 可以在请求前运行 CallOption 的埋点设置,让开发者可以介入
for _, o := range opts {
if err := o.before(c); err != nil {
return nil, toRPCErr(err)
}
}
c.maxSendMessageSize = getMaxSize(mc.MaxReqSize, c.maxSendMessageSize, defaultClientMaxSendMessageSize)
c.maxReceiveMessageSize = getMaxSize(mc.MaxRespSize, c.maxReceiveMessageSize, defaultClientMaxReceiveMessageSize)
if err := setCallInfoCodec(c); err != nil {
return nil, err
} callHdr := &transport.CallHdr{
Host: cc.authority,
Method: method,
ContentSubtype: c.contentSubtype,
} // 这里可以设置自定义的解压缩 CallOption,这里只要 c.compressorType 声明 grpc 支持的压缩格式就行了,比如 gzip
// 当然你也可以自己声明相关的解压,具体可以参考 encoding/encoding.go 中,自行注册相关的压缩支持
// Set our outgoing compression according to the UseCompressor CallOption, if
// set. In that case, also find the compressor from the encoding package.
// Otherwise, use the compressor configured by the WithCompressor DialOption,
// if set.
var cp Compressor
var comp encoding.Compressor
if ct := c.compressorType; ct != "" {
callHdr.SendCompress = ct
if ct != encoding.Identity {
comp = encoding.GetCompressor(ct)
if comp == nil {
return nil, status.Errorf(codes.Internal, "grpc: Compressor is not installed for requested grpc-encoding %q", ct)
}
}
} else if cc.dopts.cp != nil {
callHdr.SendCompress = cc.dopts.cp.Type()
cp = cc.dopts.cp
}
if c.creds != nil {
callHdr.Creds = c.creds
} // grpc 如果开启了 trace 跟踪,会生成的相关 context,并将它包装到 ctx 内,透传到请求服务
var trInfo *traceInfo
if EnableTracing {
trInfo = &traceInfo{
tr: trace.New("grpc.Sent."+methodFamily(method), method),
firstLine: firstLine{
client: true,
},
}
if deadline, ok := ctx.Deadline(); ok {
trInfo.firstLine.deadline = time.Until(deadline)
}
trInfo.tr.LazyLog(&trInfo.firstLine, false)
ctx = trace.NewContext(ctx, trInfo.tr)
} // rpc 请求的 ctx 会包含:快速失败标志-failFast,编码-codec,解压缩-cp和comp
ctx = newContextWithRPCInfo(ctx, c.failFast, c.codec, cp, comp)
// ... 省略 cs := &clientStream{
callHdr: callHdr,
ctx: ctx,
methodConfig: &mc,
opts: opts,
callInfo: c,
cc: cc,
desc: desc,
codec: c.codec,
cp: cp,
comp: comp,
cancel: cancel,
beginTime: beginTime,
firstAttempt: true,
}
// 设置答应的日志
// ...省略
cs.binlog = binarylog.GetMethodLogger(method) // 请求的具体过程:newAttemptLocked 和 withRetry
cs.callInfo.stream = cs
// Only this initial attempt has stats/tracing.
// TODO(dfawley): move to newAttempt when per-attempt stats are implemented.
if err := cs.newAttemptLocked(sh, trInfo); err != nil {
cs.finish(err)
return nil, err
} op := func(a *csAttempt) error { return a.newStream() }
if err := cs.withRetry(op, func() { cs.bufferForRetryLocked(0, op) }); err != nil {
cs.finish(err)
return nil, err
} // ...省略
// 这里监听连接中 ctx 上下文是否关闭,如果关闭,需要 clientStream 去 finish() 关闭这个链接
if desc != unaryStreamDesc {
// Listen on cc and stream contexts to cleanup when the user closes the
// ClientConn or cancels the stream context. In all other cases, an error
// should already be injected into the recv buffer by the transport, which
// the client will eventually receive, and then we will cancel the stream's
// context in clientStream.finish.
go func() {
select {
case <-cc.ctx.Done():
cs.finish(ErrClientConnClosing)
case <-ctx.Done():
cs.finish(toRPCErr(ctx.Err()))
}
}()
}
return cs, nil
}

grpc 中 callInfo 请求参数结构体为:

// callInfo contains all related configuration and information about an RPC.
type callInfo struct {
compressorType string
failFast bool
stream ClientStream
maxReceiveMessageSize *int
maxSendMessageSize *int
creds credentials.PerRPCCredentials
contentSubtype string
codec baseCodec
maxRetryRPCBufferSize int
}

获取连接 transport 函数主要为 clientStream.newAttemptLoacked 来进行处理:

// newAttemptLocked creates a new attempt with a transport.
// If it succeeds, then it replaces clientStream's attempt with this new attempt.
func (cs *clientStream) newAttemptLocked(sh stats.Handler, trInfo *traceInfo) (retErr error) {
// newAttempt 就是连接结构体
newAttempt := &csAttempt{
cs: cs,
dc: cs.cc.dopts.dc,
statsHandler: sh,
trInfo: trInfo,
}
defer func() {
if retErr != nil {
// This attempt is not set in the clientStream, so it's finish won't
// be called. Call it here for stats and trace in case they are not
// nil.
newAttempt.finish(retErr)
}
}() if err := cs.ctx.Err(); err != nil {
return toRPCErr(err)
} // 从 clientconn 内获取一条可用的传输连接
t, done, err := cs.cc.getTransport(cs.ctx, cs.callInfo.failFast, cs.callHdr.Method)
if err != nil {
return err
}
if trInfo != nil {
trInfo.firstLine.SetRemoteAddr(t.RemoteAddr())
}
newAttempt.t = t
newAttempt.done = done
cs.attempt = newAttempt
return nil
}

在 getTransport 中,具体提供的连接为 pick 方法,根据 picker_wrapper.go 中获取的,在看到 wrapper 你应该知道这个是可以开发者自行定义的,实际上在负载均衡算法中,确实可以自定义选择不同的连接,我们来看看 pick 内具体是做什么的?

pick在设计的时候是阻塞的方式的来调用的,cc.blockingpicker.pick()

func (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) {
t, done, err := cc.blockingpicker.pick(ctx, failfast, balancer.PickInfo{
Ctx: ctx,
FullMethodName: method,
})
if err != nil {
return nil, nil, toRPCErr(err)
}
return t, done, nil
} // 在以下情况下可能会阻塞:
// - 没有选择器
// - 当前选择器返回 ErrNoSubConnAvailable
// - 当前选择器返回其他错误并且 failfast 为 false。
// - 当前选择器返回的 subConn 不是 READY
// pick returns the transport that will be used for the RPC.
// It may block in the following cases:
// - there's no picker
// - the current picker returns ErrNoSubConnAvailable
// - the current picker returns other errors and failfast is false.
// - the subConn returned by the current picker is not READY
// When one of these situations happens, pick blocks until the picker gets updated.
func (pw *pickerWrapper) pick(ctx context.Context, failfast bool, info balancer.PickInfo) (transport.ClientTransport, func(balancer.DoneInfo), error) {
var ch chan struct{} var lastPickErr error // 开启循环,直到能从 balance 连接池中获取符合要求的连接
for {
pw.mu.Lock()
if pw.done {
pw.mu.Unlock()
return nil, nil, ErrClientConnClosing
} if pw.picker == nil {
ch = pw.blockingCh
}
if ch == pw.blockingCh {
// 为了避免 pick 本身调用会引起竞态,这里也要用信道来控制了pick的调用
// pw.blockingCh 为控制只有一个协程获取到执行权
// 如果 pw.picker is nil 或者 有其他的协程在调用pick方法都会引起阻塞 // This could happen when either:
// - pw.picker is nil (the previous if condition), or
// - has called pick on the current picker.
pw.mu.Unlock()
select {
case <-ctx.Done():
var errStr string
if lastPickErr != nil {
errStr = "latest balancer error: " + lastPickErr.Error()
} else if connectionErr := pw.connectionError(); connectionErr != nil {
errStr = "latest connection error: " + connectionErr.Error()
} else {
errStr = ctx.Err().Error()
}
switch ctx.Err() {
case context.DeadlineExceeded:
return nil, nil, status.Error(codes.DeadlineExceeded, errStr)
case context.Canceled:
return nil, nil, status.Error(codes.Canceled, errStr)
}
case <-ch:
}
continue
} ch = pw.blockingCh
p := pw.picker
pw.mu.Unlock() // 调用 pickerWrapper 的 Pick() 方法获取有用的一个连接
pickResult, err := p.Pick(info)
// 错误处理
if err != nil {
// ... 省略
} // 这里 SubConn 是获取了一个连接,但是这个连接可能还没 READY,对于准备好的连接才会返回
acw, ok := pickResult.SubConn.(*acBalancerWrapper)
if !ok {
grpclog.Error("subconn returned from pick is not *acBalancerWrapper")
continue
}
if t, ok := acw.getAddrConn().getReadyTransport(); ok {
if channelz.IsOn() {
return t, doneChannelzWrapper(acw, pickResult.Done), nil
}
return t, pickResult.Done, nil
}
if pickResult.Done != nil {
// Calling done with nil error, no bytes sent and no bytes received.
// DoneInfo with default value works.
pickResult.Done(balancer.DoneInfo{})
}
grpclog.Infof("blockingPicker: the picked transport is not ready, loop back to repick")
// If ok == false, ac.state is not READY.
// A valid picker always returns READY subConn. This means the state of ac
// just changed, and picker will be updated shortly.
// continue back to the beginning of the for loop to repick.
}
}

在这里会有个疑问为啥 pick 调用后获取的连接还要根据 acw.getAddrConn().getReadyTransport() 去判断是否 ready ?其实这个问题细想一下就知道了。pick方法返回的只是一组(ip+端口)的服务可用连接,但是这个时候是没办法判断是否可用的。对于服务的上线和下线,那么有可能从 ip 和端口还没从连接池中摘除,那么判断是否可用的逻辑应该交给外部的 balace_wrapper 来做,所以用 SubConn 来进行解耦。从信道中获取可用的 address

type SubConn interface {
// UpdateAddresses updates the addresses used in this SubConn.
// gRPC checks if currently-connected address is still in the new list.
// If it's in the list, the connection will be kept.
// If it's not in the list, the connection will gracefully closed, and
// a new connection will be created.
//
// This will trigger a state transition for the SubConn.
UpdateAddresses([]resolver.Address)
// Connect starts the connecting for this SubConn.
Connect()
}

在获取到可用的 transport 其实就是一个网络接口句柄,关于句柄一般都是 Close(),Write() 等等这些方法

// ClientTransport is the common interface for all gRPC client-side transport
// implementations.
type ClientTransport interface {
// Close tears down this transport. Once it returns, the transport
// should not be accessed any more. The caller must make sure this
// is called only once.
Close() error // GracefulClose starts to tear down the transport: the transport will stop
// accepting new RPCs and NewStream will return error. Once all streams are
// finished, the transport will close.
//
// It does not block.
GracefulClose() // Write sends the data for the given stream. A nil stream indicates
// the write is to be performed on the transport as a whole.
Write(s *Stream, hdr []byte, data []byte, opts *Options) error // NewStream creates a Stream for an RPC.
NewStream(ctx context.Context, callHdr *CallHdr) (*Stream, error) // CloseStream clears the footprint of a stream when the stream is
// not needed any more. The err indicates the error incurred when
// CloseStream is called. Must be called when a stream is finished
// unless the associated transport is closing.
CloseStream(stream *Stream, err error) // Error returns a channel that is closed when some I/O error
// happens. Typically the caller should have a goroutine to monitor
// this in order to take action (e.g., close the current transport
// and create a new one) in error case. It should not return nil
// once the transport is initiated.
Error() <-chan struct{} // GoAway returns a channel that is closed when ClientTransport
// receives the draining signal from the server (e.g., GOAWAY frame in
// HTTP/2).
GoAway() <-chan struct{} // GetGoAwayReason returns the reason why GoAway frame was received.
GetGoAwayReason() GoAwayReason // RemoteAddr returns the remote network address.
RemoteAddr() net.Addr // IncrMsgSent increments the number of message sent through this transport.
IncrMsgSent() // IncrMsgRecv increments the number of message received through this transport.
IncrMsgRecv()
}

获取到连接后,判断这个连接是不是已经建立了连接了,如果还是空闲的 idle 那么直接 connect()  一下

func (ac *addrConn) getReadyTransport() (transport.ClientTransport, bool) {
ac.mu.Lock()
if ac.state == connectivity.Ready && ac.transport != nil {
t := ac.transport
ac.mu.Unlock()
return t, true
}
var idle bool
if ac.state == connectivity.Idle {
idle = true
}
ac.mu.Unlock()
// Trigger idle ac to connect.
if idle {
ac.connect()
}
return nil, false
}

SendMsg分析

上面就是整个 newClientStream 的过程了。那么发送过程中 grpc 又是怎么做的呢?

func (cs *clientStream) SendMsg(m interface{}) (err error) {
defer func() {
if err != nil && err != io.EOF {
// Call finish on the client stream for errors generated by this SendMsg
// call, as these indicate problems created by this client. (Transport
// errors are converted to an io.EOF error in csAttempt.sendMsg; the real
// error will be returned from RecvMsg eventually in that case, or be
// retried.)
cs.finish(err)
}
}() // 使用重试的方式 withRetry() 来调用发送 sendMsg()
msgBytes := data // Store the pointer before setting to nil. For binary logging.
op := func(a *csAttempt) error {
err := a.sendMsg(m, hdr, payload, data)
// nil out the message and uncomp when replaying; they are only needed for
// stats which is disabled for subsequent attempts.
m, data = nil, nil
return err
}
err = cs.withRetry(op, func() { cs.bufferForRetryLocked(len(hdr)+len(payload), op) })
if cs.binlog != nil && err == nil {
cs.binlog.Log(&binarylog.ClientMessage{
OnClientSide: true,
Message: msgBytes,
})
}
return
}

在 sendMsg() 函数中其实非常好理解,就是拿 clientStream 已经初始化获取到的 transport 连接,调用 write() 方法,在网络句柄上将数据发送出去。

// 先编码和组装协议头和协议data
// prepareMsg returns the hdr, payload and data
// using the compressors passed or using the
// passed preparedmsg
func prepareMsg(m interface{}, codec baseCodec, cp Compressor, comp encoding.Compressor) (hdr, payload, data []byte, err error) {
if preparedMsg, ok := m.(*PreparedMsg); ok {
return preparedMsg.hdr, preparedMsg.payload, preparedMsg.encodedData, nil
}
// The input interface is not a prepared msg.
// Marshal and Compress the data at this point
data, err = encode(codec, m)
if err != nil {
return nil, nil, nil, err
}
compData, err := compress(data, cp, comp)
if err != nil {
return nil, nil, nil, err
}
hdr, payload = msgHeader(data, compData)
return hdr, payload, data, nil
} // 再进行发送
func (a *csAttempt) sendMsg(m interface{}, hdr, payld, data []byte) error {
cs := a.cs
if a.trInfo != nil {
a.mu.Lock()
if a.trInfo.tr != nil {
a.trInfo.tr.LazyLog(&payload{sent: true, msg: m}, true)
}
a.mu.Unlock()
}
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
}
if a.statsHandler != nil {
a.statsHandler.HandleRPC(cs.ctx, outPayload(true, m, data, payld, time.Now()))
}
if channelz.IsOn() {
a.t.IncrMsgSent()
}
return nil
}

接着的 withRetry() 方法调用流程比较长,其实也就是上面提到的 connect(),根据连接尝试连接

func (cs *clientStream) withRetry(op func(a *csAttempt) error, onSuccess func()) error {
for {
// retryLocked很重要,到底做什么呢?
if err := cs.retryLocked(err); err != nil {
cs.mu.Unlock()
return err
}
}
} func (cs *clientStream) retryLocked(lastErr error) error {
for {
// 看newAttemptLocked
if err := cs.newAttemptLocked(nil, nil); err != nil {
return err
}
}
} func (cs *clientStream) newAttemptLocked(sh stats.Handler, trInfo *traceInfo) (retErr error) {
newAttempt := &csAttempt{
cs: cs,
dc: cs.cc.dopts.dc,
statsHandler: sh,
trInfo: trInfo,
}
// 每次getTransport获取使用的连接,cc是ClientConn对象,涉及负载均衡了。
t, done, err := cs.cc.getTransport(ctx, cs.callInfo.failFast, cs.callHdr.Method)
if err != nil {
return err
}
if trInfo != nil {
trInfo.firstLine.SetRemoteAddr(t.RemoteAddr())
}
newAttempt.t = t
newAttempt.done = done
cs.attempt = newAttempt
return nil
} func (cc *ClientConn) getTransport(ctx context.Context, failfast bool, method string) (transport.ClientTransport, func(balancer.DoneInfo), error) {
// 关注pick方法
t, done, err := cc.blockingpicker.pick(ctx, failfast, balancer.PickInfo{
Ctx: ctx,
FullMethodName: method,
})
} func (pw *pickerWrapper) pick(ctx context.Context, failfast bool, info balancer.PickInfo) (transport.ClientTransport, func(balancer.DoneInfo), error) {
// 选择满足条件的transport
if t, ok := acw.getAddrConn().getReadyTransport(); ok {
if channelz.IsOn() {
return t, doneChannelzWrapper(acw, pickResult.Done), nil
}
return t, pickResult.Done, nil
}
}
} func (ac *addrConn) getReadyTransport() (transport.ClientTransport, bool) {
// 创建连接
ac.connect()
return nil, false
} func (ac *addrConn) connect() error {
// 异步连接
go ac.resetTransport()
return nil
} func (ac *addrConn) resetTransport() {
for i := 0; ; i++ {
// 创建连接,如果有一个创建成功,返回
newTr, addr, reconnect, err := ac.tryAllAddrs(addrs, connectDeadline)
}
} func (ac *addrConn) createTransport(addr resolver.Address, copts transport.ConnectOptions, connectDeadline time.Time) (transport.ClientTransport, *grpcsync.Event, error) {
// NewClientTransport创建
newTr, err := transport.NewClientTransport(connectCtx, ac.cc.ctx, addr, copts, onPrefaceReceipt, onGoAway, onClose)
} func NewClientTransport(connectCtx, ctx context.Context, addr resolver.Address, opts ConnectOptions, onPrefaceReceipt func(), onGoAway func(GoAwayReason), onClose func()) (ClientTransport, error) {
return newHTTP2Client(connectCtx, ctx, addr, opts, onPrefaceReceipt, onGoAway, onClose)
} // 最关键的方法来了,太多的细节,在这里关注如何接受incoming消息
func newHTTP2Client(connectCtx, ctx context.Context, addr resolver.Address, opts ConnectOptions, onPrefaceReceipt func(), onGoAway func(GoAwayReason), onClose func()) (_ *http2Client, err error) {
// Start the reader goroutine for incoming message. Each transport has
// a dedicated goroutine which reads HTTP2 frame from network. Then it
// dispatches the frame to the corresponding stream entity.
go t.reader()
} // 处理http2数据和server端对应。
func (t *http2Client) reader() {
defer close(t.readerDone)
// Check the validity of server preface.
frame, err := t.framer.fr.ReadFrame()
if err != nil {
t.Close() // this kicks off resetTransport, so must be last before return
return
}
t.conn.SetReadDeadline(time.Time{}) // reset deadline once we get the settings frame (we didn't time out, yay!)
if t.keepaliveEnabled {
atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
}
sf, ok := frame.(*http2.SettingsFrame)
if !ok {
t.Close() // this kicks off resetTransport, so must be last before return
return
}
t.onPrefaceReceipt()
t.handleSettings(sf, true) // loop to keep reading incoming messages on this transport.
for {
t.controlBuf.throttle()
frame, err := t.framer.fr.ReadFrame()
if t.keepaliveEnabled {
atomic.StoreInt64(&t.lastRead, time.Now().UnixNano())
}
if err != nil {
// Abort an active stream if the http2.Framer returns a
// http2.StreamError. This can happen only if the server's response
// is malformed http2.
if se, ok := err.(http2.StreamError); ok {
t.mu.Lock()
s := t.activeStreams[se.StreamID]
t.mu.Unlock()
if s != nil {
// use error detail to provide better err message
code := http2ErrConvTab[se.Code]
errorDetail := t.framer.fr.ErrorDetail()
var msg string
if errorDetail != nil {
msg = errorDetail.Error()
} else {
msg = "received invalid frame"
}
t.closeStream(s, status.Error(code, msg), true, http2.ErrCodeProtocol, status.New(code, msg), nil, false)
}
continue
} else {
// Transport error.
t.Close()
return
}
}
switch frame := frame.(type) {
case *http2.MetaHeadersFrame:
t.operateHeaders(frame)
case *http2.DataFrame:
t.handleData(frame)
case *http2.RSTStreamFrame:
t.handleRSTStream(frame)
case *http2.SettingsFrame:
t.handleSettings(frame, false)
case *http2.PingFrame:
t.handlePing(frame)
case *http2.GoAwayFrame:
t.handleGoAway(frame)
case *http2.WindowUpdateFrame:
t.handleWindowUpdate(frame)
default:
if logger.V(logLevel) {
logger.Errorf("transport: http2Client.reader got unhandled frame type %v.", frame)
}
}
}
}

这里梳理一下调用流程就是:

开始 ==> withRetry ==> retryLocked ==> newAttemptLocked ==> getTransport ==> pick ==> getReadyTransport ==> connect ==> resetTransport ==> createTransport ==> NewClientTransport ==> newHTTP2Client ==> reader ==> 结束

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) {
cs := a.cs
if a.statsHandler != nil && payInfo == nil {
payInfo = &payloadInfo{}
} if !a.decompSet {
// Block until we receive headers containing received message encoding.
if ct := a.s.RecvCompress(); ct != "" && ct != encoding.Identity {
if a.dc == nil || a.dc.Type() != ct {
// No configured decompressor, or it does not match the incoming
// message encoding; attempt to find a registered compressor that does.
a.dc = nil
a.decomp = encoding.GetCompressor(ct)
}
} else {
// No compression is used; disable our decompressor.
a.dc = nil
}
// Only initialize this state once per stream.
a.decompSet = true
} // recv 接收数据 payInfo,未解码的数据放到了结构体的 []byte 中
err = recv(a.p, cs.codec, a.s, a.dc, m, *cs.callInfo.maxReceiveMessageSize, payInfo, a.decomp)
if err != nil {
if err == io.EOF {
if statusErr := a.s.Status().Err(); statusErr != nil {
return statusErr
}
return io.EOF // indicates successful end of stream.
}
return toRPCErr(err)
}
if a.trInfo != nil {
a.mu.Lock()
if a.trInfo.tr != nil {
a.trInfo.tr.LazyLog(&payload{sent: false, msg: m}, true)
}
a.mu.Unlock()
} // handler 是处理传输层获取到的二进制数据,HandleRPC 其实就是rpc的接收到数据后的处理方法
if a.statsHandler != nil {
a.statsHandler.HandleRPC(cs.ctx, &stats.InPayload{
Client: true,
RecvTime: time.Now(),
Payload: m,
// TODO truncate large payload.
Data: payInfo.uncompressedBytes,
WireLength: payInfo.wireLength,
Length: len(payInfo.uncompressedBytes),
})
}
if channelz.IsOn() {
a.t.IncrMsgRecv()
}
// 如果这里是服务流模式的rpc,则直接返回了
if cs.desc.ServerStreams {
// Subsequent messages should be received by subsequent RecvMsg calls.
return nil
} // 这里对于非服务流模式的,还需要处理 nil 和 EOF,EOF表示此次接收完成,否则捕捉返回的 err
// Special handling for non-server-stream rpcs.
// This recv expects EOF or errors, so we don't collect inPayload.
err = recv(a.p, cs.codec, a.s, a.dc, m, *cs.callInfo.maxReceiveMessageSize, nil, a.decomp)
if err == nil {
return toRPCErr(errors.New("grpc: client streaming protocol violation: get <nil>, want <EOF>"))
}
if err == io.EOF {
return a.s.Status().Err() // non-server streaming Recv returns nil on success
}
return toRPCErr(err)
}

在对 transport 句柄中读取的二进制数据主要是这样获取的

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)
} func recvAndDecompress(p *parser, s *transport.Stream, dc Decompressor, maxReceiveMessageSize int, payInfo *payloadInfo, compressor encoding.Compressor) ([]byte, error) {
// 接收
pf, d, err := p.recvMsg(maxReceiveMessageSize)
} // 从stream中读出完整的gRPC消息,返回消息和payload形式,调用者管理返回的消息内存。
// 如果存在错误,可能的错误是,
// io.EOF,当没有消息的时候
// io.ErrUnexpectedEOF
// of type transport.ConnectionError
// 或者status包中定义的错误。
func (p *parser) recvMsg(maxReceiveMessageSize int) (pf payloadFormat, msg []byte, err error) {
// 请求header头
if _, err := p.r.Read(p.header[:]); err != nil {
return 0, nil, err
} pf = payloadFormat(p.header[0])
// 获取消息体的长度
length := binary.BigEndian.Uint32(p.header[1:]) if length == 0 {
return pf, nil, nil
}
if int64(length) > int64(maxInt) {
return 0, nil, status.Errorf(codes.ResourceExhausted, "grpc: received message larger than max length allowed on current machine (%d vs. %d)", length, maxInt)
}
if int(length) > maxReceiveMessageSize {
return 0, nil, status.Errorf(codes.ResourceExhausted, "grpc: received message larger than max (%d vs. %d)", length, maxReceiveMessageSize)
}
// TODO(bradfitz,zhaoq): garbage. reuse buffer after proto decoding instead
// of making it for each message:
msg = make([]byte, int(length))
if _, err := p.r.Read(msg); err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return 0, nil, err
}
return pf, msg, nil
}

至此我们大概可以了解到当一个 rpc 请求的时候,一个 rpc 框架都做了哪些工作。对于 grpc 来说,它的设计侧重点主要是为了通用性,在跨语言的调用中,如何保证调用不发生错乱,所以可以看到它更多的是在传输层上设计了pb编码和压缩,在连接pick的时候怎么在竞态条件下做到不错不乱,并且兼顾一定的并发性能要求。除此之外还开放了需要rpc内核细节的介入接口,方便开发者自定义相关的rpc流程操作,比如负载均衡,编解码,压缩等等。

浅浅的源码剖析grpc-go(一)的更多相关文章

  1. jQuery之Deferred源码剖析

    一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...

  2. Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

    声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...

  3. Apache Spark源码剖析

    Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著   ISBN 978-7-121-25420- ...

  4. 基于mybatis-generator-core 1.3.5项目的修订版以及源码剖析

    项目简单说明 mybatis-generator,是根据数据库表.字段反向生成实体类等代码文件.我在国庆时候,没事剖析了mybatis-generator-core源码,写了相当详细的中文注释,可以去 ...

  5. STL"源码"剖析-重点知识总结

    STL是C++重要的组件之一,大学时看过<STL源码剖析>这本书,这几天复习了一下,总结出以下LZ认为比较重要的知识点,内容有点略多 :) 1.STL概述 STL提供六大组件,彼此可以组合 ...

  6. SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现

    SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...

  7. 自己实现多线程的socket,socketserver源码剖析

    1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...

  8. Java多线程9:ThreadLocal源码剖析

    ThreadLocal源码剖析 ThreadLocal其实比较简单,因为类里就三个public方法:set(T value).get().remove().先剖析源码清楚地知道ThreadLocal是 ...

  9. JS魔法堂:mmDeferred源码剖析

    一.前言 avalon.js的影响力愈发强劲,而作为子模块之一的mmDeferred必然成为异步调用模式学习之旅的又一站呢!本文将记录我对mmDeferred的认识,若有纰漏请各位指正,谢谢.项目请见 ...

  10. Node 进阶:express 默认日志组件 morgan 从入门使用到源码剖析

    本文摘录自个人总结<Nodejs学习笔记>,更多章节及更新,请访问 github主页地址.欢迎加群交流,群号 197339705. 章节概览 morgan是express默认的日志中间件, ...

随机推荐

  1. Win10 企业版激活方法

    如果大家想要激活 Win10的企业版,可以依次执行下面的命令,分别表示安装win10企业版密钥,设置kms服务器,激活win10企业版;slmgr /ipk NW6C2-QMPVW-D7KKK-3GK ...

  2. docker镜像列表存在但删除显示 No such image问题解决

    近期使用了docker,但删除镜像时候遇到了无法删除问题.提示:No such Image.原因有两个,解决方法如下: 原因1: 容器还存在是无法删除镜像的 解决步骤: 1.停掉容器(docker s ...

  3. Android RxJava 异常时堆栈信息显示不全(不准确),解决方案都在这里了

    现象 大家好,我是徐公,今天为大家带来的是 RxJava 的一个血案,一行代码 return null 引发的. 前阵子,组内的同事反馈说 RxJava 在 debug 包 crash 了,捕获到的异 ...

  4. SCA技术进阶系列(一):SBOM应用实践初探

    现代软件都是组装的而非纯自研.随着开源组件在数字化应用中的使用比例越来越高,混源开发已成为当前业内主流开发方式.开源组件的引入虽然加快了软件开发效率,但同时将开源安全问题引入了整个软件供应链.软件组成 ...

  5. SpringCloud学习 系列八、OpenFeign

    系列导航 SpringCloud学习 系列一. 前言-为什么要学习微服务 SpringCloud学习 系列二. 简介 SpringCloud学习 系列三. 创建一个没有使用springCloud的服务 ...

  6. 七、手动制作docker镜像

    系列导航 一.docker入门(概念) 二.docker的安装和镜像管理 三.docker容器的常用命令 四.容器的网络访问 五.容器端口转发 六.docker数据卷 七.手动制作docker镜像 八 ...

  7. vue学习笔记 十九、实例完整代码

    系列导航 vue学习笔记 一.环境搭建 vue学习笔记 二.环境搭建+项目创建 vue学习笔记 三.文件和目录结构 vue学习笔记 四.定义组件(组件基本结构) vue学习笔记 五.创建子组件实例 v ...

  8. 【调试】crash使用方法

    crash简介 crash是redhat的工程师开发的,主要用来离线分析linux内核转存文件,它整合了gdb工具,功能非常强大.可以查看堆栈,dmesg日志,内核数据结构,反汇编等等. crash支 ...

  9. C#对字符串进行加密解密

    首先上效果图 加解密接口 internal string ToEncrypt(string encryptKey, string str) { try { byte[] P_byte_key = // ...

  10. java基础数据类型-int类型-浮点型-数据类型的转换--day02

    目录 1. 变量的命名 2. 常量 3. 变量 4. 进制 4.1 进制转换 4.2 整型数据类型 5 浮点型 6. 字符串 7. 整形与字符型之间的转换 8 最常见的一个乱码问题 9. 布尔类型 1 ...