gRPC-go源碼剖析之grpc服務(wù)器端在啟動(dòng)時(shí)都做了什么事情

這次分享一下當(dāng)grpc服務(wù)器在啟動(dòng)時(shí)都做了什么事情?
可以自己先思考一下,假設(shè)讓我們自己去開(kāi)發(fā)一個(gè)簡(jiǎn)單版本的grpc服務(wù)器端啟動(dòng)時(shí)都會(huì)做什么事情呢?
a.一些初始化工作
b.監(jiān)聽(tīng)某個(gè)端口
c.注冊(cè)服務(wù)端提供的服務(wù)
。。。。。
好了,接下來(lái)看一下grpc-go框架服務(wù)器端啟動(dòng)時(shí)的流程圖:

grpc服務(wù)器端啟動(dòng)時(shí)都做了哪些事情

在下面的章節(jié)中只是介紹了常用的初始化組件,有些功能需要手動(dòng)顯示的調(diào)用,或者import導(dǎo)入才能初始化或者注冊(cè),比方說(shuō)grpc-go/encoding/gzip/gzip.go文件中的gzip壓縮器需要手動(dòng)導(dǎo)入,因此就不再一一介紹了。
一個(gè)鏈接請(qǐng)求,對(duì)應(yīng)一個(gè)http2Server對(duì)象,一個(gè)幀接收器,一個(gè)幀發(fā)送器;

1、注冊(cè)、初始化工作

下面幾個(gè)小節(jié),僅僅列出了grpc-go源碼中哪些文件實(shí)現(xiàn)了注冊(cè)、初始化等工作。
至于詳細(xì)原理介紹,會(huì)再后面的章節(jié)中分享。

1.1、注冊(cè)服務(wù)

通過(guò)下面的形式,可以將提供的服務(wù)注冊(cè)到grpc服務(wù)器端,以供客戶端調(diào)用;
這里我們以源碼中自帶的heloworld為例,將SayHello服務(wù)注冊(cè)到grpc服務(wù)器端:


注冊(cè)服務(wù)

1.2、解析器初始化

在grpc框架里內(nèi)置了幾種解析器,也可以自定義解析器;
像passthrough、dns解析器在grpc服務(wù)器啟動(dòng)時(shí)會(huì)自己注冊(cè);
像manual、xds解析器,需要在代碼里顯示注冊(cè)才生效;

1.2.1、passthrough解析器(默認(rèn)使用,啟動(dòng)時(shí)自己注冊(cè))

.../internal/resolver/passthrough/passthrough.go文件中

func init() {
   resolver.Register(&passthroughBuilder{})
}

1.2.2、dns解析器(啟動(dòng)時(shí)自己注冊(cè))

.../internal/resolver/dns/dns_resolver.go文件中

func init() {
   resolver.Register(NewBuilder())
}

1.2.3、Manual解析器(需要手動(dòng)顯示的注冊(cè))

.../resolver/manual/manual.go文件中

func GenerateAndRegisterManualResolver() (*Resolver, func()) {
   scheme := strconv.FormatInt(time.Now().UnixNano(), 36)
   r := NewBuilderWithScheme(scheme)
   resolver.Register(r)
   return r, func() { resolver.UnregisterForTesting(scheme) }
}

如果想要使用manual解析器的話,需要手動(dòng)顯示的注冊(cè)一下,如下參考例子:
.../examples/features/health/client/main.go文件

