天天看點

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

随着微服務架構的流行,許多高性能 rpc 架構應運而生,由阿裡開源的 dubbo 架構 go 語言版本的 dubbo-go 也成為了衆多開發者不錯的選擇。本文将介紹 dubbo-go 架構的基本使用方法,以及從 export 調用鍊的角度進行 server 端源碼導讀,希望能引導讀者進一步認識這款架構。

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

作者 | 李志信

dubbo-go 源碼:https://github.com/apache/dubbo-go

導讀:随着微服務架構的流行,許多高性能 rpc 架構應運而生,由阿裡開源的 dubbo 架構 go 語言版本的 dubbo-go 也成為了衆多開發者不錯的選擇。本文将介紹 dubbo-go 架構的基本使用方法,以及從 export 調用鍊的角度進行 server 端源碼導讀,希望能引導讀者進一步認識這款架構。下周将發表本文的姊妹篇:《從 client 端源碼導讀 dubbo-go 架構》。

當拿到一款架構之後,一種不錯的源碼閱讀方式大緻如下:從運作最基礎的 helloworld demo 源碼開始 —> 再檢視配置檔案 —> 開啟各種依賴服務(比如zk、consul) —> 開啟服務端 —> 再到通過 client 調用服務端 —> 列印完整請求日志和回包。調用成功之後,再根據架構的設計模型,從配置檔案解析開始,自頂向下遞閱讀整個架構的調用棧。

對于 C/S 模式的 rpc 請求來說,整個調用棧被拆成了 client 和 server 兩部分,是以可以分别從 server 端的配置檔案解析閱讀到 server 端的監聽啟動,從 client 端的配置檔案解析閱讀到一次 invoker Call 調用。這樣一次完整請求就明晰了起來。

運作官網提供的 helloworld-demo

官方 demo 相關連結:https://github.com/dubbogo/dubbo-samples/tree/master/golang/helloworld/dubbo

1. dubbo-go 2.7 版本 QuickStart

1)開啟一個 go-server 服務

  • 将倉庫 clone 到本地
$ git clone https://github.com/dubbogo/dubbo-samples.git
  • 進入 dubbo 目錄
$ cd dubbo-samples/golang/helloworld/dubbo

進入目錄後可看到四個檔案夾,分别支援 go 和 java 的 client 以及 server,我們嘗試運作一個 go 的 server。進入 app 子檔案夾内,可以看到裡面儲存了 go 檔案。

$ cd go-server/app
  • sample 檔案結構

可以在 go-server 裡面看到三個檔案夾:app、assembly、profiles。

其中 app 檔案夾下儲存 go 源碼,assembly 檔案夾下儲存可選的針對特定環境的 build 腳本,profiles 下儲存配置檔案。對于 dubbo-go 架構,配置檔案非常重要,沒有檔案将導緻服務無法啟動。

  • 設定指向配置檔案的環境變量

由于 dubbo-go 架構依賴配置檔案啟動,讓架構定位到配置檔案的方式就是通過環境變量來找。對于 server 端需要兩個必須配置的環境變量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,分别應該指向服務端配置檔案、日志配置檔案。

在 sample 裡面,我們可以使用 dev 環境,即 profiles/dev/log.yml  和 profiles/dev/server.yml 兩個檔案。在 app/ 下,通過指令行中指定好這兩個檔案:

$ export CONF_PROVIDER_FILE_PATH="../profiles/dev/server.yml"
$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
  • 設定 go 代理并運作服務
$ go run .

如果提示 timeout,則需要設定 goproxy 代理。

$ export GOPROXY="http://goproxy.io"

再運作 go run 即可開啟服務。

2)運作 zookeeper

安裝 zookeeper,并運作 zkServer, 預設為 2181 端口。

3)運作 go-client 調用 server 服務

  • 進入 go-client 的源碼目錄
$ cd go-client/app
  • 同理,在 /app 下配置環境變量
$ export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"

配置 go 代理:

  • 運作程式

即可在日志中找到列印出的請求結果:

response result: &{A001 Alex Stocks 18 2020-10-28 14:52:49.131 +0800 CST}

同樣,在運作的 server 中,也可以在日志中找到列印出的請求:

