gRPC-transport包源碼分析

gRPC是基于HTTP/2標準和proto協議開發的,gRPC的很多特性都依賴于HTTP/2標準提供。gRPC設計的四種模式是基于底層HTTP/2的流的概念。transport包是基于HTTP/2標準的實現,提供了流控等特性。

流控

transport提供基于connection和stream的兩級流控。

-------------------------------------gRPC流控默認值----------------------------------------------
    defaultWindowSize = 65535 //64K
    initialWindowSize     = defaultWindowSize      // for an RPC
    initialConnWindowSize = defaultWindowSize * 16 // for a connection
-------------------------------------流控數據結構------------------------------------------------
type inFlow struct {
    //流控限制未處理的數據的數量
    limit uint32
    mu sync.Mutex
    //pendingData包含所有收到但未被應用消費的數據
    pendingData uint32
    //pendingUpdate包含被消費但為發送更新窗口的數量,減少窗口更新的頻率
    pendingUpdate uint32
}
//真實的流控處理函數,server在接收到client的請求后會先
//檢查pendingData+pendingUpdate是否超過limit限制
func (f *inFlow) onData(n uint32) error {
    f.mu.Lock()
    defer f.mu.Unlock()
    f.pendingData += n
    if f.pendingData+f.pendingUpdate > f.limit {
        return fmt.Errorf("received %d-bytes data exceeding the limit %d bytes", f.pendingData+f.pendingUpdate, f.limit)
    }
    return nil
}
//http2標準中規定:針對控制類的frame,為了確保能夠得到高優先級的處理不做流控。DataFrame的流控處理在如下的函數中進行處理。
----------------------------------server端處理流------------------------------------------------
//server端handleData用于接收dataFrame
func (t *http2Server) handleData(f *http2.DataFrame) {
    size := len(f.Data())
    //針對connection的流控,如果client和server在該connection的負載大于16 * 64K,server會主動斷開與client之間的連接。
    if err := t.fc.onData(uint32(size)); err != nil {
        //onData函數實現見流控的數據結構
        grpclog.Printf("transport: http2Server %v", err)
        //超過負載,直接關閉connection
        t.Close()
        return
    }
    // 選擇正確的流進行處理
    s, ok := t.getStream(f)
    if !ok {
        if w := t.fc.onRead(uint32(size)); w > 0 {
          //更新流控窗口的大小
            t.controlBuf.put(&windowUpdate{0, w})
        }
        return
    }
    if size > 0 {
        s.mu.Lock()
        if s.state == streamDone {
            s.mu.Unlock()
            // stream已經被關閉,需要更新流控窗口
            if w := t.fc.onRead(uint32(size)); w > 0 {
                t.controlBuf.put(&windowUpdate{0, w})
            }
            return
        }
      //同一連接上的不同stream具有競爭關系,提供了strean級的流控
        if err := s.fc.onData(uint32(size)); err != nil {
            //onData()函數實現見流控數據結構
            s.mu.Unlock()
            //關閉超過流控限制的stream
            t.closeStream(s)
            //通知client再建立streamID相同的stream
            t.controlBuf.put(&resetStream{s.id, http2.ErrCodeFlowControl})
            return
        }
        s.mu.Unlock()
        data := make([]byte, size)
        copy(data, f.Data())
        s.write(recvMsg{data: data})
    }
    if f.Header().Flags.Has(http2.FlagDataEndStream) {
        s.mu.Lock()
        if s.state != streamDone {
            s.state = streamReadDone
        }
        s.mu.Unlock()
        s.write(recvMsg{err: io.EOF})
    }
}

RPC調用的執行過程

以unary模式的rpc調用為例分析一次RPC請求在gRPC中的流轉過程,其他三種模式底層調用的函數與unary模式相同(四種模式從底層的HTTP/2分析都是stream,并且仍然是一套request和response的實現)。

: 以下源碼分析部分均是以grpc/example/route_guide為例進行分析。對其他模式感興趣的讀者可自行分析。

