我們知道RPC框架是一個CS的架構,有服務的提供者,有服務的消費者,那麼一次RPC請求到底經歷也什麼了?這篇文章一起從原始碼揭祕gRPC的一次請求生命週期,從其中我們探尋RPC框架設計時一些比要的模組,進行抽象總結。,
文章較長,希望大家有耐心,看完有幫助求關注,分享,點贊
客戶端傳送一次請求的過程分析
在看客戶端如何發起一次請求時,我們先看看pb檔案,和生成的pb.go檔案,事實上常規的rpc請求和流式的rpc請求是不一樣的,這裡我們主要分析常規的rpc請求(也就是一次請求,一次響應)
//定義介面
service HelloService{
//一個簡單的rpc
rpc HelloWorld(HelloRequest) returns (HelloResponse){}
}
通過protobuffer工具生成pb.go檔案,這個檔案中包含的資訊比較多,這裡我們先主要看對HelloService服務的描述資訊
//其實就是建立一個ServiceDesc物件,這個結構體定義在server.go檔案中
var _HelloService_serviceDesc = grpc.ServiceDesc{
//服務的名稱
ServiceName: "proto.HelloService",
//服務的實現處理結構體
HandlerType: (*HelloServiceServer)(nil),
//正常的rpc,一次請求,一次響應的方法集
Methods: []grpc.MethodDesc{
{
MethodName: "HelloWorld",
//處理helloWorld的handler
Handler: _HelloService_HelloWorld_Handler,
},
},
//後設資料,也就是proto檔案
Metadata: "hello_world.proto",
}
我們從HelloWorld的RPC請求看起,看看這個一次請求,一次響應是怎麼執行的,首先在pb.go檔案中,我們看到客戶端使用的api的定義,如下程式碼
//這是protobuffer生成的客戶端api
type HelloServiceClient interface {
//一個簡單的rpc
HelloWorld(ctx context.Context, in *HelloRequest,
opts ...grpc.CallOption) (*HelloResponse, error)
}
這個HelloWorld方法接受三個引數:
- content 上下文引數,
- HelloRequest 請求入參,
- grpc.callOption
這裡著重說一下第三個引數,前兩個引數相信大家都知道是什麼意思,看gRPC中對這個引數的定義和描述,很清楚的知道這是一個介面,介面中定義了before方法和after方法,看如下注釋很容易明白的,我們可以自定義結構體實現自己想要處理的一些邏輯。
type CallOption interface {
// before is called before the call is sent to any server. If before
// returns a non-nil error, the RPC fails with that error
// 翻譯過來就是,在發起rpc請求呼叫之前呼叫before方法,如果一個非空的error錯誤,則這次rpc請求失敗
before(*callInfo) error
// after is called after the call has completed. after cannot return an
// error, so any failures should be reported via output parameters.
// 翻譯過來就是在rpc方法執行之後呼叫after方法,after不能返回錯誤,因此任何失敗都應該通過輸出引數來報告
after(*callInfo)
}
接下來我們看看客戶端api的實現,也是在pb.go檔案中,核心是Invoke方法,
type helloServiceClient struct {
cc *grpc.ClientConn
}
//建立一個該服務的客戶端,入參是客戶端和伺服器端建立的連線
func NewHelloServiceClient(cc *grpc.ClientConn) HelloServiceClient {
return &helloServiceClient{cc}
}
func (c *helloServiceClient) HelloWorld(ctx context.Context,
in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
//new一個返回物件
out := new(HelloResponse)
//呼叫客戶端連線的Inoke方法 (核心)
err := c.cc.Invoke(ctx, "/proto.HelloService/HelloWorld", in, out, opts...)
if err != nil {
return nil, err
}
//返回處理結果
return out, nil
}
當我們在程式碼中發起呼叫時,像如下程式碼一樣傳入引數,第三個引數我們可以傳入一個空的CallOption
,這是grpc提供的預設實現,這個實現在rpc_util.go檔案中。事實上,grpc提供了很多預設實現,都在這個檔案中,這不是本次的重點,就不展開說了
//連線grpc的服務端
conn, err := grpc.Dial("127.0.0.1:8090", grpc.WithInsecure())
//建立客戶端存根物件,呼叫的這個方法在生成的pb.go檔案中
c := pb.NewHelloServiceClient(conn)
//發起呼叫
c.HelloWorld(context.BackContext(),new(HelloRequest),grpc.EmptyCallOption{})
最後我們深入invoke方法中做了什麼,invoke方法在call.go檔案中
//發起rpc呼叫並等待響應
func (cc *ClientConn) Invoke(ctx context.Context, method string, args,
reply interface{}, opts ...CallOption) error {
//把客戶端的攔截器和和呼叫方法入參的攔截器合併(就是放到一個切片中)
opts = combine(cc.dopts.callOptions, opts)
//攔截器不為空就呼叫這個方法
if cc.dopts.unaryInt != nil {
//攔截器中還是會呼叫invoke方法(也就是下面的方法)
return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
}
//否則呼叫invoke
return invoke(ctx, method, args, reply, cc, opts...)
}
在invoke方法中主要做了如下如下事情
- 建立客戶端流物件(在這個方法中傳送前執行befor方法),這個方法中主要初始化一些流物件引數,比如超時時間,傳送最大訊息大小,接受最大訊息大小,
- 傳送請求
- 接受服務端響應 (在接受響應後執行after方法)
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)
}
我們進入到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
}
//建立傳送訊息的方法
op := func(a *csAttempt) error {
//真正的訊息傳送是在這個方法中
err := a.sendMsg(m, hdr, payload, data)
m, data = nil, nil
return err
}
//開始執行傳送,帶重試功能
err = cs.withRetry(op,
func() {
cs.bufferForRetryLocked(len(hdr)+len(payload), op)
}
)
return
}
我們再進入RecvMsg中看看客戶端是如何接受訊息的
func (cs *clientStream) RecvMsg(m interface{}) error {
var recvInfo *payloadInfo
if cs.binlog != nil {
recvInfo = &payloadInfo{}
}
err := cs.withRetry(func(a *csAttempt) error {
//整的開始接受服務端結果,並且反序列化,填充到m物件上,m就是返回值
return a.recvMsg(m, recvInfo)
}, cs.commitAttemptLocked)
if err != nil || !cs.desc.ServerStreams {
//這裡面回撥用after方法
cs.finish(err)
}
return err
}
服務端處理一次請求的過程分析
在之前的文章gRPC-Server啟動做了哪些事,詳細分析了gRPCServer的啟動流程,這篇文章我們接著看看服務端監聽到一個客戶端連線之後,是如何處理這個請求的。
在grpc.Server(listener)
中有如下片段程式碼
//每來一個連線開啟一個協程開始處理客戶端請求
go func() {
//開始處理客戶端連線
s.handleRawConn(rawConn)
}()
我們主要分析的是在handleRwConn方法中做了哪些事
- 設定連線建立的超時時間
- 許可權認證
- 建立基於http2的連線
- 處理請求
func (s *Server) handleRawConn(rawConn net.Conn) {
//設定連線超時時間
rawConn.SetDeadline(time.Now().Add(s.opts.connectionTimeout))
//許可權認證(是tls認證或者證照認證,不是我們自定義的許可權認證)
conn, authInfo, err := s.useTransportAuthenticator(rawConn)
// 建立基於http2的連線(主要是建立一個http2的Sever伺服器),要看懂這一塊需要深入瞭解http2協議
st := s.newHTTP2Transport(conn, authInfo)
if st == nil {
return
}
rawConn.SetDeadline(time.Time{})
if !s.addConn(st) {
return
}
//開啟新的協程處理請求
go func() {
s.serveStreams(st)
s.removeConn(st)
}()
}
繼續深入ServerStreams()方法看看是如何處理客戶端請求的
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()
}
HandleStream方法中主要是迴圈讀取http2協議傳送的各種幀,然後交給不同的方法去處理,其中MetaHeadersFrame幀會觸發呼叫服務端的服務實現,traceCtx主要負責跟蹤執行過程。這裡省略很多程式碼,感興趣的去閱讀原始碼,文章裡就不貼上了。
func (t *http2Server) HandleStreams(handle func(*Stream),
traceCtx func(context.Context, string) context.Context) {
defer close(t.readerDone)
//這是個沒有條件的for迴圈
for {
t.controlBuf.throttle()
//獲取http2協議中的幀(不懂的可以翻看gRPC之流式呼叫原理http2協議分析)
frame, err := t.framer.fr.ReadFrame()
atomic.StoreUint32(&t.activity, 1)
//判斷幀的型別然後去處理
switch frame := frame.(type) {
case *http2.MetaHeadersFrame:
//這個幀是真正去呼叫服務實現了,這個方法會呼叫上面的 s.handleStream(st, stream, s.traceInfo(st, stream))
if t.operateHeaders(frame, handle, traceCtx) {
t.Close()
break
}
}
}
}
最後我們看看這個真正呼叫我們自己業務服務程式碼的方法是做了什麼,省略很多非核心的程式碼,這樣流程比較清晰s.handleStream(st, stream, s.traceInfo(st, stream))
func (s *Server) handleStream(t transport.ServerTransport,
stream *transport.Stream, trInfo *traceInfo) {
//從流中獲取服務端需要執行的方法後設資料描述
sm := stream.Method()
//提取服務名稱,方法名稱
service := sm[:pos]
method := sm[pos+1:]
//判斷服務是否存在,存在則得到具體的服務
srv, knownService := s.m[service]
if knownService {
//這個是一元方法執行 (一次請求,一次響應)
if md, ok := srv.md[method]; ok {
//此時的md就是最上面的ServiceDesc中的Handler,在processUnaryRPC中調 用這個handler,就是呼叫了我們的業務邏輯服務程式碼。
s.processUnaryRPC(t, stream, srv, md, trInfo)
return
}
//這個是流式方法執行(多次請求,多次響應)
if sd, ok := srv.sd[method]; ok {
s.processStreamingRPC(t, stream, srv, sd, trInfo)
return
}
}
}
總結
深入閱讀進去,你會發現原始碼並不是特別難懂,關鍵在於踏出第一步,上面分析了grpc從客戶端發起請求到服務端接受處理的全流程,中間也有很多細節並沒有說,比如鑑權,比如建立http2服務,攔截器執行,trace跟蹤等,尤其是錯誤處理,但本篇文章重點是帶領大家貫穿整個流程,把從客戶端發起請求到服務端處理銜接起來,並不是把所有細節說明白,一篇文章也說不明白,最後我用一張圖表述整個流程,讓大家更加清晰的理解。
歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來
本作品採用《CC 協議》,轉載必須註明作者和本文連結