req:[]interface {}{"A001"}
rsp:main.User{Id:"A001", Name:"Alex Stocks", Age:18, Time:time.Time{...}

恭喜!一次基于 dubbo-go 的 rpc 調用成功。

4)常見問題

  • 當日志開始部分出現 profiderInit 和 ConsumerInit 均失敗的日志,檢查環境變量中配置路徑是否正确,配置檔案是否正确。
  • 當日志中出現 register 失敗的情況,一般為向注冊中心注冊失敗,檢查注冊中心是否開啟,檢查配置檔案中關于 register 的端口是否正确。
  • sample 的預設開啟端口為 20000,確定啟動前無占用。

2. 配置環境變量

export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
           

3. 服務端源碼

1)目錄結構

dubbo-go 架構的 example 提供的目錄如下:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • app/ 檔案夾下存放源碼,可以自己編寫環境變量配置腳本 buliddev.sh
  • assembly/ 檔案夾下存放不同平台的建構腳本
  • profiles/ 檔案夾下存放不同環境的配置檔案
  • target/ 檔案夾下存放可執行檔案

2)關鍵源碼

源碼放置在 app/ 檔案夾下,主要包含 server.go 和 user.go 兩個檔案,顧名思義,server.go 用于使用架構開啟服務以及注冊傳輸協定;user.go 則定義了 rpc-service 結構體,以及傳輸協定的結構。

  • user.go
func init() {
	config.SetProviderService(new(UserProvider))
	// ------for hessian2------
	hessian.RegisterPOJO(&User{})
}
type User struct {
	Id   string
	Name string
	Age  int32
	Time time.Time
}
type UserProvider struct {
}
func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) {
           

可以看到,user.go 中存在 init 函數,是服務端代碼中最先被執行的部分。User 為使用者自定義的傳輸結構體,UserProvider 為使用者自定義的 rpc_service;包含一個 rpc 函數,GetUser。當然,使用者可以自定義其他的 rpc 功能函數。

在 init 函數中,調用 config 的 SetProviderService 函數,将目前 rpc_service 注冊在架構 config 上。

可以檢視 dubbo 官方文檔提供的設計圖:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

service 層下面就是 config 層,使用者服務會逐層向下注冊,最終實作服務端的暴露。

rpc-service 注冊完畢之後,調用 hessian 接口注冊傳輸結構體 User。

至此,init 函數執行完畢。

  • server.go
// they are necessary:
//  	export CONF_PROVIDER_FILE_PATH="xxx"
//  	export APP_LOG_CONF_FILE="xxx"
func main() {
	hessian.RegisterPOJO(&User{})
	config.Load()
	initSignal()
}
func initSignal() {
	signals := make(chan os.Signal, 1)
	...
           

之後執行 main 函數。

main 函數中隻進行了兩個操作,首先使用 hessian 注冊元件将 User 結構體注冊(與之前略有重複),進而可以在接下來使用 getty 打解包。

之後調用 config.Load 函數,該函數位于架構 config/config_loader.go 内,這個函數是整個架構服務的啟動點,下面會詳細講這個函數内重要的配置處理過程。執行完 Load() 函數之後,配置檔案會讀入架構,之後根據配置檔案的内容,将注冊的 service 實作到配置結構裡,再調用 Export 暴露給特定的 registry,進而開啟特定的 service 進行對應端口的 tcp 監聽,成功啟動并且暴露服務。

最終開啟信号監聽 initSignal() 優雅地結束一個服務的啟動過程。

4. 用戶端源碼

用戶端包含 client.go 和 user.go 兩個檔案,其中 user.go 與服務端完全一緻,不再贅述。

  • client.go
// they are necessary:
//  	export CONF_CONSUMER_FILE_PATH="xxx"
//  	export APP_LOG_CONF_FILE="xxx"
func main() {
	hessian.RegisterPOJO(&User{})
	config.Load()
	time.Sleep(3e9)
	println("\n\n\nstart to test dubbo")
	user := &User{}
	err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
	if err != nil {
  panic(err)
	}
	println("response result: %v\n", user)
	initSignal()
}
           

main 函數和服務端也類似,首先将傳輸結構注冊到 hessian 上,再調用 config.Load() 函數。在下文會介紹,用戶端和服務端會根據配置類型執行 config.Load() 中特定的函數 loadConsumerConfig() 和 loadProviderConfig(),進而達到“開啟服務”、“調用服務”的目的。

加載完配置之後,還是通過實作服務、增加函數 proxy、申請 registry 和 reloadInvoker 指向服務端 ip 等操作,重寫了用戶端執行個體 userProvider 的對應函數,這時再通過調用 GetUser 函數,可以直接通過 invoker,調用到已經開啟的服務端,實作 rpc 過程。

下面會從 server 端和 client 端兩個角度,詳細講解服務啟動、registry 注冊和調用過程。

5. 自定義配置檔案(非環境變量)方法

1)服務端自定義配置檔案

  • var providerConfigStr =

    xxxxx

    // 配置檔案内容,可以參考 log 和 client。在這裡你可以定義配置檔案的擷取方式,比如配置中心,本地檔案讀取。
log 位址:https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/log.yml
client 位址:https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/client.yml
  • config.Load()

    之前設定配置,例如:
func main() {
	hessian.RegisterPOJO(&User{})
	providerConfig := config.ProviderConfig{}
	yaml.Unmarshal([]byte(providerConfigStr), &providerConfig)
	config.SetProviderConfig(providerConfig)
	defaultServerConfig := dubbo.GetDefaultServerConfig()
	dubbo.SetServerConfig(defaultServerConfig)
	logger.SetLoggerLevel("warn") // info,warn
	config.Load()
	select {
	}
}
           

2)用戶端自定義配置檔案

  • var consumerConfigStr =

    xxxxx

    // 配置檔案内容,可以參考 log 和 clien。在這裡你可以定義配置檔案的擷取方式,比如配置中心,本地檔案讀取。
  • config.Load()