unary模式的RPC請求在gRPC中的執行過程
------------------------------------------proto的聲明-------------------------------------------
service RouteGuide {
  rpc GetFeature(Point) returns (Feature) {}
}
------------------------------------------pb.go源碼---------------------------------------------
func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) {
    out := new(Feature)
  // -->/routeguide.RouteGuide/GetFeature ->/package/server/method
    err := grpc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, c.cc, opts...)
    if err != nil {
        return nil, err
    }
    return out, nil
}
//以下代碼去掉錯誤處理和非關鍵函數的調用
//以下代碼分析的是grpc client端如何發送request到server
-----------------------------------------grpc-client代碼----------------------------------------
func invoke(ctx context.Context, method string, args, reply interface{}, cc *ClientConn, opts ...CallOption) (err error) {
    c := defaultCallInfo //構造rpc調用的defaultCallInfo并根據用戶傳入的信息進行填充
    topts := &transport.Options{
        Last:  true,
        Delay: false,
    }
    for {
        var (
            err    error
            t      transport.ClientTransport
            stream *transport.Stream
            put func()
        )
        //callHdr攜帶詳細的RPC調用信息,如Method->/routeguide.RouteGuide/GetFeature
        callHdr := &transport.CallHdr{
            Host:   cc.authority,
            Method: method,
        }
        gopts := BalancerGetOptions{
            BlockingWait: !c.failFast,
        }
        t, put, err = cc.getTransport(ctx, gopts)
        if err != nil {
            if _, ok := err.(*rpcError); ok {
                return err
            }
            //非failFast情況下,err為以下兩種情況會重試
            if err == errConnClosing || err == errConnUnavailable {
                if c.failFast {
                    return Errorf(codes.Unavailable, "%v", err)
                }
                continue
            }
            return Errorf(codes.Internal, "%v", err)
        }
        //將client請求信息發送,并等待server返回
        stream, err = sendRequest(ctx, cc.dopts.codec, cc.dopts.cp, callHdr, t, args, topts)
        if err != nil {
            if put != nil {
                put()
                put = nil
            }
            if _, ok := err.(transport.ConnectionError); ok || err == transport.ErrStreamDrain {
                if c.failFast {
                    return toRPCErr(err)
                }
                continue
            }
            return toRPCErr(err)
        }
        //在sendRequest創建的stream上等待server返回response
        err = recvResponse(cc.dopts, t, &c, stream, reply)
        if err != nil {
            if put != nil {
                put()
                put = nil
            }
            if _, ok := err.(transport.ConnectionError); ok || err == transport.ErrStreamDrain {
                if c.failFast {
                    return toRPCErr(err)
                }
                continue
            }
            return toRPCErr(err)
        }
        //關閉創建的stream
        t.CloseStream(stream, nil)
        if put != nil {
            put()
            put = nil
        }
        return Errorf(stream.StatusCode(), "%s", stream.StatusDesc())
    }
}
----------------------------------------------sendRequest()說明--------------------------------
func sendRequest(ctx context.Context, codec Codec, compressor Compressor, callHdr *transport.CallHdr, t transport.ClientTransport, args interface{}, opts *transport.Options) (_ *transport.Stream, err error) {  
     //根據callHdr中包含的host和method信息創建對應的stream
    //函數具體實現-transport/http2_client.go/http2Client.NewStream()
    stream, err := t.NewStream(ctx, callHdr)
    //序列化消息并定義消息頭
    //消息頭=5yte=1byte(msg是否壓縮) + 4byte(msg長度)
    //函數具體實現-rpc_util.go
    outBuf, err := encode(codec, args, compressor, cbuf)
    //將outBuf按照http2幀的大小分幀并發送到對端,下面會對該函數具體分析
    err = t.Write(stream, outBuf, opts)
    //發送成功,返回該stream,用于接收response
    return stream, nil
}
------------------------------------ClientTransport.Write()說明---------------------------------
//真正將message分幀在指定的stream上傳輸的函數如下,將對該函數進行詳細分析
func (t *http2Client) Write(s *Stream, data []byte, opts *Options) error {
    r := bytes.NewBuffer(data)
    for {
        var p []byte
        if r.Len() > 0 {
            size := http2MaxFrameLen
            s.sendQuotaPool.add(0)
            // 等待stream的流控上有配額發送數據,stream.sendQuotaPool=65535
            sq, err := wait(s.ctx, s.done, s.goAway, t.shutdownChan, s.sendQuotaPool.acquire())
            if err != nil {
                return err
            }
            t.sendQuotaPool.add(0)
            // 等待connection的流控有配額去發送數據,t.sendQuotaPool= 65535 * 16
            tq, err := wait(s.ctx, s.done, s.goAway, t.shutdownChan, t.sendQuotaPool.acquire())
            if err != nil {
                if _, ok := err.(StreamError); ok || err == io.EOF {
                    t.sendQuotaPool.cancel()
                }
                return err
            }
            if sq < size {
                size = sq
            }
            if tq < size {
                size = tq
            }
            p = r.Next(size)
            ps := len(p)
            if ps < sq {
                // 返回stream預留超額的配額數量
                s.sendQuotaPool.add(sq - ps)
            }
            if ps < tq {
                // 返回connection預留超額的配額數量
                t.sendQuotaPool.add(tq - ps)
            }
        }
        var (
            endStream  bool
            forceFlush bool
        )
        //判斷是否為最后一幀l
        if opts.Last && r.Len() == 0 {
            endStream = true
        }
        // 表明這將有一個writer將要去寫data frame
        t.framer.adjustNumWriters(1)
        // 釋放t.writableChan上加的鎖,獲得在該transport上寫的權利,確保只有一個調用者可以調用t.framer.writeData()函數。
        if _, err := wait(s.ctx, s.done, s.goAway, t.shutdownChan, t.writableChan); err != nil {
            if _, ok := err.(StreamError); ok || err == io.EOF {
                // 釋放connection上預留的配額數量
                t.sendQuotaPool.add(len(p))
            }
            if t.framer.adjustNumWriters(-1) == 0 {
                // 如果該Writer是這一批的最后一個有責任去刷新http2.frames的緩存區
                //將刷新的請求排入一個隊列而不是直接刷新合一避免和其他的Writer或者刷新請求的競爭
                t.controlBuf.put(&flushIO{})
            }
            return err
        }
        select {
        case <-s.ctx.Done():
            t.sendQuotaPool.add(len(p))
            if t.framer.adjustNumWriters(-1) == 0 {
                t.controlBuf.put(&flushIO{})
            }
           //再次為該transport加鎖
            t.writableChan <- 0
            return ContextErr(s.ctx.Err())
        default:
        }
        if r.Len() == 0 && t.framer.adjustNumWriters(0) == 1 {
            // 強制刷新因為這是grpc message的最后一個數據幀
            //對于調用者來說此刻僅僅只有一個writer
            forceFlush = true
        }
        //如果t.framer.writeData失敗,所有等待處理的stream將會在http2Clinet.Close()函數中進行處理,此處不必顯示調用CloseStream()
      //writeData()不會并發被調用,確保server端收到的frame不會亂序(不會出現dataframe早于headerframe先到)
        if err := t.framer.writeData(forceFlush, s.id, endStream, p); err != nil {
          //writeData()增加二進制幀的頭部,函數實現-net/http2/frame.go
            t.notifyError(err)
            return connectionErrorf(true, err, "transport: %v", err)
        }
        if t.framer.adjustNumWriters(-1) == 0 {
            t.framer.flushWrite()
        }
      //再次為該transport加鎖
        t.writableChan <- 0
        if r.Len() == 0 {
            break
        }
    }
    if !opts.Last {
        return nil
    }
    s.mu.Lock()
    if s.state != streamDone {
      //更新stream的狀態
        s.state = streamWriteDone
    }
    s.mu.Unlock()
    return nil
}
//以下代碼是分析grpc-server接收client的請求后內部的處理流程
---------------------------------------grpc-server代碼------------------------------------------
//serve函數在net.Listener接收客戶端的連接,創建一個新的ServerTransport和service goroutine為每個連接,服務goroutine讀取gRPC請求,然后調用server中注冊的函數。
func (s *Server) Serve(lis net.Listener) error {
    
    s.lis[lis] = true

    for {
        rawConn, err := lis.Accept()
        if err != nil {
            s.mu.Lock()
            s.printf("done serving; Accept = %v", err)
            s.mu.Unlock()
            return err
        }
        //開始一個單獨的goroutine處理client的連接-rawConn
        //繼續for循環等待其他client的到來
        go s.handleRawConn(rawConn)
    }
}
//handleRawConn運行在獨立的goroutine,并且處理已經接收連接但未執行任何I/O操作的連接
func (s *Server) handleRawConn(rawConn net.Conn) {
    conn, authInfo, err := s.useTransportAuthenticator(rawConn)
    if err != credentials.ErrConnDispatched {
            rawConn.Close()
        }
        return
    }
    if s.opts.useHandlerImpl {
        s.serveUsingHandler(conn)
    } else {
        s.serveNewHTTP2Transport(conn, authInfo)
    }
}
//serveNewHTTP2Transport建立一個新的HTTP/2 tranport并且為在該transport上的流提供服務
func (s *Server) serveNewHTTP2Transport(c net.Conn, authInfo credentials.AuthInfo) {
    //調用transport/http2_server.go
    st, err := transport.NewServerTransport("http2", c, 2, authInfo)
    if !s.addConn(st) {
        st.Close()
        return
    }
   //在transport上接收client發送stream并進行處理的函數
    s.serveStreams(st)
}

