gRPC-Server 啟動原始碼分析,一起弄懂它(八)

a_wei發表於2019-10-21

看完有收穫,肯定關注,點贊

gRPC已經斷斷續續寫了七篇文章了,但基本都是屬於gRPC的使用上,旨在幫助大家如何使用gRPC,瞭解gRPC的功能以及特性,並透過示例程式碼讓大家能快速入門,對開發人員而言,一個可執行的demo是最好的教程,文章連線和可執行demo如下,今天我們開始深入一點gRPC服務是怎麼啟動的,一起揭開他的神秘面紗。

1. gRPC的特性和背後設計的原則
2. gRPC的介面描述語言ProtoBuffer
3. gRPC之GoLang入門HelloWord
4. gRPC之流式呼叫原理http2協議分析
5. gRPC認證的多種方式實踐
6. gRPC攔截器那點事,希望幫到你
7. gRPC註冊中心,常用的註冊中心你懂了嗎?AP還是CP

https://github.com/sunpengwei1992/go_common/tree/master/grpc/helloworld_demo

一個gRPC-Server啟動主要以下幾行程式碼,如下一個簡單的gRPC-Server就啟動起來了,但其流程可不簡單,簡單的背後意味著封裝,一行一行來分析

func StartServer() {
   lis, _ := net.Listen("tcp", "127.0.0.1:8090")
   //建立一個grpc伺服器物件
   gRpcServer := grpc.NewServer()
   pb.RegisterHelloServiceServer(gRpcServer, &impl.HelloServiceServer{})
   //開啟服務端
   gRpcServer.Serve(lis)
}

下面這一行程式碼從表面很簡單,建立了一個grpServer例項,但是這個例項的引數以及入參的引數是非常多了,弄明白了這些引數的含義,後面程式碼的閱讀會舒暢很多