func main() {
     p := config.ConsumerConfig{}
     yaml.Unmarshal([]byte(consumerConfigStr), &p)
     config.SetConsumerConfig(p)
     defaultClientConfig := dubbo.GetDefaultClientConfig()
     dubbo.SetClientConf(defaultClientConfig)
     logger.SetLoggerLevel("warn") // info,warn
     config.Load()

     user := &User{}
     err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
     if err != nil {
         log.Print(err)
         return
     }
  log.Print(user)
}
           

Server 端

服務暴露過程涉及到多次原始 rpcService 的封裝、暴露,網上其他文章的圖感覺太過籠統,在此,簡要地繪制了一個使用者定義服務的資料流圖:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

1. 加載配置

1)架構初始化

在加載配置之前,架構提供了很多已定義好的協定、工廠等元件,都會在對應子產品 init 函數内注冊到 extension 子產品上,以供接下來配置檔案中進行選用。

其中重要的有:

  • 預設函數代理工廠:common/proxy/proxy_factory/default.go
func init() {
	extension.SetProxyFactory("default", NewDefaultProxyFactory)
}
           

它的作用是将原始 rpc-service 進行封裝,形成 proxy_invoker,更易于實作遠端 call 調用,詳情可見其 invoke 函數。

  • 注冊中心注冊協定:

    registry/protocol/protocol.go

func init() {
	extension.SetProtocol("registry", GetProtocol)
}
           

它負責将 invoker 暴露給對應注冊中心,比如 zk 注冊中心。

  • zookeeper 注冊協定:registry/zookeeper/zookeeper.go
func init() {
	extension.SetRegistry("zookeeper", newZkRegistry)
}
           

它合并了 base_resiger,負責在服務暴露過程中,将服務注冊在 zookeeper 注冊器上,進而為調用者提供調用方法。

  • dubbo 傳輸協定:protocol/dubbo/dubbo.go
func init() {
	extension.SetProtocol(DUBBO, GetProtocol)
}
           

它負責監聽對應端口,将具體的服務暴露,并啟動對應的事件 handler,将遠端調用的 event 事件傳遞到 invoker 内部,調用本地 invoker 并獲得執行結果傳回。

  • filter 包裝調用鍊協定:protocol/protocolwrapper/protocol_filter_wrapper.go
func init() {
	extension.SetProtocol(FILTER, GetProtocol)
}
           

它負責在服務暴露過程中,将代理 invoker 打包,通過配置好的 filter 形成調用鍊,并傳遞給 dubbo 協定進行暴露。

上述提前注冊好的架構已實作的元件,在整個服務暴露調用鍊中都會用到,會根據配置取其所需。

2)配置檔案

服務端需要的重要配置有三個字段:services、protocols、registries。

profiles/dev/server.yml:

registries :
  "demoZk":
    protocol: "zookeeper"
    timeout	: "3s"
    address: "127.0.0.1:2181"
services:
  "UserProvider":
    # 可以指定多個registry,使用逗号隔開;不指定預設向所有注冊中心注冊
    registry: "demoZk"
    protocol : "dubbo"
    # 相當于dubbo.xml中的interface
    interface : "com.ikurento.user.UserProvider"
    loadbalance: "random"
    warmup: "100"
    cluster: "failover"
    methods:
    - name: "GetUser"
      retries: 1
      loadbalance: "random"