func (s *Server) serveStreams(st transport.ServerTransport) {
    defer s.removeConn(st)
    defer st.Close()
    var wg sync.WaitGroup
   //transport.ServerTranport下的st.HandleStreams處理client發送的stream
    st.HandleStreams(func(stream *transport.Stream) {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s.handleStream(st, stream, s.traceInfo(st, stream))
        }()
    })
    wg.Wait()
}
----------------------------transport/http2Server.HanleStreams()分析----------------------------
func (t *http2Server) HandleStreams(handle func(*Stream)) {
    // 檢查client 發送的preface是否合法
    preface := make([]byte, len(clientPreface))
    if _, err := io.ReadFull(t.conn, preface); err != nil {
        grpclog.Printf("transport: http2Server.HandleStreams failed to receive the preface from client: %v", err)
        t.Close()
        return
    }
    if !bytes.Equal(preface, clientPreface) {
        grpclog.Printf("transport: http2Server.HandleStreams received bogus greeting from client: %q", preface)
        t.Close()
        return
    }

    frame, err := t.framer.readFrame()
    if err == io.EOF || err == io.ErrUnexpectedEOF {
        t.Close()
        return
    }
    if err != nil {
        grpclog.Printf("transport: http2Server.HandleStreams failed to read frame: %v", err)
        t.Close()
        return
    }
    //讀取client發送的SettingFrame
    sf, ok := frame.(*http2.SettingsFrame)
    if !ok {
        grpclog.Printf("transport: http2Server.HandleStreams saw invalid preface type %T from client", frame)
        t.Close()
        return
    }
    //根據SettingFrame的內容進行設置
    t.handleSettings(sf)
    //讀取client發送的request內容
    for {
        frame, err := t.framer.readFrame()
        if err != nil {
            if se, ok := err.(http2.StreamError); ok {
                t.mu.Lock()
                s := t.activeStreams[se.StreamID]
                t.mu.Unlock()
                if s != nil {
                    t.closeStream(s)
                }
                t.controlBuf.put(&resetStream{se.StreamID, se.Code})
                continue
            }
            if err == io.EOF || err == io.ErrUnexpectedEOF {
                t.Close()
                return
            }
            grpclog.Printf("transport: http2Server.HandleStreams failed to read frame: %v", err)
            t.Close()
            return
        }
        switch frame := frame.(type) {
        case *http2.MetaHeadersFrame:
            //t.operateHeaders函數解碼headers內容,并將傳輸該frame的stream進行記錄
            //函數實現包括根據stream攜帶的callHdr信息,如何路由到grpc.Server中注冊server具體實現method的過程
            //函數實現-transport/http2_server.go operateHeader()函數
            if t.operateHeaders(frame, handle) {
                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:
        default:
            grpclog.Printf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame)
        }
    }
}

