Skip to content

Request Sending Precedures in GRPC Client Side

As a developer, understanding the behaviors of grpc cannot keep away from the implementation. If we only know the high-level design but do not care about the implementation, we cannot get help in coding through our learning effects.

In this article, I will list some necessary concepts and draw some diagrams to understand implementation better.

Brief of http2

Grpc uses http2 as the underlying transport protocol, however, grpc implements its own protocol, which means it differs from the standard http2 library. Firstly, to distinguish grpc http2, we need to know RPC http2 first.

RFC HTTP2

In this chapter, the following image shows the most important concepts in RFC Http2.

  • Connection: tcp connection, only 1 in http2 request.
  • Stream: stream stands for a whole request, with one or many messages.
  • Message: contains many frames, stands for request or response.
  • Frame: carries data or control data.
  • This article doesn’t focus on http2, the image below is enough here. https://web-dev.imgix.net/image/C47gYyWYVMMhDmtYSLOWazuyePF2/0bfdEw00aKXFDxT0yEKt.svg

Extend concepts of grpc

Streams in HTTP/2 enable multiple concurrent conversations on a single connection; channels extend this concept by enabling multiple streams over multiple concurrent connections

Grpc says the channels and streams are n:m relative, this is a ClientConn(channel implementation in go) holds many connections to the peer. When sending an RPC, the client might choose one of addresses to send an RPC. That’s what it means about multiple streams over multiple concurrent connections.

Client RPC struct overview

Core structs in go code

  • ClientConn: the logic channel concept in grpc, as keywords, change name in go.
  • ClientStream: object handles about a whole lifespan of a rpc request with 1-1 relative. ClientStream is created based on a channels, or ClientConn in golang from an address, a remote proxy or a client side load balancer.
  • Clarify stream and channels, optional to read. The ClientConn is created from an address, a remote proxy or a client side load balancer. gRPC Load Balancing Streams in HTTP/2 enable multiple concurrent conversations on a single connection, but the channel extends this concept by enabling multiple streams over multiple concurrent connections. The reason might be increasing throughout the capacity. Here there is a confusing thing, as the gRPC implements their own http2 protocol, so they extend the protocol to fit the channel. Don’t think multiple connections connect different servers, they are the same one. > Channels represent virtual connections to an endpoint, which in reality may be backed by many HTTP/2 connections. > gRPC on HTTP/2 Engineering a Robust, High-performance Protocol
  • csAttempt: a single transport stream attmept(might fail).
  • transport.ClientTransport: transport interface, http2_client is the implementation.
  • transport.Stream: an RPC in transport layer.

  • Here are the diagrams about those core object. https://kroki.io/plantuml/svg/eNp1UstOwzAQvPsr9saFpvcIoSKfkRDJD7juNrHqR7DXSOXrGRdStUjk4Ngzs7uza--KmCw1ePUwiyyl324Xb2JDOpvCtvBH5Wh5c3BmyiYoZaqkWMOes9KaNs-kdU82sxGm_ZmmvFiC2Hc3tItOALkvSAzSW87dm7Mnzo80J38gySaWJWXptHccZVzPKibkzW6aRb2aE1NJAcsMylahmTP3akM_UTrF2KPgFRgEvgKgAZAtLyIcFunbFsC_RREw3vFrnnFQHA_UPN10dxkhGXp_02SN9ys19MSfnM8XIrdBFjg2hUykGh3O0HRK-ZSWdYJwBk6cp1Kt5VKO1SvCpwd6QtIR9SYW_OmYU_g7zjvpcL2XcYCw8IFSROQqggbl4J-lLi2jQW9osdHNx28PphQ3RZLUxoj21Q4L3sc3FOu5Jw== https://kroki.io/plantuml/svg/eNqlks9ugzAMxu95Ct92Wds7qqZOnCdVDS9gggtR84clTqW9_RzRdmXSTuMA2LE___zBITMmLt6pl4l5zs1uNzsMNbM10e8yfRYKhjaDxTGhVwoLx1B8T0m1GjZvYPJ7A5nCAJ5yxpFUiEyQ7Dix-sALQY5eblNMbArDRIkatYHWWQqsORH6BlotKZFiJj9zU18lwQlDnqVxu1R391gautX5XafTqqJUBAVyiQ7shbJrGxiJ5QnnFD30KGsaStujNRfZZVWrZb7IyRadlspMA8QgrcrFON92zSP0Xz_EUAJbB7kYIy6ci3soriwakLH2CQZKuNCqW1krc8822Dyt_Xw2OpEhe6XH2TNz3Q-r5n8wk7kuDnX6tbbKQEmUYNjG8Aftbyjh3csHFssrA8Lp2IJB59RBYvm1vgHrks4X

Reflection between grpc concepts and implementation

Grpc has new concepts of remote procedure calls (RPCs), and messages. The table below shows the relationship:

Grpc concepts Implementation Description diagram abbreviation
Channels ClientConn virtual connections to an endpoint, which in reality may be backed by many HTTP/2 connections CC
RPC ClientStream RPCs are associated with a connection and in practice plain HTTP/2 streams CS
Messages flow in the callstack Messages are associated with RPCs and get sent as HTTP/2 data frames. It’s constructed in every call stack and finally sent by the http2 client. N/A

From the table, you could see the logic concepts are mainly for users to better understand, they sometimes doesn’t help us understand the code management so much. We much check the code carefully to find out the corresponding from the logic to code implementation details.