protocols:
  "dubbo":
    name: "dubbo"
    port: 20000
           

其中 service 指定了要暴露的 rpc-service 名("UserProvider)、暴露的協定名("dubbo")、注冊的協定名("demoZk")、暴露的服務所處的 interface、負載均衡政策、叢集失敗政策及調用的方法等等。

其中,中間服務的協定名需要和 registries 下的 mapkey 對應,暴露的協定名需要和 protocols 下的 mapkey 對應。

可以看到上述例子中,使用了 dubbo 作為暴露協定,使用了 zookeeper 作為中間注冊協定,并且給定了端口。如果 zk 需要設定使用者名和密碼,也可以在配置中寫好。

3)配置檔案的讀入和檢查

config/config_loader.go:: Load()

在上述 example 的 main 函數中,有 config.Load() 函數的直接調用,該函數執行細節如下:

// Load Dubbo Init
func Load() {
	// init router
	initRouter()
	// init the global event dispatcher
	extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType)
	// start the metadata report if config set
	if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil {
  logger.Errorf("Provider starts metadata report error, and the error is {%#v}", err)
  return
	}
	// reference config
	loadConsumerConfig()
	// service config
	loadProviderConfig()
	// init the shutdown callback
	GracefulShutdownInit()
}
           

在本文中,我們重點關心 loadConsumerConfig() 和 loadProviderConfig() 兩個函數。

對于 provider 端,可以看到 loadProviderConfig() 函數代碼如下:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

前半部分是配置的讀入和檢查,進入 for 循環後,是單個 service 的暴露起始點。

前面提到,在配置檔案中已經寫好了要暴露的 service 的種種資訊,比如服務名、interface 名、method 名等等。在圖中 for 循環内,會将所有 service 的服務依次實作。

for 循環的第一行,根據 key 調用 GetProviderService 函數,拿到注冊的 rpcService 執行個體,這裡對應上述提到的 init 函數中,使用者手動注冊的自己實作的 rpc-service 執行個體:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

這個對象也就成為了 for 循環中的 rpcService 變量,将這個對象注冊通過 Implement 函數寫到 sys(ServiceConfig 類型)上,設定好 sys 的 key 和協定組,最終調用了 sys 的 Export 方法。

此處對應流程圖的部分:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

至此,架構配置結構體已經拿到了所有 service 有關的配置,以及使用者定義好的 rpc-service 執行個體,它觸發了 Export 方法,旨在将自己的執行個體暴露出去。這是 Export 調用鍊的起始點。

2. 原始 service 封裝入 proxy_invoker

config/service_config.go :: Export()

接下來進入 ServiceConfig.Export() 函數.

這個函數進行了一些細碎的操作,比如為不同的協定配置設定随機端口,如果指定了多個中心注冊協定,則會将服務通過多個中心注冊協定的 registryProtocol 暴露出去,我們隻關心對于一個注冊協定是如何操作的。還有一些操作比如生成調用 url 和注冊 url,用于為暴露做準備。

1)首先通過配置生成對應 registryUrl 和 serviceUrl

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

registryUrl 是用來向中心注冊元件發起注冊請求的,對于 zookeeper 的話,會傳入其 ip 和端口号,以及附加的使用者名密碼等資訊。

這個 regUrl 目前隻存有注冊(zk)相關資訊,後續會補寫入 ServiceIvk,即服務調用相關資訊,裡面包含了方法名,參數等...

2)對于一個注冊協定,将傳入的 rpc-service 執行個體注冊在 common.ServiceMap

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

這個 Register 函數将服務執行個體注冊了兩次,一次是以 Interface 為 key 寫入接口服務組内,一次是以 interface 和 proto 為 key 寫入特定的一個唯一的服務。

後續會從 common.Map 裡面取出來這個執行個體。

3)擷取預設代理工廠,将執行個體封裝入代理 invoker

// 拿到一個proxyInvoker,這個invoker的url是傳入的regUrl,這個地方将上面注冊的service執行個體封裝成了invoker
// 這個GetProxyFactory傳回的預設是common/proxy/proxy_factory/default.go
// 這個預設工廠調用GetInvoker獲得預設的proxyInvoker,儲存了目前注冊url
invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl)
// 暴露出來 生成exporter,開啟tcp監聽
// 這裡就該跳到registry/protocol/protocol.go registryProtocol 調用的Export,将目前proxyInvoker導出
exporter = c.cacheProtocol.Export(invoker)
           