1.func main() {
2.   flag.Parse()

3.   r, cleanup := manual.GenerateAndRegisterManualResolver()
4.   defer cleanup()
5.   r.InitialState(resolver.State{
6.      Addresses: []resolver.Address{
7.         {Addr: "localhost:50051"},
8.         {Addr: "localhost:50052"},
9.      },
10.   })

11.   address := fmt.Sprintf("%s:///unused", r.Scheme())
// -------省略不相關(guān)代碼------

核心代碼說(shuō)明:
1.第3行,手動(dòng)注冊(cè)指定解析器
.../examples/features/debugging/client/main.go文件:

1.func main() {
2.   /***** Set up the server serving channelz service. *****/
3.   lis, err := net.Listen("tcp", ":50052")
4.   if err != nil {
5.      log.Fatalf("failed to listen: %v", err)
6.   }
7.   defer lis.Close()
8.   s := grpc.NewServer()
9.   service.RegisterChannelzServiceToServer(s)
10.   go s.Serve(lis)
11.   defer s.Stop()

12.   /***** Initialize manual resolver and Dial *****/
13.   r, rcleanup := manual.GenerateAndRegisterManualResolver()
14.   defer rcleanup()
15.   // Set up a connection to the server.
16.   conn, err := grpc.Dial(r.Scheme()+":///test.server", grpc.WithInsecure(), grpc.WithBalancerName("round_robin"))
17.   if err != nil {
18.      log.Fatalf("did not connect: %v", err)
19.   }
20.   defer conn.Close()
21.// ------省略不相關(guān)代碼------

主要代碼說(shuō)明:
1.第13行,手動(dòng)注冊(cè)指定解析器

1.2.4、xds 解析器(需要手動(dòng)顯示的注冊(cè))

xds解析器,一般情況下,grpc服務(wù)器啟動(dòng)時(shí)并沒(méi)有注冊(cè)xds解析器,需要手動(dòng)的注冊(cè)。
.../xds/internal/resolver/xds_resolver.go文件中初始化:

func init() {
   resolver.Register(&xdsResolverBuilder{})
}

如何注冊(cè)xds解析器呢?
在.../xds/xds.go文件中:

1.package xds

2.import (
3.   _ "google.golang.org/grpc/xds/internal/balancer" // Register the balancers.
4.   _ "google.golang.org/grpc/xds/internal/resolver" // Register the xds_resolver
5.)

主要流程說(shuō)明:
1.第4行,注冊(cè)xds解析器
根據(jù)go語(yǔ)言的特性,如果某個(gè)包沒(méi)有使用的話,是不會(huì)導(dǎo)入的;因此,如果我們想使用xds解析器的話,需要手動(dòng)的顯示導(dǎo)入,如下所示:
隨便找一個(gè)grpc服務(wù)器端啟動(dòng)的例子:

import (
   "context"
   "fmt"
   "github.com/CodisLabs/codis/pkg/utils/log"
   "google.golang.org/grpc"
   "google.golang.org/grpc/codes"
   "google.golang.org/grpc/metadata"
   "google.golang.org/grpc/status"
   "net"
   pb "servergrpc/model/sgrpc/model2"
   "time"
   _ "google.golang.org/grpc/balancer/grpclb"
   //_ "google.golang.org/grpc/balancer/rls/internal"
   _ "google.golang.org/grpc/xds"
)

代碼說(shuō)明:
1.第14行,將xds包導(dǎo)入到grpc服務(wù)器里,也就是將xds作為插件注冊(cè)到了grpc服務(wù)器里

1.2.5、自定義解析器

在../examples/features/name_resolving/client/main.go文件進(jìn)行初始化:

func init() {
   // Register the example ResolverBuilder. This is usually done in a package's
   // init() function.
   resolver.Register(&exampleResolverBuilder{})
}

grpc-go源碼自帶的測(cè)試用例中還有其他自定義解析器,這里就不一一列舉了。

1.3、平衡構(gòu)建器的注冊(cè)

平衡構(gòu)建器的注冊(cè)是通過(guò)../balancer/balancer.go文件中的Register函數(shù)來(lái)實(shí)現(xiàn)的。

func Register(b Builder) {
   m[strings.ToLower(b.Name())] = b
}

注冊(cè)時(shí),需要傳入一個(gè)構(gòu)建器Builder,類(lèi)型是接口:

1.// Builder creates a balancer.
2.type Builder interface {
3.   // Build creates a new balancer with the ClientConn.
4.   // 在 balancer_conn_wrappers.go文件中的newCCBalancerWrapper方法里,調(diào)用此方法了
5.   Build(cc ClientConn, opts BuildOptions) Balancer

6.   // Name returns the name of balancers built by this builder.
7.   // It will be used to pick balancers (for example in service config).
8.   Name() string
9.}

主要代碼說(shuō)明:
1.第5行:為客戶端連接器ClientConn創(chuàng)建一個(gè)平衡器
2.第8行:返還的是平衡器的名稱(chēng);注冊(cè)或者根據(jù)名稱(chēng)獲取指定平衡器時(shí)使用的。
下面圖片展示了當(dāng)前grpc-go框架中內(nèi)置的平衡構(gòu)建器:

內(nèi)置平衡構(gòu)建器展示

注意:
平衡構(gòu)建器是用來(lái)創(chuàng)建平衡器的。
平衡器的創(chuàng)建是在客戶端跟服務(wù)器端進(jìn)行鏈接過(guò)程中創(chuàng)建的。

1.3.1、baseBuilder平衡構(gòu)建器

在grpc-go/balancer/base/balancer.go文件中定義了baseBuilder平衡構(gòu)建器。

1.type baseBuilder struct {
2.   name          string
3.   pickerBuilder PickerBuilder
4.   config        Config
}

主要代碼說(shuō)明:
1.第2行:用來(lái)設(shè)置構(gòu)建器的名稱(chēng)
2.第3行:這是一個(gè)picker構(gòu)建器
看一下PickerBuilder源碼:
在grpc-go/balancer/base/base.go文件中:

1.// PickerBuilder creates balancer.Picker.
2.type PickerBuilder interface {
3.   // Build returns a picker that will be used by gRPC to pick a SubConn.
4.   Build(info PickerBuildInfo) balancer.Picker
}

主要代碼說(shuō)明:
1.第4行:創(chuàng)建一個(gè)balancer.Picker構(gòu)建器;
這個(gè)Picker到底是用來(lái)做什么呢?
比方說(shuō),在某個(gè)場(chǎng)景下存在多個(gè)服務(wù)器端提供服務(wù),客戶端同時(shí)與這些服務(wù)器端建立起了鏈接,當(dāng)客戶端需要調(diào)用服務(wù)器端的服務(wù)時(shí),需要從這些鏈接中根據(jù)平衡器策略選擇一個(gè)鏈接進(jìn)行服務(wù)調(diào)用。(簡(jiǎn)單的說(shuō),就是picker需要從眾多鏈接中選擇一個(gè)進(jìn)行幀的接收)
下面圖片顯示了grpc-go框架中內(nèi)置實(shí)現(xiàn)的PickerBuilder:

PickerBuilder平衡構(gòu)建器

baseBuilder構(gòu)建器是基礎(chǔ)構(gòu)建器,啟動(dòng)時(shí)不需要注冊(cè);或者可以認(rèn)為baseBuilder是一個(gè)平衡構(gòu)建器模板,像下面的round_robin平衡構(gòu)建器就是以這個(gè)模塊為基礎(chǔ)創(chuàng)建的。

1.3.2、pickFirst平衡構(gòu)建器注冊(cè)

在grpc-go/pickfirst.go文件中注冊(cè)了pickFirst構(gòu)建器:

func init() {
   balancer.Register(newPickfirstBuilder())
}

其中,傳入的構(gòu)建器newPickerfirstBuilder(),代碼如下:

// PickFirstBalancerName is the name of the pick_first balancer.
const PickFirstBalancerName = "pick_first"

// 初始化Pickerfirst 構(gòu)建器
func newPickfirstBuilder() balancer.Builder {
   return &pickfirstBuilder{}
}

type pickfirstBuilder struct{}

1.3.3、round_robin平衡器注冊(cè)

在grpc-go/balancer/roundrobin/roundrobin.go文件中:

1.// Name is the name of round_robin balancer.
2.const Name = "round_robin"

3.// newBuilder creates a new roundrobin balancer builder.
4.func newBuilder() balancer.Builder {
5.   return base.NewBalancerBuilder(Name, &rrPickerBuilder{}, base.Config{HealthCheck: true})
6.}

7.func init() {
8.   balancer.Register(newBuilder())
9.}

10.type rrPickerBuilder struct{}

主要代碼說(shuō)明:
1.第2行:設(shè)置平衡器的名稱(chēng)
2.第4-6行:創(chuàng)建round_robin平衡構(gòu)建器函數(shù),內(nèi)部調(diào)用的是base.NewBalancerBuilder函數(shù)
3.第7-9行:將round_robin平衡器構(gòu)建器注冊(cè)到grpc服務(wù)器里
4.第10行:定義一個(gè)rrPickerBuilder結(jié)構(gòu)體;很明顯,前綴rr是round_robin的首字母縮寫(xiě);在grpc-go框架中,有很多類(lèi)似的情況。

接下來(lái),看一下base.NewBalancerBuilder函數(shù)內(nèi)部:

1.func NewBalancerBuilder(name string, pb PickerBuilder, config Config) balancer.Builder {
2.      return &baseBuilder{
3.      name:          name,
4.      pickerBuilder: pb,
5.      config:        config,
6.   }
7.}

主要代碼說(shuō)明:
1.第2-6行:round_robin構(gòu)建器內(nèi)部調(diào)用的是baseBuilder構(gòu)建器。
在實(shí)現(xiàn)平衡器的過(guò)程中最主要的就是要實(shí)現(xiàn)PickerBuilder,如何眾多鏈接中選擇一個(gè)鏈接進(jìn)行客戶端跟服務(wù)器端的數(shù)據(jù)交換,也就是設(shè)置選擇鏈接的策略。不同的平衡器選擇策略不同,如隨機(jī)選擇,始終選擇第一個(gè)pickerFirst,或者根據(jù)鏈接的權(quán)重進(jìn)行選擇,或者輪詢選擇策略round_robin。

1.3.4、grpclb平衡器注冊(cè)

在grpc-go/balancer/grpclb/grpclb.go文件中,進(jìn)行了初始化:

1.func init() {
2.   balancer.Register(newLBBuilder())
3.   dns.EnableSRVLookups = true
4.}

5.// newLBBuilder creates a builder for grpclb.
6.func newLBBuilder() balancer.Builder {
7.      return newLBBuilderWithFallbackTimeout(defaultFallbackTimeout)
8.}
9.func newLBBuilderWithFallbackTimeout(fallbackTimeout time.Duration) balancer.Builder {
10.   return &lbBuilder{
11.      fallbackTimeout: fallbackTimeout,
12.   }
13.}

14.type lbBuilder struct {
15.   fallbackTimeout time.Duration
16.}

主要代碼說(shuō)明:
1.第1-4行:注冊(cè)lb構(gòu)建器
2.第6-8行:創(chuàng)建lb構(gòu)建器函數(shù),內(nèi)部調(diào)用的是newLBBuilderWithFallbackTimeout函數(shù)
需要說(shuō)明的是,grpclb構(gòu)建器在grpc啟動(dòng)時(shí)默認(rèn)并沒(méi)有啟動(dòng),如果想要啟動(dòng)的話,需要顯示的導(dǎo)入,如下所示:
在/grpc-go/examples/features/load_balancing/server/main.go測(cè)試用例中:

1.package main

2.import (
3.   "context"
4.   "fmt"
5.   "github.com/CodisLabs/codis/pkg/utils/log"
6.   "net"
7.   "sync"

8.   "google.golang.org/grpc"

9.   _ "google.golang.org/grpc/balancer/grpclb"
10.   pb "servergrpc/examples/features/proto/echo"
11.)

主要代碼說(shuō)明:
1.第9行:將grpclb構(gòu)建器顯示的導(dǎo)入到服務(wù)器中

1.4、編解碼器初始化

grpc-go框架中使用protoc作為默認(rèn)的編解碼器。
在grpc-go/encoding/proto/proto.go文件中:

1.// Name is the name registered for the proto compressor.
2.const Name = "proto"

3.func init() {
4.   encoding.RegisterCodec(codec{})
5.}

6.// codec is a Codec implementation with protobuf. It is the default codec for gRPC.
7.type codec struct{}

主要代碼說(shuō)明:
1.第3-5行:注冊(cè)編解碼器codec
看一下,encoding.RegisterCodec函數(shù)內(nèi)部:

1.func RegisterCodec(codec Codec) {
2.   if codec == nil {
3.      panic("cannot register a nil Codec")
4.   }
5.   if codec.Name() == "" {
6.      panic("cannot register Codec with empty string result for Name()")
7.   }
8.   contentSubtype := strings.ToLower(codec.Name())
9.   registeredCodecs[contentSubtype] = codec
10.}

主要代碼說(shuō)明:
1.第2-7行:主要做一些校驗(yàn)工作。
2.第9行:將protoc注冊(cè)到registeredCodecs容器里
注意:
此函數(shù)并非線程安全的,在啟動(dòng)時(shí)如果存在多個(gè)相同名稱(chēng)的編碼器注冊(cè)的話,會(huì)以最后一個(gè)注冊(cè)的編碼器有效。

1.5、攔截器初始化

攔截器的初始化主要分為兩大步驟:

1.5.1、自定義攔截器

例如:在grpc-go/examples/features/interceptor/server/main.go文件中:

1.func unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
2.   // authentication (token verification)
3.   md, ok := metadata.FromIncomingContext(ctx)
4.   if !ok {
5.      return nil, errMissingMetadata
6.   }
7.   if !valid(md["authorization"]) {
8.      return nil, errInvalidToken
9.   }
10.   m, err := handler(ctx, req)
11.   if err != nil {
12.      logger("RPC failed with error %v", err)
13.   }
14.   return m, err
15.}

主要代碼說(shuō)明:
1.第2-9行:在調(diào)用真正處理函數(shù)之前,要做的事情;比方說(shuō),打印日志之類(lèi)的工作。
2.第10行:調(diào)用真正處理的函數(shù),也就是客戶端要調(diào)用的服務(wù)。在后面分析攔截器的原理時(shí)再詳細(xì)的說(shuō)明。

1.5.2、將攔截器注冊(cè)到服務(wù)器端

1.func main() {
2.   flag.Parse()

3.   lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
4.   if err != nil {
5.      log.Fatalf("failed to listen: %v", err)
6.   }

7.   s := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(unaryInterceptor), grpc.StreamInterceptor(streamInterceptor))

8.   // Register EchoServer on the server.
9.   pb.RegisterEchoServer(s, &server{})

10.   if err := s.Serve(lis); err != nil {
11.      log.Fatalf("failed to serve: %v", err)
12.   }
13.}