gRpcServer := grpc.NewServer()`

分析NewServer原始碼

// NewServer creates a gRPC server which has no service registered and has not started to accept requests yet.
//NewServer建立一個grpc伺服器,該伺服器沒有註冊服務,還不能開始接收請求
func NewServer(opt ...ServerOption) *Server {
       opts := defaultServerOptions
       for _, o := range opt {
          o.apply(&opts)
       }
       s := &Server{
          lis:    make(map[net.Listener]bool),
          opts:   opts,
          conns:  make(map[transport.ServerTransport]bool),
          m:      make(map[string]*service),
          quit:   grpcsync.NewEvent(),//退出事件
          done:   grpcsync.NewEvent(),//完成事件
          czData: new(channelzData),
       }
       s.cv = sync.NewCond(&s.mu)
       if EnableTracing {
          _, file, line, _ := runtime.Caller(1)
          s.events = trace.NewEventLog("grpc.Server"))
       }
       if channelz.IsOn() {
          s.channelzID = channelz.RegisterServer(&channelzServer{s}, "")
       }
       return s
}

上面的程式碼,總體四個步驟,當然,其中也有諸多細節,暫不深追究,後面會說到

  1. 接受入參引數切片,並把預設引數全部賦值到入參上
  2. 構造Server例項
  3. 判斷是否tracing(鏈路跟蹤),IsOn(返回channelz資料收集是否開啟)
  4. 返回server例項

然後我們說一下入參和返回Server的結構體的引數組成都是什麼含義?

ServerOption入參
type serverOptions struct {
   creds                 credentials.TransportCredentials  //cred證照
   codec                 baseCodec //序列化和反序列化
   cp                    Compressor //壓縮介面
   dc                    Decompressor //解壓縮介面
   unaryInt              UnaryServerInterceptor //一元攔截器
   streamInt             StreamServerInterceptor //流攔截器
   inTapHandle           tap.ServerInHandle //見下面文件
   statsHandler          stats.Handler //見下面文件
   maxConcurrentStreams  uint32 //http2中最大的併發流個數
   maxReceiveMessageSize int //最大接受訊息大小
   maxSendMessageSize    int //最大傳送訊息大小
   unknownStreamDesc     *StreamDesc
   keepaliveParams       keepalive.ServerParameters //長連線的server引數
   keepalivePolicy       keepalive.EnforcementPolicy
   //初始化stream的Window大小,下限值是64K,上限2^31
   initialWindowSize     int32 
   //初始化conn大小,一個conn會有多個stream,  等於上面的值 * 16 ,http2的限 制是大於0,預設一個連線有100個流,超過了就被拒絕
   initialConnWindowSize int32 
   writeBufferSize       int //寫緩衝大小
   readBufferSize        int //讀緩衝大小
   connectionTimeout     time.Duration //連線超時時間
   maxHeaderListSize     *uint32 //最大頭部大小
}

cred證照是介面grpc內部是有實現的程式碼包如下,我們使用是隻需要,呼叫方法傳入證照檔案就可以了

\google.golang.org\grpc@v1.23.1\credentials\credentials.go
creds, err := credentials.NewClientTLSFromFile("E:\\server.pem", "")

inTapHandle tap.ServerInHandle定義在伺服器端建立新流之前執行的函式,如果你定義的這個ServerInhandler返回非nil錯誤,則服務端不會建立流,並將一個rst_stream流傳送給具有refused_stream的客戶端,它旨在用於您不想服務端建立新的流浪費資源來接受新客戶端的情況,比如微服務常見的限流功能,注意,他是在每個conn的協程中執行,而不是在每個rpc的協程中執行中執行。
statsHandler stats.Handler這個介面中定義的方法主要是為統計做處理的,比如一次呼叫中的rpc和conn,預設的實現有如下

ClientHandler //主要是Client端服務的(rpc,conn)跟蹤和狀態統計
ServerHandler //主要是Server端服務的(rpc,conn)跟蹤和狀態統計
statshandler //grpc測試用的
Server引數
// Server is a gRPC server to serve RPC requests.
type Server struct {
   opts serverOptions //上面介紹的就是
   mu     sync.Mutex // guards following
   lis    map[net.Listener]bool //服務端的監聽地址
   //server transport是所有grpc伺服器端傳輸實現的通用介面。
   conns  map[transport.ServerTransport]bool 
   serve  bool  //表示服務是否開啟,在Serve()方法中賦值為true
   drain  bool  //在呼叫GracefulStop(優雅的停止服務)方法被賦值為true
   cv     *sync.Cond          // 當連線關閉以正常停止時發出訊號
   m      map[string]*service // service name -> service info
   events trace.EventLog //跟蹤事件日誌
   quit         *grpcsync.Event  //同步退出事件
   done       *grpcsync.Event //同步完成事件
   channelzRemoveOnce   sync.Once
   serveWG   sync.WaitGroup //counts active Serve goroutines for GracefulStop
   channelzID int64 // channelz unique identification number
   czData     *channelzData //儲存一些conn的自增id資料
}

分析RegisterHelloServiceServer原始碼

我們看到最上面的StartServer程式碼中呼叫了pb(proto buffer)的程式碼,這是自動生成的,這個方法的作用要把HelloService的實現註冊到Server上,我們看下面的程式碼,這個方法的入參有兩個,一個是NewServer建立的grpcServer例項,一個是HelloService的實現類,然後呼叫grpcServer的RegisterService的方法。

func RegisterHelloServiceServer(s *grpc.Server, srv HelloServiceServer) {
   s.RegisterService(&_HelloService_serviceDesc, srv)
}

RegisterService方法如下,registerservice將服務及其實現註冊到grpc伺服器。它是從idl(介面描述語言 Interface Description Lanauage)的程式碼中呼叫的。這必須是在呼叫SERVE方法之前呼叫。s.register(sd, ss) 方法最終是吧服務的名稱的和服務的描述資訊註冊到上面Server中的map[string]*service

func (s *Server) RegisterService(sd *ServiceDesc, ss interface{}) {
   ht := reflect.TypeOf(sd.HandlerType).Elem()
   st := reflect.TypeOf(ss)
   if !st.Implements(ht) {
      grpclog.Fatalf("grpc: Server.RegisterService found")
   }
   //註冊服務
   s.register(sd, ss)
}
//刪除了非核心程式碼
func (s *Server) register(sd *ServiceDesc, ss interface{}) {
   s.mu.Lock()
   defer s.mu.Unlock()
   //構造服務的例項
   srv := &service{
      server: ss, md: make(map[string]*MethodDesc),
      sd: make(map[string]*StreamDesc), mdata:sd.Metadata,
   }
   //放入方法
   for i := range sd.Methods {
      d := &sd.Methods[i]
      srv.md[d.MethodName] = d
   }
   //放入流
   for i := range sd.Streams {
      d := &sd.Streams[i]
      srv.sd[d.StreamName] = d
   }
   s.m[sd.ServiceName] = srv
}

分析gRpcServer.Serve(lis)原始碼

Server()方法就正式開始監聽客戶端的連線,並開啟協程處理客戶端連線,方法核心步驟如下

  1. 加鎖,初始化一些引數
  2. defer處理最後的資源情況
  3. for迴圈接受客戶端的連線,每一個客戶端的連線,開啟一個協程處理

func (s *Server) Serve(lis net.Listener) error {
    s.mu.Lock()
    s.serve = true
    s.serveWG.Add(1)
    //優雅的停止服務
    defer func() {
        s.serveWG.Done()
        if s.quit.HasFired() {
            <-s.done.Done()
        }
    }()
    //包裝連線物件,並宣告為true,代表有效
    ls := &listenSocket{Listener: lis}
    s.lis[ls] = true
    s.mu.Unlock()
    //清理資源
    defer func() {
        s.mu.Lock()
        if s.lis != nil && s.lis[ls] {
            ls.Close()
            delete(s.lis, ls)
        }
        s.mu.Unlock()
    }()
    //監聽客戶端連線
    for {
        rawConn, err := lis.Accept()
        s.serveWG.Add(1)
        //處理客戶端請求
        go func() {
            s.handleRawConn(rawConn)
            s.serveWG.Done()
        }()
    }
}

我們可以看到 grpcServer整體啟動流程是非常清晰的,而且go的程式碼閱讀起來也比較清爽,但命名上大多都是縮寫,需要結合上下文以及註釋來仔細觀察,今天的內容你掌握了嗎?希望大家還是親自去閱讀一下原始碼,結合文章一起對照學習,這樣記憶和理解也更深入,包括前面的一些基礎練習也要自己實踐,實踐才是真理。

歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來

【GoLang 那點事】gRPC-Server 啟動原始碼分析,一起弄懂它

本作品採用《CC 協議》,轉載必須註明作者和本文連結
那小子阿偉

相關文章