grpc-go客户端源码分析

代码讲解基于v1.37.0版本。

和grpc-go服务端源码分析一样,我们先看一段示例代码,

const (address     = "localhost:50051"defaultName = "world"
)func main() {// Set up a connection to the server.conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())if err != nil {log.Fatalf("did not connect: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)// Contact the server and print out its response.name := defaultNameif len(os.Args) > 1 {name = os.Args[1]}ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})if err != nil {log.Fatalf("could not greet: %v", err)}log.Printf("Greeting: %s", r.GetMessage())
}

先调用grpc.Dial生成一个grpc.ClientConn对象,具体的初始化操作在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对象,对应结构体包括的字段。

// ClientConn represents a virtual connection to a conceptual endpoint, to
// perform RPCs.
//
// ClientConn是用于RPC通信的虚拟连接
// 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.
//
// ClientConn可以选择连接终端的数量同时选择负载均衡逻辑
//
// 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.
// ClientConn封装了一系列的功能,包括名称解析,TCP连接建立(包括重试和退避策略),TLS,重新名字解析和重联机制。type ClientConn struct {ctx    context.Contextcancel context.CancelFunctarget       stringparsedTarget resolver.Target // 负载均衡选择authority    stringdopts        dialOptions // 初始化可设置选项,在每一次请求会带上,看call.go中的combine方法csMgr        *connectivityStateManager // 连接状态维护 balancerBuildOpts balancer.BuildOptions // 忽略 blockingpicker    *pickerWrapper // 负载均衡设置 safeConfigSelector iresolver.SafeConfigSelector // 忽略 mu              sync.RWMutexresolverWrapper *ccResolverWrapper // 实现了resolver.ClientConn,位于./resolver/resolver.go中,ClientConn的上层包装器(疑惑?)sc              *ServiceConfigconns           map[*addrConn]struct{} // 存放连接的地方// Keepalive parameter can be updated if a GoAway is received.mkp             keepalive.ClientParameterscurBalancerName stringbalancerWrapper *ccBalancerWrapper // 负载均衡器上的包装器(疑惑?)retryThrottler  atomic.ValuefirstResolveEvent *grpcsync.EventchannelzID int64 // channelz unique identification numberczData     *channelzDatalceMu               sync.Mutex // protects lastConnectionErrorlastConnectionError error
}

调用pb.NewGreeterClient(conn)返回当前PB的Client对象,具体的实现代码:

func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient {return &greeterClient{cc}
}// ClientConnInterface defines the functions clients need to perform unary and
// streaming RPCs.  It is implemented by *ClientConn, and is only intended to
// be referenced by generated code.// ClientConnInterface定义了执行RPC方法(包括unary和streaming)的对象需要实现的函数
// ClientConn实现了该interface{},只希望被自动生成的代码调用。type ClientConnInterface interface {// Invoke performs a unary RPC and returns after the response is received// into reply.// Invoke执行unary类型的请求并返回数据Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...CallOption) error// NewStream begins a streaming RPC.// NewStream开启streaming RPC.NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error)
}

虽然ClientConn实现了ClientConnInterface,然而实现代码并没有放到一起。

ClientConn和ClientConnInterface定义在clientconn.go文件中。ClientConn实现Invoke方法是在call.go文件中。ClientConn实现NewStream方法是在stream.go文件中。

说说c.SayHello(ctx, &pb.HelloRequest{Name: name}),使用之前定义的GreeterClient来调用SayHello方法,具体代码实现如下,

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {out := new(HelloReply)// c是当前初始化的greetClient,cc是之前初始化好的ClientConn,Invoke表示使用unary方法,接下来就可以跳转到call.go文件中查看。err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)if err != nil {return nil, err}return out, nil
}

接下来看Invoke方法具体实现,

func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error {// 省略一些代码 return invoke(ctx, method, args, reply, cc, opts...)
}func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {// 创建ClientStream,newClientStream这个方法unary方法也会调用,使用第二个参数StreamDesc来区分。cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)if err != nil {return err}// 发消息,cs是grpc.clientStream对象,调用clientStream的SendMsg方法 if err := cs.SendMsg(req); err != nil {return err}// 收消息,cs是grpc.clientStream对象,调用clientStream的RecvMsg方法 return cs.RecvMsg(reply)
}

对于newClientStream的调用,