func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream)) (close bool) {
    buf := newRecvBuffer()
    //保存client傳輸的stream信息
    s := &Stream{
        id:  frame.Header().StreamID,
        st:  t,
        buf: buf,
        fc:  &inFlow{limit: initialWindowSize},
    }

    var state decodeState
    for _, hf := range frame.Fields {
        state.processHeaderField(hf)
    }
    if err := state.err; err != nil {
        if se, ok := err.(StreamError); ok {
            t.controlBuf.put(&resetStream{s.id, statusCodeConvTab[se.Code]})
        }
        return
    }

    if frame.StreamEnded() {
        s.state = streamReadDone
    }
    s.recvCompress = state.encoding
    if state.timeoutSet {
        s.ctx, s.cancel = context.WithTimeout(context.TODO(), state.timeout)
    } else {
        s.ctx, s.cancel = context.WithCancel(context.TODO())
    }
  
    if uint32(len(t.activeStreams)) >= t.maxStreams {
        t.mu.Unlock()
        t.controlBuf.put(&resetStream{s.id, http2.ErrCodeRefusedStream})
        return
    }
    //對stream的合法性進行檢查
    if s.id%2 != 1 || s.id <= t.maxStreamID {
        t.mu.Unlock()
        grpclog.Println("transport: http2Server.HandleStreams received an illegal stream id: ", s.id)
        return true
    }
    t.maxStreamID = s.id
    s.sendQuotaPool = newQuotaPool(int(t.streamSendQuota))
    t.activeStreams[s.id] = s
    t.mu.Unlock()
    s.windowHandler = func(n int) {
        t.updateWindow(s, uint32(n))
    }
    //調用server.go serveStreams()傳入的handle去處理server端接收的stream
    //handle()會調用server.go handleStream()路由到server端真正實現的函數
    handle(s)
    return
}
//handleData處理server端接收到數據幀
func (t *http2Server) handleData(f *http2.DataFrame) {
    size := len(f.Data())
    //檢查transport的流控
    if err := t.fc.onData(uint32(size)); err != nil {
        grpclog.Printf("transport: http2Server %v", err)
        t.Close()
        return
    }
    s, ok := t.getStream(f)
    if !ok {
        if w := t.fc.onRead(uint32(size)); w > 0 {
            t.controlBuf.put(&windowUpdate{0, w})
        }
        return
    }
    if size > 0 {
        s.mu.Lock()
        if s.state == streamDone {
            s.mu.Unlock()
            //檢查stream的流控
            if w := t.fc.onRead(uint32(size)); w > 0 {
                t.controlBuf.put(&windowUpdate{0, w})
            }
            return
        }
        if err := s.fc.onData(uint32(size)); err != nil {
            s.mu.Unlock()
            t.closeStream(s)
            t.controlBuf.put(&resetStream{s.id, http2.ErrCodeFlowControl})
            return
        }
        s.mu.Unlock()
        data := make([]byte, size)
        copy(data, f.Data())
        s.write(recvMsg{data: data})
    }
    if f.Header().Flags.Has(http2.FlagDataEndStream) {
        s.mu.Lock()
        if s.state != streamDone {
            s.state = streamReadDone
        }
        s.mu.Unlock()
        s.write(recvMsg{err: io.EOF})
    }
}

