gRPC 数据流转全过程剖析

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 专享技术内容哦。

FtooAtPSkEJwnW-9xkCLqSTRpBKX

发布了3634 篇原创文章 · 获赞 3487 · 访问量 325万+

猜你喜欢

转载自blog.csdn.net/valada/article/details/104272159
今日推荐