func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, opts ...CallOption) (_ ClientStream, err error) {var newStream = func(ctx context.Context, done func()) (iresolver.ClientStream, error) {return newClientStreamWithParams(ctx, desc, cc, method, mc, onCommit, done, opts...)}return newStream(ctx, func() {})
}// 初始化stream传入参数说明,
// desc *StreamDesc,决定调用unary还是stream
// cc *ClientConn,grpc连接对象
// opts ...CallOption,初始化对象传入的各种参数
func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc serviceconfig.MethodConfig, onCommit, doneFunc func(), opts ...CallOption) (_ iresolver.ClientStream, err error) {// 省略部分代码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,onCommit:     onCommit,}op := func(a *csAttempt) error { return a.newStream() }// 使用forloop初始化stream,这个代码写的比较绕if err := cs.withRetry(op, func() { cs.bufferForRetryLocked(0, op) }); err != nil {cs.finish(err)return nil, err}return cs, nil
}

看看withRetry实现方式,

func (cs *clientStream) withRetry(op func(a *csAttempt) error, onSuccess func()) error {cs.mu.Lock()for {if cs.committed {cs.mu.Unlock()return op(cs.attempt)}a := cs.attempt // 这里是指针 cs.mu.Unlock()err := op(a) // 将指针传入op函数中,op是`op := func(a *csAttempt) error { return a.newStream() }`cs.mu.Lock()if a != cs.attempt {// We started another attempt already.continue}if err == io.EOF {<-a.s.Done()}if err == nil || (err == io.EOF && a.s.Status().Code() == codes.OK) {onSuccess()cs.mu.Unlock()return err}if err := cs.retryLocked(err); err != nil {cs.mu.Unlock()return err}}
}func (a *csAttempt) newStream() error {cs := a.cscs.callHdr.PreviousAttempts = cs.numRetriess, err := a.t.NewStream(cs.ctx, cs.callHdr)if err != nil {if _, ok := err.(transport.PerformedIOError); ok {// Return without converting to an RPC error so retry code can// inspect.return err}return toRPCErr(err)}cs.attempt.s = s // 在这里设置stream,绕了一路 cs.attempt.p = &parser{r: s}return nil
}

通过上面代码分析,我们已经知道invoke方法中stream的初始化逻辑,接下来看看SendMsg和RecvMsg方法。

首先SendMsg和RecvMsg方法都属于ClientStream接口,该接口中还有其他的方法,主要用于http2通信。

// ClientStream defines the client-side behavior of a streaming RPC.
// ClientStream定义了rpc通信的客户端行为。
//
// All errors returned from ClientStream methods are compatible with the
// status package.
// 所有ClientStream方法返回的错误都适用于status包中的定义。type ClientStream interface {// Header returns the header metadata received from the server if there// is any. It blocks if the metadata is not ready to read.// Header会返回服务端的header元数据,如果元数据还未满足条件会一直阻塞。Header() (metadata.MD, error)// Trailer returns the trailer metadata from the server, if there is any.// It must only be called after stream.CloseAndRecv has returned, or// stream.Recv has returned a non-nil error (including io.EOF).// Trailer会返回trailer元数据,只会在stream.CloseAndRecv返回或者stream.Recv返回错误的时候才会被调用。Trailer() metadata.MD// CloseSend closes the send direction of the stream. It closes the stream// when non-nil error is met. It is also not safe to call CloseSend// concurrently with SendMsg.// CloseSend用于关闭发送方的流,如果遇到非空错误,也会关闭。CloseSend和SendMsg不是并发安全的。CloseSend() error// Context returns the context for this stream.//// It should not be called until after Header or RecvMsg has returned. Once// called, subsequent client-side retries are disabled.// Context 返回了stream的上下文,在Header或者RecvMsg返回之后不应该被调用。// 一旦被点用,后续的客户端重试都无效了。Context() context.Context// SendMsg is generally called by generated code. On error, SendMsg aborts// the stream. If the error was generated by the client, the status is// returned directly; otherwise, io.EOF is returned and the status of// the stream may be discovered using RecvMsg.// SendMsg通常被自动生成的代码调用。如果遇到错误,SendMsg就会停止stream。// 如果错误是因为客户端产生的,状态会立即返回,否则返回io.EOF,使用RecvMsg能够发现stream的状态。//// SendMsg blocks until://   - There is sufficient flow control to schedule m with the transport, or//   - The stream is done, or//   - The stream breaks.// SendMsg会阻塞的三种场景,流被终止,流完成,存在足够流控制// SendMsg does not wait until the message is received by the server. An// untimely stream closure may result in lost messages. To ensure delivery,// users should ensure the RPC completed successfully using RecvMsg.// SendMsg不会阻塞直到服务器接收到完整的数据,过早的流关闭会导致消息丢失。// 为了保证接受率,用户应该使用RecvMsg保证RPC成功结束。// It is safe to have a goroutine calling SendMsg and another goroutine// calling RecvMsg on the same stream at the same time, but it is not safe// to call SendMsg on the same stream in different goroutines. It is also// not safe to call CloseSend concurrently with SendMsg.// 在不同的协程中并发调用SendMsg和RecvMsg是可以的,但是如果在不同的协程中同时调用SendMsg不是并发安全的。// CloseSend和SendMsg也不是并发安全的。SendMsg(m interface{}) error// RecvMsg blocks until it receives a message into m or the stream is// done. It returns io.EOF when the stream completes successfully. On// any other error, the stream is aborted and the error contains the RPC// status.// RecvMsg会一直阻塞直到接收到所有的数据或者stream停止了。如果流成功完成了,会返回io.EOF。// 对于其他错误,流会被终止,并且错误会带有RPC状态。// // It is safe to have a goroutine calling SendMsg and another goroutine// calling RecvMsg on the same stream at the same time, but it is not// safe to call RecvMsg on the same stream in different goroutines.// SendMsg和RecvMsg调用是并发安全的,在不同的协程中调用RecvMsg不是并发安全。RecvMsg(m interface{}) error
}

分析一下invoke方法调用的SendMsg方法,

func (cs *clientStream) SendMsg(m interface{}) (err error) {// 省略部分代码 // 处理一下消息 hdr, payload, data, err := prepareMsg(m, cs.codec, cs.cp, cs.comp)if err != nil {return err}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, nilreturn err}// 采用重试的方式确保消息发送出去err = cs.withRetry(op, func() { cs.bufferForRetryLocked(len(hdr)+len(payload), op) })// 省略部分代码
}

(a *csAttempt) sendMsg发送消息中的Write属于type ClientTransport interface{}中的方法,在internal/transport/transport.go文件中。

func (a *csAttempt) sendMsg(m interface{}, hdr, payld, data []byte) error {cs := a.csif err := a.t.Write(a.s, hdr, payld, &transport.Options{Last: !cs.desc.ClientStreams}); err != nil {// ...}// ...return nil
}// Write formats the data into HTTP2 data frame(s) and sends it out. The caller
// should proceed only if Write returns nil.
// Writ方法将数据弄成数据帧的方式,然后发送出去,关注代码最后一句controlBuf.put功能。
func (t *http2Client) Write(s *Stream, hdr []byte, data []byte, opts *Options) error {if opts.Last {// If it's the last message, update stream state.if !s.compareAndSwapState(streamActive, streamWriteDone) {return errStreamDone}} else if s.getState() != streamActive {return errStreamDone}df := &dataFrame{streamID:  s.id,endStream: opts.Last,h:         hdr,d:         data,}if hdr != nil || data != nil { // If it's not an empty data frame, check quota.if err := s.wq.get(int32(len(hdr) + len(data))); err != nil {return err}}return t.controlBuf.put(df)
}// controlBuffer is a way to pass information to loopy.
// Information is passed as specific struct types called control frames.
// A control frame not only represents data, messages or headers to be sent out
// but can also be used to instruct loopy to update its internal state.
// It shouldn't be confused with an HTTP2 frame, although some of the control frames
// like dataFrame and headerFrame do go out on wire as HTTP2 frames.
// controlBuffer是将信息传递给loopy的一种方式,通过特殊的结构体形式传递的信息称为控制control frames。control frame不仅仅代表数据,消息,消息头,还可以通知loopy来更新内部状态。
// 注意不能和http2的帧搞混,尽管某些像dataFrame,headerFrame。
type controlBuffer struct {ch              chan struct{}done            <-chan struct{}mu              sync.MutexconsumerWaiting boollist            *itemListerr             error// transportResponseFrames counts the number of queued items that represent// the response of an action initiated by the peer.  trfChan is created// when transportResponseFrames >= maxQueuedTransportResponseFrames and is// closed and nilled when transportResponseFrames drops below the// threshold.  Both fields are protected by mu.transportResponseFrames inttrfChan                 atomic.Value // *chan struct{}
}

关于err = cs.withRetry(op, func() { cs.bufferForRetryLocked(len(hdr)+len(payload), op) })如何接收消息,这里详细说明一下,调用流程较长。

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 {// 看newAttemptLockedif 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 = tnewAttempt.done = donecs.attempt = newAttemptreturn 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) {// 选择满足条件的transportif 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 returnreturn}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 returnreturn}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 messagecode := http2ErrConvTab[se.Code]errorDetail := t.framer.fr.ErrorDetail()var msg stringif 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)}}}
}

客户端http2流程图如下,


接下来看看RecvMsg的调用逻辑。

// 当面使用了withRetry方法,用于重试
func (cs *clientStream) RecvMsg(m interface{}) error {// 省略一些代码,关注a.recvMsg(m, recvInfo)代码err := cs.withRetry(func(a *csAttempt) error {return a.recvMsg(m, recvInfo)}, cs.commitAttemptLocked)return err
}

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)
}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) {// 读请求头 if _, err := p.r.Read(p.header[:]); err != nil {return 0, nil, err}// 读消息体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-go客户端源码分析相关推荐

  1. TeamTalk客户端源码分析七

    TeamTalk客户端源码分析七 一,CBaseSocket类 二,select模型 三,样例分析:登录功能 上篇文章我们分析了network模块中的引用计数,智能锁,异步回调机制以及数据的序列化和反 ...

  2. 人人网官方Android客户端源码分析(1)

    ContentProvider是不同应用程序之间进行数据交换的标准API,ContentProvider以某种Uri的形式对外提供数据,允许其他应用访问或修改数据;其他应用程序使用ContentRes ...

  3. mosquitto客户端对象“struct mosquitto *mosq”管理下篇(mosquitto2.0.15客户端源码分析之四)

    文章目录 前言 5 设置网络参数 5.1 客户端连接服务器使用的端口号 `mosq->port` 5.2 指定绑定的网络地址 `mosq->bind_address` 5.3 客户端连接服 ...

  4. Eoe客户端源码分析---SlidingMenu的使用

    Eoe客户端源码分析及代码注释 使用滑动菜单SlidingMenu,单击滑动菜单的不同选项,可以通过ViewPager和PagerIndicator显示对应的数据内容. 0  BaseSlidingF ...

  5. WordPress Blog Android客户端源码分析(一)

    一直想找一个大型的Android开源项目进行分析,由于自身和导师课程需要选择了wordpress的Android客户端源码进行学习和解读.源码github官方下载地址:开源项目地址.分析源码的最佳手段 ...

  6. TeamTalk源码分析(十一) —— pc客户端源码分析

           --写在前面的话  在要不要写这篇文章的纠结中挣扎了好久,就我个人而已,我接触windows编程,已经六七个年头了,尤其是在我读研的三年内,基本心思都是花在学习和研究windows程序上 ...

  7. BT客户端源码分析之八:BT对等连接的建立过程

    作者:小马哥 日期:2005-01-09 rstevens2008 At hotmail.com 版权所有,未经允许,不得转载 转载请注明出处: http://www.wlm.com.cn/openi ...

  8. GRPC golang版源码分析之客户端(二)

    Table of Contents 1. 前言 2. 负载均衡 3. 相关链接 1 前言 前面一篇文章分析了一个grpc call的大致调用流程,顺着源码走了一遍,但是grpc中有一些特性并没有进行分 ...

  9. GRPC golang版源码分析之客户端(一)

    Table of Contents 1. 前言 2. 源码目录浏览 3. 客户端 4. 相关链接 1 前言 grpc是一个通用的rpc框架,用google实现,当然也有go语言的版本.在工作中主要用到 ...

最新文章

  1. ring0下的 fs:[124]
  2. PYG教程【五】链路预测
  3. Failed to resolve: org.jetbrains.kotlin:kotlin-stdlib-jre7:1.3.21
  4. 关于Xldown和Xlup的用法(Excel VBA)
  5. 报错 插入更新_window如何解决mysql数据量过大导致的报错
  6. ubuntu在 hdfs上创建一个文件夹_NAS上如何创建和使用加密文件夹?
  7. 聚类模型ari_7.9 聚类模型评估
  8. 发送邮件 显示对方服务器未响应,邮件对方服务器未响应
  9. Windows 10 无法访问共享的解决办法
  10. 北航超算运行matlab,计算性能超50万亿次破纪录,北航荣获ASC19世界大学生超算竞赛最高计算性能奖...
  11. Questasim覆盖率数据分析
  12. 车辆管理系统无法连接服务器,智能通道人员车辆管理软件常见问题
  13. perfect forward secrecy
  14. [c#]使用Fleck实现简单的WebSocket含兼容低版本IE
  15. 常用软件测试工具 ,赶紧收藏
  16. 九连环课程设计c语言,用C语言编程解九连环
  17. 判断通过微信、支付宝扫一扫进入的页面
  18. 信用卡客户风险评估-聚类分析实验报告(python)
  19. sql中插入带有单引号的数据
  20. 开发三年,靠这份Java面试宝典,拿到字节offer

热门文章

  1. PHP版本选择讲解:VC6与VC9,Thread Safe与None-Thread Safe等的选择
  2. Android的Dialog类设计的太糟糕了!
  3. 自动将存储过程转成C#代码的过程[转]
  4. jQuery环境搭建
  5. spring中@Value的使用(读取配置文件信息)
  6. Notepad++离线安装使用Markdown插件
  7. 超详细CookieSession的原理与用法
  8. python openstack vpc互通_深入浅出新一代云网络——VPC中的那些功能与基于OpenStack Neutron的实现(一)-简述与端口转发...
  9. Android屏幕禁止休眠的方法
  10. LINUX系统服务总结之三:nis服务器全集