以上源碼分析一次gRPC調用,從client端如何發送請求到grpc.server端如何路由到server端注冊函數的所有過程。

問題總結:

1.grpc的http/2的stream流是如何變化的?

答:unary模式的stream的創建、刪除都是由gRPC控制的,剩下的三種模式是將stream的很多操作暴露給用戶層,由用戶自行控制,但sendRequset和recvResponse的流程和unary模式處理相同。筆者測試發現grpc用到的都是client端的stream,server端的stream在gRPC中并未使用。client端發起的stream都是基數開始的,并且最大值為2^31-1,如果client的streamID超過限制,server端會斷開與client的連接。測試結果如下:

//2^31的最大取值2147483648
client stream id 2147483649
2017/08/04 10:44:17 transport: http2Client.notifyError got notified that the client transport was broken invalid stream ID.
2017/08/04 10:44:17 &{0xc4201787e0}.RouteChat(_) = _, rpc error: code = 13 desc = transport: invalid stream ID
exit status 1
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,787評論 18 139
  • 轉自:http://blog.csdn.net/kesonyk/article/details/50924489 ...
    晴天哥_王志閱讀 24,884評論 2 38
  • gRPC 是一個高性能、通用的開源RPC框架,其由 Google 主要面向移動應用開發并基于HTTP/2 協議標準...
    劉琨_10f5閱讀 1,311評論 4 6
  • 經過很長一段時間的開發,TiDB 終于發了 RC3。RC3 版本對于 TiKV 來說最重要的功能就是支持了 gRP...
    siddontang閱讀 41,976評論 7 80
  • gRPC 是基于 HTTP/2 協議的,要深刻理解 gRPC,理解下 HTTP/2 是必要的。本篇文章會先簡單介紹...
    tcgx閱讀 1,287評論 1 1