gRPC 是 Google 开源的一款高性能的 RPC 框架,基于 HTTP2 协议,支持跨语言,非常通用化。
本讲 Chat 主要介绍数据包是如何在 gRPC Client 端和 Server 端进行流转的。主要包括:
- 网络协议的介绍:HTTP2 和自定义私有协议。
- gRPC 如何进行协议编码和解码。
- gRPC 如何解析 HTTP 帧。
- 解析出来的包头和消息体如何进行数据流转。
本节 Chat 包含一定的技术深度,需要对 gRPC 和 gRPC 框架有一定基础的同学阅读。主要使用 Go 语言进行讲解。
GRPC 是 Google 开源的一款高性能的 RPC 框架,基于 HTTP2 协议,支持跨语言,非常通用化。
下面主要从 协议、http2 帧解析过程、grpc 协议编解码实现 和 stream 四个方面来进行介绍 grpc 的数据流转。
一、协议
我们知道网络传输都是以二进制的形式,所以所有的协议底层的传输也是二进制。那么问题来了,client 往 server 发一个数据包,server 如何知道数据包是完成还是还在发送呢?又或者,假如一个数据包过大,client 需要拆成几个包发送,或者数据包过小,client 需要合成一个包发送,server 如何识别呢?为了解决这些问题,client 和 server 都会约定好一些双方都能理解的“规则“,这就是协议。
http2 帧格式
我们知道 grpc 传输层是基于 http2 协议规范,所以我们先要了解 http2 协议帧的格式。http 2 协议帧格式如下:
Frame FormatAll frames begin with a fixed 9-octet header followed by a variable-length payload. +-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +=+=============================================================+ | Frame Payload (0...) ... +---------------------------------------------------------------+
对于一个网络包而言,首先要知道这个包的格式,然后才能按照约定的格式解析出这个包。那么 grpc 的包是什么样的格式呢? 看了源码后,先直接揭晓出来,它其实是这样的
http 帧格式为:length (3 byte) + type(1 byte) + flag (1 byte) + R (1 bit) + stream identifier (31 bit) + paypoad,payload 是消息具体内容
前 9 个字节是 http 包头,length 表示消息长度,type 表示 http 帧的类型,http 一共规定了 10 种帧类型:
- HEADERS帧 头信息,对应于HTTP HEADER
- DATA帧 对应于HTTP Response Body
- PRIORITY帧 用于调整流的优先级
- RST_STREAM帧 流终止帧,用于中断资源的传输
- SETTINGS帧 用户客户服务器交流连接配置信息
- PUSH_PROMISE帧 服务器向客户端主动推送资源
- GOAWAY帧 通知对方断开连接
- PING帧 心跳帧,检测往返时间和连接可用性
- WINDOW_UPDATE帧 调整帧大小
- CONTINUATION帧 HEADERS太大时的续帧
flag 表示标志位,http 一共三种标志位:
- END_STREAM 流结束标志,表示当前帧是流的最后一个帧
- END_HEADERS 头结束表示,表示当前帧是头信息的最后一个帧
- PADDED 填充标志,在数据Payload里填充无用信息,用于干扰信道监听
R 是 1bit 的保留位,stream identifier 是流 id,http 会为每一个数据流分配一个 id
具体可以参考:http frame
grpc 协议格式
grpc 是基于 http2 协议的,我们来看看 grpc 协议里面的一些关键信息:
可以看到,grpc 协议的信息是基于 http 协议的格式给传递下去的。
二、解析 http 帧
上面我们说到了,grpc 协议信息通过 http 协议的格式进行传递,同时 http2 是通过帧的方式进行传递,那么 http2 帧是如何解析的呢?
回到 examples 目录的 helloworld 目录, 在 server main 函数中跟踪 s.Serve 方法: s.Serve() ——> s.handleRawConn(rawConn) ——> s.serveStreams(st)
来看一下 serveStreams 这个方法
func (s *Server) serveStreams(st transport.ServerTransport) { defer st.Close() var wg sync.WaitGroup st.HandleStreams(func(stream *transport.Stream) { wg.Add(1) go func() { defer wg.Done() s.handleStream(st, stream, s.traceInfo(st, stream)) }() }, func(ctx context.Context, method string) context.Context { if !EnableTracing { return ctx } tr := trace.New("grpc.Recv."+methodFamily(method), method) return trace.NewContext(ctx, tr) }) wg.Wait()}
这里调用了 transport 的 HandleStreams 方法, 这个方法就是 http 帧的处理的具体实现。它的底层直接调用的 http2 包的 ReadFrame 方法去读取一个 http 帧数据。
type framer struct { writer *bufWriter fr *http2.Framer}func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) { defer close(t.readerDone) for { frame, err := t.framer.fr.ReadFrame() atomic.StoreUint32(&t.activity, 1) ... switch frame := frame.(type) { case *http2.MetaHeadersFrame: if t.operateHeaders(frame, handle, traceCtx) { t.Close() break } case *http2.DataFrame: t.handleData(frame) case *http2.RSTStreamFrame: t.handleRSTStream(frame) case *http2.SettingsFrame: t.handleSettings(frame) case *http2.PingFrame: t.handlePing(frame) case *http2.WindowUpdateFrame: t.handleWindowUpdate(frame) case *http2.GoAwayFrame: // TODO: Handle GoAway from the client appropriately. default: errorf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame) } }}
通过 http2 包的 ReadFrame 直接读取出一个帧的数据。
func (fr *Framer) ReadFrame() (Frame, error) { fr.errDetail = nil if fr.lastFrame != nil { fr.lastFrame.invalidate() } fh, err := readFrameHeader(fr.headerBuf[:], fr.r) if err != nil { return nil, err } if fh.Length > fr.maxReadSize { return nil, ErrFrameTooLarge } payload := fr.getReadBuf(fh.Length) if _, err := io.ReadFull(fr.r, payload); err != nil { return nil, err } f, err := typeFrameParser(fh.Type)(fr.frameCache, fh, payload) if err != nil { if ce, ok := err.(connError); ok { return nil, fr.connError(ce.Code, ce.Reason) } return nil, err } if err := fr.checkFrameOrder(f); err != nil { return nil, err } if fr.logReads { fr.debugReadLoggerf("http2: Framer %p: read %v", fr, summarizeFrame(f)) } if fh.Type == FrameHeaders && fr.ReadMetaHeaders != nil { return fr.readMetaFrame(f.(*HeadersFrame)) } return f, nil}
fh, err := readFrameHeader(fr.headerBuf[:], fr.r) 这一行代码读取了 http 的包头数据,我们来看一下 headerBuf 的长度,发现果然是 9 个字节。
const frameHeaderLen = 9func readFrameHeader(buf []byte, r io.Reader) (FrameHeader, error) { _, err := io.ReadFull(r, buf[:frameHeaderLen]) if err != nil { return FrameHeader{}, err } return FrameHeader{ Length: (uint32(buf[0])<<16 | uint32(buf[1])<<8 | uint32(buf[2])), Type: FrameType(buf[3]), Flags: Flags(buf[4]), StreamID: binary.BigEndian.Uint32(buf[5:]) & (1<<31 - 1), valid: true, }, nil}
解析业务数据
经过上面的过程,我们终于将 http 包头给读出来了。前面说到了,读出 http 包体之后,还需要解析 grpc 协议头。那这部分是怎么去解析的呢?
回到 http2 读帧的部分,当发现帧的格式是 MetaHeadersFrame,也就是第一个帧时,会调用 operateHeaders 方法
case *http2.MetaHeadersFrame: if t.operateHeaders(frame, handle, traceCtx) { t.Close() break }
看一下 operateHeaders ,里面会去调用 handle(s) , 这个handle 其实是 之前 s.Serve() ——> s.handleRawConn(rawConn) ——> s.serveStreams(st) 这个路径下的 HandleStreams 方法传入的,也就是会去调用 handleStream 这个方法
st.HandleStreams(func(stream *transport.Stream) { wg.Add(1) go func() { defer wg.Done() s.handleStream(st, stream, s.traceInfo(st, stream)) }() }, func(ctx context.Context, method string) context.Context { if !EnableTracing { return ctx } tr := trace.New("grpc.Recv."+methodFamily(method), method) return trace.NewContext(ctx, tr) })
s.handleStream(st, stream, s.traceInfo(st, stream)) ——> s.processUnaryRPC(t, stream, srv, md, trInfo) ———> d, err := recvAndDecompress(&parser{r: stream}, stream, dc, s.opts.maxReceiveMessageSize, payInfo, decomp) ,进入 recvAndDecompress 这个函数,里面调用了
pf, d, err := p.recvMsg(maxReceiveMessageSize)
进入 recvMsg,发现它就是解析 grpc 协议 的函数,先把协议头读出来,用了 5 个字节。从协议头中得知协议体消息的长度,然后用一个相应长度的 byte 数组把协议体读出来
type parser struct { r io.Reader header [5]byte}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 } pf = payloadFormat(p.header[0]) length := binary.BigEndian.Uint32(p.header[1:]) ... 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}
继续回到这个图,前面说到了 grpc 协议头是 5个字节。
compressed-flag 表示是否压缩,值为 1 是压缩消息体数据,0 不压缩。length 表示消息体数据长度。现在终于知道了这个数据结构的由来!
读取出来的数据是二进制的,读出来原数据之后呢,我们就可以针对相应的数据做解包操作了。
三、grpc 协议编解码
一般的协议都会包括协议头和协议体,对于业务而言,一般只关心需要发送的业务数据。所以,协议头的内容一般是框架自动帮忙填充。将业务数据包装成指定协议格式的数据包就是编码的过程,从指定协议格式中的数据包中取出业务数据的过程就是解码的过程。
每个 rpc 框架基本都有自己的编解码器,下面我们就来说说 grpc 的编解码过程。
grpc 解码
我们还是从我们的 examples 目录下的 helloworld demo 中 server 的 main 函数入手
func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) }}
在 s.Serve(lis) ——> s.handleRawConn(rawConn) —— > s.serveStreams(st) ——> s.handleStream(st, stream, s.traceInfo(st, stream)) ——> s.processUnaryRPC(t, stream, srv, md, trInfo) 方法中有一段代码:
sh := s.opts.statsHandler...df := func(v interface{}) error { if err := s.getCodec(stream.ContentSubtype()).Unmarshal(d, v); err != nil { return status.Errorf(codes.Internal, "grpc: error unmarshalling request: %v", err) } if sh != nil { sh.HandleRPC(stream.Context(), &stats.InPayload{ RecvTime: time.Now(), Payload: v, WireLength: payInfo.wireLength, Data: d, Length: len(d), }) } if binlog != nil { binlog.Log(&binarylog.ClientMessage{ Message: d, }) } if trInfo != nil { trInfo.tr.LazyLog(&payload{sent: false, msg: v}, true) } return nil}
这段代码的逻辑先调 getCodec 获取解包类,然后调用这个类的 Unmarshal 方法进行解包。将业务数据取出来,然后调用 handler 进行处理。
func (s *Server) getCodec(contentSubtype string) baseCodec { if s.opts.codec != nil { return s.opts.codec } if contentSubtype == "" { return encoding.GetCodec(proto.Name) } codec := encoding.GetCodec(contentSubtype) if codec == nil { return encoding.GetCodec(proto.Name) } return codec}
我们来看 getCodec 这个方法,它是通过 contentSubtype 这个字段来获取解包类的。假如不设置 contentSubtype ,那么默认会用名字为 proto 的解码器。
我们来看看 contentSubtype 是如何设置的。之前说到了 grpc 的底层默认是基于 http2 的。在 serveHttp 时调用了 NewServerHandlerTransport 这个方法来创建一个 ServerTransport,然后我们发现,其实就是根据 content-type 这个字段去生成的。
func NewServerHandlerTransport(w http.ResponseWriter, r *http.Request, stats stats.Handler) (ServerTransport, error) { ... contentType := r.Header.Get("Content-Type") // TODO: do we assume contentType is lowercase? we did before contentSubtype, validContentType := contentSubtype(contentType) if !validContentType { return nil, errors.New("invalid gRPC request content-type") } if _, ok := w.(http.Flusher); !ok { return nil, errors.New("gRPC requires a ResponseWriter supporting http.Flusher") } st := &serverHandlerTransport{ rw: w, req: r, closedCh: make(chan struct{}), writes: make(chan func()), contentType: contentType, contentSubtype: contentSubtype, stats: stats, }}
我们来看看 contentSubtype 这个方法 。
...baseContentType = "application/grpc"...func contentSubtype(contentType string) (string, bool) { if contentType == baseContentType { return "", true } if !strings.HasPrefix(contentType, baseContentType) { return "", false } // guaranteed since != baseContentType and has baseContentType prefix switch contentType[len(baseContentType)] { case '+', ';': // this will return true for "application/grpc+" or "application/grpc;" // which the previous validContentType function tested to be valid, so we // just say that no content-subtype is specified in this case return contentType[len(baseContentType)+1:], true default: return "", false }}
可以看到 grpc 协议默认以 application/grpc 开头,假如不一这个开头会返回错误,假如我们想使用 json 的解码器,应该设置 content-type = application/grpc+json 。下面是一个基于 grpc 协议的请求 request :
HEADERS (flags = END_HEADERS):method = POST:scheme = http:path = /google.pubsub.v2.PublisherService/CreateTopic:authority = pubsub.googleapis.comgrpc-timeout = 1Scontent-type = application/grpc+protogrpc-encoding = gzipauthorization = Bearer y235.wef315yfh138vh31hv93hv8h3vDATA (flags = END_STREAM)<Length-Prefixed Message>
详细可参考 proto-http2
怎么拿的呢,再看一下 encoding.getCodec 方法
func GetCodec(contentSubtype string) Codec { return registeredCodecs[contentSubtype]}
它其实取得是 registeredCodecs 这个 map 中的 codec,这个 map 是 RegisterCodec 方法注册进去的。
var registeredCodecs = make(map[string]Codec)func RegisterCodec(codec Codec) { if codec == nil { panic("cannot register a nil Codec") } if codec.Name() == "" { panic("cannot register Codec with empty string result for Name()") } contentSubtype := strings.ToLower(codec.Name()) registeredCodecs[contentSubtype] = codec}
毫无疑问, encoding 目录的 proto 包下肯定在初始化时调用注册方法了。果然
func init() { encoding.RegisterCodec(codec{})}
绕了一圈,调用的其实是 proto 的 Unmarshal 方法,如下:
func (codec) Unmarshal(data []byte, v interface{}) error { protoMsg := v.(proto.Message) protoMsg.Reset() if pu, ok := protoMsg.(proto.Unmarshaler); ok { // object can unmarshal itself, no need for buffer return pu.Unmarshal(data) } cb := protoBufferPool.Get().(*cachedProtoBuffer) cb.SetBuf(data) err := cb.Unmarshal(protoMsg) cb.SetBuf(nil) protoBufferPool.Put(cb) return err}
grpc 编码
在剖析解码代码的基础上,编码代码就很轻松了,其实直接找到 encoding 目录的 proto 包,看 Marshal 方法在哪儿被调用就行了。
于是我们很快就找到了调用路径,也是这个路径:
s.Serve(lis) ——> s.handleRawConn(rawConn) —— > s.serveStreams(st) ——> s.handleStream(st, stream, s.traceInfo(st, stream)) ——> s.processUnaryRPC(t, stream, srv, md, trInfo)
processUnaryRPC 方法中有一段 server 发送响应数据的代码。其实也就是这一行:
if err := s.sendResponse(t, stream, reply, cp, opts, comp); err != nil {
我们其实也能猜到,发送数据给 client 之前肯定要编码。果然调用了 encode 方法
func (s *Server) sendResponse(t transport.ServerTransport, stream *transport.Stream, msg interface{}, cp Compressor, opts *transport.Options, comp encoding.Compressor) error { data, err := encode(s.getCodec(stream.ContentSubtype()), msg) if err != nil { grpclog.Errorln("grpc: server failed to encode response: ", err) return err } ...}
来看一下 encode
func encode(c baseCodec, msg interface{}) ([]byte, error) { if msg == nil { // NOTE: typed nils will not be caught by this check return nil, nil } b, err := c.Marshal(msg) if err != nil { return nil, status.Errorf(codes.Internal, "grpc: error while marshaling: %v", err.Error()) } if uint(len(b)) > math.MaxUint32 { return nil, status.Errorf(codes.ResourceExhausted, "grpc: message too large (%d bytes)", len(b)) } return b, nil}
它调用了 c.Marshal 方法, Marshal 方法其实是 baseCodec 定义的一个通用抽象方法
type baseCodec interface { Marshal(v interface{}) ([]byte, error) Unmarshal(data []byte, v interface{}) error}
proto 实现了 baseCodec,前面说到了通过 s.getCodec(stream.ContentSubtype(),msg) 获取到的其实是 contentType 里面设置的协议名称,不设置的话默认取 proto 的编码器。所以最终是调用了 proto 包下的 Marshal 方法,如下:
func (codec) Marshal(v interface{}) ([]byte, error) { if pm, ok := v.(proto.Marshaler); ok { // object can marshal itself, no need for buffer return pm.Marshal() } cb := protoBufferPool.Get().(*cachedProtoBuffer) out, err := marshal(v, cb) // put back buffer and lose the ref to the slice cb.SetBuf(nil) protoBufferPool.Put(cb) return out, err}
ok,那么至此,grpc 的整个编解码的流程我们就已经剖析完了
四、Stream 的构造
数据承载体
为了回答上面的问题,我们需要一个数据承载体结构,来保存协议里面的一些需要透传的一些重要信息,比如 Method 等。在 grpc 中,这个结构就是 Stream, 我们来看一下 Stream 的定义。
// Stream represents an RPC in the transport layer.type Stream struct { id uint32 st ServerTransport // nil for client side Stream ctx context.Context // the associated context of the stream cancel context.CancelFunc // always nil for client side Stream done chan struct{} // closed at the end of stream to unblock writers. On the client side. ctxDone <-chan struct{} // same as done chan but for server side. Cache of ctx.Done() (for performance) method string // the associated RPC method of the stream recvCompress string sendCompress string buf *recvBuffer trReader io.Reader fc *inFlow wq *writeQuota // Callback to state application's intentions to read data. This // is used to adjust flow control, if needed. requestRead func(int) headerChan chan struct{} // closed to indicate the end of header metadata. headerChanClosed uint32 // set when headerChan is closed. Used to avoid closing headerChan multiple times. // hdrMu protects header and trailer metadata on the server-side. hdrMu sync.Mutex // On client side, header keeps the received header metadata. // // On server side, header keeps the header set by SetHeader(). The complete // header will merged into this after t.WriteHeader() is called. header metadata.MD trailer metadata.MD // the key-value map of trailer metadata. noHeaders bool // set if the client never received headers (set only after the stream is done). // On the server-side, headerSent is atomically set to 1 when the headers are sent out. headerSent uint32 state streamState // On client-side it is the status error received from the server. // On server-side it is unused. status *status.Status bytesReceived uint32 // indicates whether any bytes have been received on this stream unprocessed uint32 // set if the server sends a refused stream or GOAWAY including this stream // contentSubtype is the content-subtype for requests. // this must be lowercase or the behavior is undefined. contentSubtype string}
四、数据流转
server 端数据流转
前面说到了 grpc 协议里面的一些字段,如下:可以看到,一次请求需要携带这么多信息,server 会根据 client 携带的这些信息来进行相应的处理。server 通过解析 http2 帧获取协议数据信息,那么这些信息是如何在上下文传递呢?这里就需要一个载体,在 grpc 中,这个载体就是 Stream。
我们先来看看 server 端 Stream 的构造。前面的内容已经说过 server 的处理流程了。我们直接进入 serveStreams 这个方法。路径为:s.Serve(lis) ——> s.handleRawConn(rawConn) ——> s.serveStreams(st)
func (s *Server) serveStreams(st transport.ServerTransport) { defer st.Close() var wg sync.WaitGroup st.HandleStreams(func(stream *transport.Stream) { wg.Add(1) go func() { defer wg.Done() s.handleStream(st, stream, s.traceInfo(st, stream)) }() }, func(ctx context.Context, method string) context.Context { if !EnableTracing { return ctx } tr := trace.New("grpc.Recv."+methodFamily(method), method) return trace.NewContext(ctx, tr) }) wg.Wait()}
最上层 HandleStreams 是对 http2 数据帧的处理。grpc 一共处理了 MetaHeadersFrame 、DataFrame、RSTStreamFrame、SettingsFrame、PingFrame、WindowUpdateFrame、GoAwayFrame 等 7 种帧。
// HandleStreams receives incoming streams using the given handler. This is// typically run in a separate goroutine.// traceCtx attaches trace to ctx and returns the new context.func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) { defer close(t.readerDone) for { frame, err := t.framer.fr.ReadFrame() atomic.StoreUint32(&t.activity, 1) if err != nil { if se, ok := err.(http2.StreamError); ok { warningf("transport: http2Server.HandleStreams encountered http2.StreamError: %v", se) t.mu.Lock() s := t.activeStreams[se.StreamID] t.mu.Unlock() if s != nil { t.closeStream(s, true, se.Code, false) } else { t.controlBuf.put(&cleanupStream{ streamID: se.StreamID, rst: true, rstCode: se.Code, onWrite: func() {}, }) } continue } if err == io.EOF || err == io.ErrUnexpectedEOF { t.Close() return } warningf("transport: http2Server.HandleStreams failed to read frame: %v", err) t.Close() return } switch frame := frame.(type) { case *http2.MetaHeadersFrame: if t.operateHeaders(frame, handle, traceCtx) { t.Close() break } case *http2.DataFrame: t.handleData(frame) case *http2.RSTStreamFrame: t.handleRSTStream(frame) case *http2.SettingsFrame: t.handleSettings(frame) case *http2.PingFrame: t.handlePing(frame) case *http2.WindowUpdateFrame: t.handleWindowUpdate(frame) case *http2.GoAwayFrame: // TODO: Handle GoAway from the client appropriately. default: errorf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame) } }}
对于每一次请求而言,client 一定会先发 HeadersFrame 这个帧,grpc 这里是直接使用 http2 工具包进行实现,直接处理的 MetaHeadersFrame 帧,这个帧的定义为:
// A MetaHeadersFrame is the representation of one HEADERS frame and// zero or more contiguous CONTINUATION frames and the decoding of// their HPACK-encoded contents.//// This type of frame does not appear on the wire and is only returned// by the Framer when Framer.ReadMetaHeaders is set.type MetaHeadersFrame struct { *HeadersFrame Fields []hpack.HeaderField Truncated bool}
所以是在 MetaHeadersFrame 这个帧里去处理包头数据。所以会去执行 operateHeaders 这个方法,在这个方法里面会去构造一个 stream ,这个 stream 里面包含了传输层请求上下文的数据。包括方法名等。
s := &Stream{ id: streamID, st: t, buf: buf, fc: &inFlow{limit: uint32(t.initialWindowSize)}, recvCompress: state.data.encoding, method: state.data.method, contentSubtype: state.data.contentSubtype, }
构造完 stream 后,接下来 tranport 对数据的处理都会将 stream 层层透传下去。所以整个请求内所需要的数据都从 stream 中可以得到,这样就实现了 server 端的数据流转。
client 端数据流转
与 server 相对应,client 端也有一个 clientStream 结构,定义如下:
// clientStream implements a client side Stream.type clientStream struct { callHdr *transport.CallHdr opts []CallOption callInfo *callInfo cc *ClientConn desc *StreamDesc codec baseCodec cp Compressor comp encoding.Compressor cancel context.CancelFunc // cancels all attempts sentLast bool // sent an end stream beginTime time.Time methodConfig *MethodConfig ctx context.Context // the application's context, wrapped by stats/tracing retryThrottler *retryThrottler // The throttler active when the RPC began. binlog *binarylog.MethodLogger // Binary logger, can be nil. // serverHeaderBinlogged is a boolean for whether server header has been // logged. Server header will be logged when the first time one of those // happens: stream.Header(), stream.Recv(). // // It's only read and used by Recv() and Header(), so it doesn't need to be // synchronized. serverHeaderBinlogged bool mu sync.Mutex firstAttempt bool // if true, transparent retry is valid numRetries int // exclusive of transparent retry attempt(s) numRetriesSincePushback int // retries since pushback; to reset backoff finished bool // TODO: replace with atomic cmpxchg or sync.Once? attempt *csAttempt // the active client stream attempt // TODO(hedging): hedging will have multiple attempts simultaneously. committed bool // active attempt committed for retry? buffer []func(a *csAttempt) error // operations to replay on retry bufferSize int // current size of buffer}
client 的构造就更直接了,在 invoke 发起下游调用时, 直接在 sendMsg 之前就会提前构造 clientStream, 如下:
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)}
stream 这个结构承载了数据流转之外,同时 grpc 流式传输的实现也是基于 stream 去实现的。
阅读全文: http://gitbook.cn/gitchat/activity/5e428f8e929e2328a2687d1e
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。