這一步的 GetProxyFactory("default") 方法擷取預設代理工廠,通過傳入上述構造的 regUrl,将 url 封裝入代理 invoker。

可以進入 common/proxy/proxy_factory/default.go::ProxyInvoker.Invoke() 函數裡,看到對于 common.Map 取用為 svc 的部分,以及關于 svc 對應 Method 的實際調用 Call 的函數如下:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

到這裡,上面 GetInvoker(*regUrl) 傳回的 invoker 即為 proxy_invoker,它封裝好了使用者定義的 rpc_service,并将具體的調用邏輯封裝入了 Invoke 函數内。

為什麼使用 Proxy_invoker 來調用?
通過這個 proxy_invoke 調用使用者的功能函數,調用方式将更加抽象化,可以在代碼中看到,通過 ins 和 outs 來定義入參和出參,将整個調用邏輯抽象化為 invocation 結構體,而将具體的函數名的選擇、參數向下傳遞和 reflect 反射過程封裝在 invoke 函數内,這樣的設計更有利于之後遠端調用。個人認為這是 dubbo Invoke 調用鍊的設計思想。
至此,實作了圖中對應的部分:
Dubbo-go 源碼筆記(一)Server 端開啟服務過程

3. registry 協定在 zkRegistry 上暴露上面的 proxy_invoker

上面,我們執行到了 exporter = c.cacheProtocol.Export(invoker)。

這裡的 cacheProtocol 為一層緩存設計,對應到原始的 demo 上,這裡是預設實作好的 registryProtocol。

registry/protocol/protocol.go:: Export()

這個函數内構造了多個 EventListener,非常有 java 的設計感。

我們隻關心服務暴露的過程,先忽略這些監聽器。

1)擷取注冊 url 和服務 url

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

2)擷取注冊中心執行個體 zkRegistry

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

一層緩存操作,如果 cache 沒有需要從 common 裡面重新拿 zkRegistry。

3)zkRegistry 調用 Registry 方法,在 zookeeper 上注冊 dubboPath

上述拿到了具體的 zkRegistry 執行個體,該執行個體的定義在:registry/zookeeper/registry.go。

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

該結構體組合了 registry.BaseRegistry 結構,base 結構定義了注冊器基礎的功能函數,比如 Registry、Subscribe 等,但在這些預設定義的函數内部,還是會調用 facade 層(zkRegistry 層)的具體實作函數,這一設計模型能在保證已有功能函數不需要重複定義的同時,引入外層函數的實作,類似于結構體繼承卻又複用了代碼。這一設計模式值得學習。

我們檢視上述 registry/protocol/protocol.go:: Export() 函數,直接調用了:

// 1. 通過zk注冊器,調用Register()函數,将已有@root@rawurl注冊到zk上
	err := reg.Register(*registeredProviderUrl)
           

将已有 RegistryUrl 注冊到了 zkRegistry 上。

這一步調用了 baseRegistry 的 Register 函數,進而調用 zkRegister 的 DoRegister 函數,進而調用:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

在這個函數裡,将對應 root 創造一個新的節點。

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

并且寫入具體 node 資訊,node 為 url 經過 encode 的結果,包含了服務端的調用方式。

這部分的代碼較為複雜,具體可以看 baseRegistry 的 processURL() 函數:http://t.tb.cn/6Xje4bijnsIDNaSmyPc4Ot。

至此,将服務端調用 url 注冊到了 zookeeper 上,而用戶端如果想擷取到這個 url,隻需要傳入特定的 dubboPath,向 zk 請求即可。目前 client 是可以擷取到通路方式了,但服務端的特定服務還沒有啟動,還沒有開啟特定協定端口的監聽,這也是 registry/protocol/protocol.go:: Export() 函數接下來要做的事情。

4)proxy_invoker 封裝入 wrapped_invoker,得到 filter 調用鍊

// invoker封裝入warppedInvoker
	wrappedInvoker := newWrappedInvoker(invoker, providerUrl)
	// 經過為invoker增加filter調用鍊,再使用dubbo協定Export,開啟service并且傳回了Exporter 。
	// export_1
	cachedExporter = extension.GetProtocol(protocolwrapper.FILTER).Export(wrappedInvoker)
           

建立一個 WrappedInvoker,用于之後鍊式調用。

拿到提前實作并注冊好的 ProtocolFilterWrapper,調用 Export 方法,進一步暴露。