主要代碼說(shuō)明:
1.第7行:就是將攔截器注冊(cè)到gprc服務(wù)器端
這里僅僅說(shuō)明一下,grpc-go是如何注冊(cè)攔截器的,后面的章節(jié)將詳細(xì)的分享攔截器的原理。

2、服務(wù)器監(jiān)聽(tīng)工作

2.1、grpc服務(wù)器是如何監(jiān)聽(tīng)客戶端的請(qǐng)求呢?

例如,在grpc-go/examples/helloworld/greeter_server/main.go文件中:

1.func main() {
2.   lis, err := net.Listen("tcp", port)
3.   if err != nil {
4.      log.Fatalf("failed to listen: %v", err)
5.   }
6.   s := grpc.NewServer()
7.   pb.RegisterGreeterServer(s, &server{})
8.   if err := s.Serve(lis); err != nil {
9.      log.Fatalf("failed to serve: %v", err)
10.   }
11.}

主要代碼說(shuō)明:
1.第2行:創(chuàng)建一個(gè)監(jiān)聽(tīng)目標(biāo),只監(jiān)聽(tīng)tcp協(xié)議的請(qǐng)求,端口號(hào)是port
2.第6行:創(chuàng)建grpc服務(wù)器
3.第7行:將服務(wù)注冊(cè)到grpc服務(wù)器里
4.第8行:?jiǎn)?dòng)grpc服務(wù)器對(duì)監(jiān)聽(tīng)目標(biāo)開(kāi)始監(jiān)聽(tīng)
進(jìn)入方法Serve里:
在grpc-go/server.go文件里:(只顯示了核心代碼):