For the logic concepts, knowing grpc define three new concepts, underlies the http2 is enough for users, as it’s an RPC framework which aims to reduce work about RPC.

Send Message


⚠️  Note that in this page, I don’t talk about any details about http2 written by grpc. It should be an abstraction from how the data is transported in transport layer. Grpc already wishes it could be ignore by the user with the interface provided, and uses the interface value in its rpc logic.


Before sending message, the csAttempt is already created with a ClientTransport from http2 client, and a new http stream with type transport.Stream.

Each csAttempt holds its own transport.Stream, which means no matter it retries or not, it always sends data in a same transport stream.

Since now, how to data flow in sending is clear, if you are still not clear, could check the diagram and understand the relationship between core structs.

After knowing how a message is sent out, now let’s take an eye for the rpc call message data processing. Grpc is a RPC framework which means how to process the data from RPC call to the transport layer is important, too.

How the data is process from a rpc call to transport sending?

1.Make RPC with protobuf generated struct.

As the code snippet below, users work is done after creating a request from the protobuf generated code. Important arguments:

Name Value
Method /helloworld.Greeter/SayHello
ProtobufRequest pb.HelloRequest{Name: *name}
func main(){
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest,
 opts ...grpc.CallOption) (*HelloReply, error)
        err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
}

2.Receive protobuf generated struct directly when sending data

The csAttempt has a method of SendMsg which receive the request struct as interface directly.

func (cs *clientStream) SendMsg(m interface{}) (err error)

3.Prepare message before sending

The preparation means it gets some additional information from the message. But it doesn’t mean the origin is discard and only use the results of preparing, instead, both the result of prepariing and the origin are passed to the down call.

  • the call of prepareMsg:
hdr, payload, data, err := prepareMsg(m, cs.codec, cs.cp, cs.comp)
op := func(a *csAttempt) error {
        return a.sendMsg(m, hdr, payload, data)
}
err = cs.withRetry(op, func() {/* ignore details*/ })
  • prepareMsg: receive msg, a encoder, compressor. The prepareMsg marshal the data first, gets a []byte. Then the compressor compresses the data, finally bind header and payload based on the compressed message. Why the interface(actually a protobuf generated type) could be operated as expected is a little more contents, I would like to introduce it below. Here, knowing there is a such step is ok.
func prepareMsg(m interface{}, codec baseCodec, cp Compressor,
comp encoding.Compressor) (hdr, payload, data []byte, err error) {
        // ignore PreparedMsg check here.
    data, err = encode(codec, m)
    compData, err := compress(data, cp, comp)
    hdr, payload = msgHeader(data, compData)
    return hdr, payload, data, nil
}

4.Send the header&payload only, but use message for statsHandle

Note we keep the origin message below and still pass it down. However, the message is not sent in transport layer as we already has the header and payload, which could get a origin message back(by the same decode and decompressor).

func (a *csAttempt) sendMsg(m interface{}, hdr, payld, data []byte) error {
    cs := a.cs
        // ignore lines
    if err := a.t.Write(a.s, hdr, payld,
                &transport.Options{Last: !cs.desc.ClientStreams}); err != nil {
    }
    for _, sh := range a.statsHandlers {
        sh.HandleRPC(a.ctx, outPayload(true, m, data, payld, time.Now()))
    }
    return nil
}

Based on the code below, the origin message is used by the statsHandlers. Not important about our topic here.

How grpc encode the protobuf generated struct

In the end of this topic, I mentioned the encode and compress in grpc for protobuf are too long there, so I will check it here.

First, we find the prepareMsg requires an encoder, two compressors, which are all provided by the ClientStream. Now let’s focus on what are the values of them. Looking at how ClientStream is intialized for those three:

cs := &clientStream{
        codec:        c.codec,
        cp:           cp,
        comp:         comp,
}

The values are decided by those code lines.

c := defaultCallInfo()
for _, o := range opts {
        err := o.before(c)
}
err := setCallInfoCodec(c)

var cp Compressor
var comp encoding.Compressor
if ct := c.compressorType; ct != "" {
        callHdr.SendCompress = ct
        if ct != encoding.Identity {
            comp = encoding.GetCompressor(ct)
        }
    } else if cc.dopts.cp != nil {
        callHdr.SendCompress = cc.dopts.cp.Type()
        cp = cc.dopts.cp
    }
}

If we not setup the compressors by options, they are nil and won’t do nothing, and the they are nil by default.

But encoder is a complusory one, it’s set up in function setCallInfoCodec:

func setCallInfoCodec(c *callInfo) error {
    if ec, ok := c.codec.(encoding.Codec);ok&&c.codec != nil&&
            c.contentSubtype == "" {
        c.contentSubtype = strings.ToLower(ec.Name())//return
    }

    if c.contentSubtype == "" {
        // No codec specified in CallOptions; use proto by default.
        c.codec = encoding.GetCodec(proto.Name) //return
    }
    c.codec = encoding.GetCodec(c.contentSubtype) //return
}

There is a proto codec which is the implementation of protobuf, initialized in init. As a result, it can encode the message from protobuf generated struct.

func init() {
    encoding.RegisterCodec(codec{})
}

Conclusion

In this page, I mainly discuss some topics below:

  • Http2 concepts
  • Client side core struct overview and workflow
  • Details about grpc implementation to send a RPC request out
  • The message construction from the user level RPC call to sending it to the server.
  • encode and compress of the message

Note that once you realize those knowledges, the receive message is easy to understand. As a result, I don’t write it down in this page.