protocol/protocolwrapped/protocol_filter_wrapper.go:Export()
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
protocol/protocolwrapped/protocol_filter_wrapper.go:buildInvokerChain
Dubbo-go 源碼筆記(一)Server 端開啟服務過程

可見,根據配置的内容,通過鍊式調用的構造,将 proxy_invoker 層層包裹在調用鍊的最底部,最終傳回一個調用鍊 invoker。

對應圖中部分:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

至此,我們已經拿到 filter 調用鍊,期待将這個 chain 暴露到特定端口,用于相應請求事件。

5)通過 dubbo 協定暴露 wrapped_invoker

// 通過dubbo協定Export  dubbo_protocol調用的 export_2
	return pfw.protocol.Export(invoker)
           

回到上述 Export 函數的最後一行,調用了 dubboProtocol 的 Export 方法,将上述 chain 真正暴露。

該 Export 方法的具體實作在:protocol/dubbo/dubbo_protocol.go: Export()。

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

這一函數做了兩個事情:構造觸發器、啟動服務。

  • 将傳入的 Invoker 調用 chain 進一步封裝,封裝成一個 exporter,再将這個 export 放入 map 儲存。注意!這裡把 exporter 放入了 SetExporterMap中,在下面服務啟動的時候,會以注冊事件監聽器的形式将這個 exporter 取出!
  • 調用 dubboProtocol 的 openServer 方法,開啟一個針對特定端口的監聽。
Dubbo-go 源碼筆記(一)Server 端開啟服務過程

如上圖所示,一個 Session 被傳入,開啟對應端口的事件監聽。

至此構造出了 exporter,完成圖中部分:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

4. 注冊觸發動作

上述隻是啟動了服務,但還沒有看到觸發事件的細節,點進上面的 s.newSession 可以看到,dubbo 協定為一個 getty 的 session 預設使用了如下配置:

Dubbo-go 源碼筆記(一)Server 端開啟服務過程

其中很重要的一個配置是 EventListener,傳入的是 dubboServer 的預設 rpcHandler。

protocol/dubbo/listener.go:OnMessage()

rpcHandler 有一個實作好的 OnMessage 函數,根據 getty 的 API,當 client 調用該端口時,會觸發 OnMessage。

// OnMessage notified when RPC server session got any message in connection
func (h *RpcServerHandler) OnMessage(session getty.Session, pkg interface{}) {
           

這一函數實作了在 getty session 接收到 rpc 調用後的一系列處理:

  • 傳入包的解析
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • 根據請求包構造請求 url
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • 拿到對應請求 key,找到要被調用的 exporter
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • 拿到對應的 Invoker
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • 構造 invocation
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • 調用
Dubbo-go 源碼筆記(一)Server 端開啟服務過程
  • 傳回
Dubbo-go 源碼筆記(一)Server 端開啟服務過程

整個被調過程一氣呵成。實作了從 getty.Session 的調用事件,到經過層層封裝的 invoker 的調用。

至此,一次 rpc 調用得以正确傳回。

小結

  • 關于 Invoker 的層層封裝

能把一次調用抽象成一次 invoke;能把一個協定抽象成針對 invoke 的封裝;能把針對一次 invoke 所做出的特定改變封裝到 invoke 函數内部,可以降低子產品之間的耦合性。層層封裝邏輯更加清晰。

  • 關于 URL 的抽象

關于 dubbo 的統一化請求對象 URL 的極度抽象是之前沒有見過的... 個人認為這樣封裝能保證請求參數清單的簡化和一緻。但在開發的過程中,濫用極度抽象的接口可能造成... debug 的困難?以及不知道哪些字段是目前已經封裝好的,哪些字段是無用的。

  • 關于協定的了解

之前了解的協定還是太過具體化了,而關于 dubbo-go 對于 dubboProtocol 的協定,我認為是基于 getty 的進一步封裝,它定義了用戶端和服務端,對于 getty 的 session 應該有哪些特定的操作,進而保證主調和被調的協定一緻性,而這種保證也是一種協定的展現,是由 dubbo 協定來規範的。

如果你有任何疑問,歡迎釘釘掃碼加入交流群:釘釘群号 23331795!

作者簡介

李志信 (GitHubID LaurenceLiZhixin),中山大學軟體工程專業在校學生,擅長使用 Java/Go 語言,專注于雲原生和微服務等技術方向。

“阿裡巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,做最懂雲原生開發者的公衆号。”