1.func (s *Server) Serve(lis net.Listener) error {
2.   s.serve = true
3.   // 1、將監(jiān)聽(tīng) 進(jìn)行存儲(chǔ),true表示處于監(jiān)聽(tīng)狀態(tài)
4.   ls := &listenSocket{Listener: lis}
5.   for {
6.      rawConn, err := lis.Accept()
7.    
8.      go func() {
9.         s.handleRawConn(rawConn)
10.      }()
11.   }
12.}

在源碼文件中,找到for循環(huán)即可。
主要代碼說(shuō)明:
1.第2行:表示grpc服務(wù)器端處于運(yùn)行狀態(tài)
2.第6行:阻塞方式監(jiān)聽(tīng)客戶端的請(qǐng)求,如果沒(méi)有請(qǐng)求時(shí)會(huì)一直阻塞此處。
3.第8-10行:針對(duì)新的鏈接,grpc服務(wù)器開(kāi)啟一個(gè)協(xié)程處理客戶端的鏈接。一個(gè)請(qǐng)求鏈接對(duì)應(yīng)一個(gè)協(xié)程;

后面的章節(jié)再詳細(xì)的介紹接收到客戶端的請(qǐng)求后,grpc服務(wù)器端做了哪些事情。

本篇文章主要是分析了grpc服務(wù)器端啟動(dòng)后,都做了哪些事情;這樣的話,以后用到哪個(gè)組件時(shí),就知道在什么地方進(jìn)行的初始化,賦值等操作了。
其實(shí)很多框架都是類(lèi)似的情形,如創(chuàng)建各種組件,初始化,注冊(cè),監(jiān)聽(tīng)端口等;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容