溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

發(fā)布時間:2021-12-30 10:40:47 來源:億速云 閱讀:120 作者:柒染 欄目:云計算

Dubbo-go Server 端開啟服務(wù)過程是怎樣的,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。

下面將介紹 dubbo-go 框架的基本使用方法,以及從 export 調(diào)用鏈的角度進行 server 端源碼導讀。

當拿到一款框架之后,一種不錯的源碼閱讀方式大致如下:從運行最基礎(chǔ)的 helloworld demo 源碼開始 —> 再查看配置文件 —> 開啟各種依賴服務(wù)(比如zk、consul) —> 開啟服務(wù)端 —> 再到通過 client 調(diào)用服務(wù)端 —> 打印完整請求日志和回包。調(diào)用成功之后,再根據(jù)框架的設(shè)計模型,從配置文件解析開始,自頂向下遞閱讀整個框架的調(diào)用棧。

對于 C/S 模式的 rpc 請求來說,整個調(diào)用棧被拆成了 client 和 server 兩部分,所以可以分別從 server 端的配置文件解析閱讀到 server 端的監(jiān)聽啟動,從 client 端的配置文件解析閱讀到一次 invoker Call 調(diào)用。這樣一次完整請求就明晰了起來。

運行官網(wǎng)提供的 helloworld-demo

1. dubbo-go 2.7 版本 QuickStart

1)開啟一個 go-server 服務(wù)

  • 將倉庫 clone 到本地

$ git clone https://github.com/dubbogo/dubbo-samples.git

  • 進入 dubbo 目錄

$ cd dubbo-samples/golang/helloworld/dubbo

進入目錄后可看到四個文件夾,分別支持 go 和 java 的 client 以及 server,我們嘗試運行一個 go 的 server。進入 app 子文件夾內(nèi),可以看到里面保存了 go 文件。

$ cd go-server/app

  • sample 文件結(jié)構(gòu)

可以在 go-server 里面看到三個文件夾:app、assembly、profiles。

其中 app 文件夾下保存 go 源碼,assembly 文件夾下保存可選的針對特定環(huán)境的 build 腳本,profiles 下保存配置文件。對于 dubbo-go 框架,配置文件非常重要,沒有文件將導致服務(wù)無法啟動。

  • 設(shè)置指向配置文件的環(huán)境變量

由于 dubbo-go 框架依賴配置文件啟動,讓框架定位到配置文件的方式就是通過環(huán)境變量來找。對于 server 端需要兩個必須配置的環(huán)境變量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,分別應(yīng)該指向服務(wù)端配置文件、日志配置文件。

在 sample 里面,我們可以使用 dev 環(huán)境,即 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"

  • 設(shè)置 go 代理并運行服務(wù)

$ go run .

如果提示 timeout,則需要設(shè)置 goproxy 代理。

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

再運行 go run 即可開啟服務(wù)。

2)運行 zookeeper

安裝 zookeeper,并運行 zkServer, 默認為 2181 端口。

3)運行 go-client 調(diào)用 server 服務(wù)

  • 進入 go-client 的源碼目錄

$ cd go-client/app

  • 同理,在 /app 下配置環(huán)境變量

$ export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml" 

$ export APP_LOG_CONF_FILE="../profiles/dev/log.yml"

配置 go 代理:

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

  • 運行程序

$ go run .

即可在日志中找到打印出的請求結(jié)果:

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 調(diào)用成功。

4)常見問題

  • 當日志開始部分出現(xiàn) profiderInit 和 ConsumerInit 均失敗的日志,檢查環(huán)境變量中配置路徑是否正確,配置文件是否正確。

  • 當日志中出現(xiàn) register 失敗的情況,一般為向注冊中心注冊失敗,檢查注冊中心是否開啟,檢查配置文件中關(guān)于 register 的端口是否正確。

  • sample 的默認開啟端口為 20000,確保啟動前無占用。

2. 配置環(huán)境變量

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

3. 服務(wù)端源碼

1)目錄結(jié)構(gòu)

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

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • app/ 文件夾下存放源碼,可以自己編寫環(huán)境變量配置腳本 buliddev.sh

  • assembly/ 文件夾下存放不同平臺的構(gòu)建腳本

  • profiles/ 文件夾下存放不同環(huán)境的配置文件

  • target/ 文件夾下存放可執(zhí)行文件

2)關(guān)鍵源碼

源碼放置在 app/ 文件夾下,主要包含 server.go 和 user.go 兩個文件,顧名思義,server.go 用于使用框架開啟服務(wù)以及注冊傳輸協(xié)議;user.go 則定義了 rpc-service 結(jié)構(gòu)體,以及傳輸協(xié)議的結(jié)構(gòu)。

  • 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 函數(shù),是服務(wù)端代碼中最先被執(zhí)行的部分。User 為用戶自定義的傳輸結(jié)構(gòu)體,UserProvider 為用戶自定義的 rpc_service;包含一個 rpc 函數(shù),GetUser。當然,用戶可以自定義其他的 rpc 功能函數(shù)。

在 init 函數(shù)中,調(diào)用 config 的 SetProviderService 函數(shù),將當前 rpc_service 注冊在框架 config 上。

可以查看 dubbo 官方文檔提供的設(shè)計圖:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

service 層下面就是 config 層,用戶服務(wù)會逐層向下注冊,最終實現(xiàn)服務(wù)端的暴露。

rpc-service 注冊完畢之后,調(diào)用 hessian 接口注冊傳輸結(jié)構(gòu)體 User。

至此,init 函數(shù)執(zhí)行完畢。

  • 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)
	...

之后執(zhí)行 main 函數(shù)。

main 函數(shù)中只進行了兩個操作,首先使用 hessian 注冊組件將 User 結(jié)構(gòu)體注冊(與之前略有重復),從而可以在接下來使用 getty 打解包。

之后調(diào)用 config.Load 函數(shù),該函數(shù)位于框架 config/config_loader.go 內(nèi),這個函數(shù)是整個框架服務(wù)的啟動點,下面會詳細講這個函數(shù)內(nèi)重要的配置處理過程。執(zhí)行完 Load() 函數(shù)之后,配置文件會讀入框架,之后根據(jù)配置文件的內(nèi)容,將注冊的 service 實現(xiàn)到配置結(jié)構(gòu)里,再調(diào)用 Export 暴露給特定的 registry,進而開啟特定的 service 進行對應(yīng)端口的 tcp 監(jiān)聽,成功啟動并且暴露服務(wù)。

最終開啟信號監(jiān)聽 initSignal() 優(yōu)雅地結(jié)束一個服務(wù)的啟動過程。

4. 客戶端源碼

客戶端包含 client.go 和 user.go 兩個文件,其中 user.go 與服務(wù)端完全一致,不再贅述。

  • 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 函數(shù)和服務(wù)端也類似,首先將傳輸結(jié)構(gòu)注冊到 hessian 上,再調(diào)用 config.Load() 函數(shù)。在下文會介紹,客戶端和服務(wù)端會根據(jù)配置類型執(zhí)行 config.Load() 中特定的函數(shù) loadConsumerConfig() 和 loadProviderConfig(),從而達到“開啟服務(wù)”、“調(diào)用服務(wù)”的目的。

加載完配置之后,還是通過實現(xiàn)服務(wù)、增加函數(shù) proxy、申請 registry 和 reloadInvoker 指向服務(wù)端 ip 等操作,重寫了客戶端實例 userProvider 的對應(yīng)函數(shù),這時再通過調(diào)用 GetUser 函數(shù),可以直接通過 invoker,調(diào)用到已經(jīng)開啟的服務(wù)端,實現(xiàn) rpc 過程。

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

5. 自定義配置文件(非環(huán)境變量)方法

1)服務(wù)端自定義配置文件

  • var providerConfigStr = xxxxx// 配置文件內(nèi)容,可以參考 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() 之前設(shè)置配置,例如:

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// 配置文件內(nèi)容,可以參考 log 和 clien。在這里你可以定義配置文件的獲取方式,比如配置中心,本地文件讀取。

  • config.Load() 之前設(shè)置配置,例如:

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 端

服務(wù)暴露過程涉及到多次原始 rpcService 的封裝、暴露,網(wǎng)上其他文章的圖感覺太過籠統(tǒng),在此,簡要地繪制了一個用戶定義服務(wù)的數(shù)據(jù)流圖:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

1. 加載配置

1)框架初始化

在加載配置之前,框架提供了很多已定義好的協(xié)議、工廠等組件,都會在對應(yīng)模塊 init 函數(shù)內(nèi)注冊到 extension 模塊上,以供接下來配置文件中進行選用。

其中重要的有:

  • 默認函數(shù)代理工廠:common/proxy/proxy_factory/default.go

func init() {
	extension.SetProxyFactory("default", NewDefaultProxyFactory)
}

它的作用是將原始 rpc-service 進行封裝,形成 proxy_invoker,更易于實現(xiàn)遠程 call 調(diào)用,詳情可見其 invoke 函數(shù)。

  • 注冊中心注冊協(xié)議: registry/protocol/protocol.go

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

它負責將 invoker 暴露給對應(yīng)注冊中心,比如 zk 注冊中心。

  • zookeeper 注冊協(xié)議:registry/zookeeper/zookeeper.go

func init() {
	extension.SetRegistry("zookeeper", newZkRegistry)
}

它合并了 base_resiger,負責在服務(wù)暴露過程中,將服務(wù)注冊在 zookeeper 注冊器上,從而為調(diào)用者提供調(diào)用方法。

  • dubbo 傳輸協(xié)議:protocol/dubbo/dubbo.go

func init() {
	extension.SetProtocol(DUBBO, GetProtocol)
}

它負責監(jiān)聽對應(yīng)端口,將具體的服務(wù)暴露,并啟動對應(yīng)的事件 handler,將遠程調(diào)用的 event 事件傳遞到 invoker 內(nèi)部,調(diào)用本地 invoker 并獲得執(zhí)行結(jié)果返回。

  • filter 包裝調(diào)用鏈協(xié)議:protocol/protocolwrapper/protocol_filter_wrapper.go

func init() {
	extension.SetProtocol(FILTER, GetProtocol)
}

它負責在服務(wù)暴露過程中,將代理 invoker 打包,通過配置好的 filter 形成調(diào)用鏈,并交付給 dubbo 協(xié)議進行暴露。

上述提前注冊好的框架已實現(xiàn)的組件,在整個服務(wù)暴露調(diào)用鏈中都會用到,會根據(jù)配置取其所需。

2)配置文件

服務(wù)端需要的重要配置有三個字段: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)、暴露的協(xié)議名("dubbo")、注冊的協(xié)議名("demoZk")、暴露的服務(wù)所處的 interface、負載均衡策略、集群失敗策略及調(diào)用的方法等等。

其中,中間服務(wù)的協(xié)議名需要和 registries 下的 mapkey 對應(yīng),暴露的協(xié)議名需要和 protocols 下的 mapkey 對應(yīng)。

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

3)配置文件的讀入和檢查

config/config_loader.go:: Load()

在上述 example 的 main 函數(shù)中,有 config.Load() 函數(shù)的直接調(diào)用,該函數(shù)執(zhí)行細節(jié)如下:

// 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()
}

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

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

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

前半部分是配置的讀入和檢查,進入 for 循環(huán)后,是單個 service 的暴露起始點。

前面提到,在配置文件中已經(jīng)寫好了要暴露的 service 的種種信息,比如服務(wù)名、interface 名、method 名等等。在圖中 for 循環(huán)內(nèi),會將所有 service 的服務(wù)依次實現(xiàn)。

for 循環(huán)的第一行,根據(jù) key 調(diào)用 GetProviderService 函數(shù),拿到注冊的 rpcService 實例,這里對應(yīng)上述提到的 init 函數(shù)中,用戶手動注冊的自己實現(xiàn)的 rpc-service 實例:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

這個對象也就成為了 for 循環(huán)中的 rpcService 變量,將這個對象注冊通過 Implement 函數(shù)寫到 sys(ServiceConfig 類型)上,設(shè)置好 sys 的 key 和協(xié)議組,最終調(diào)用了 sys 的 Export 方法。

此處對應(yīng)流程圖的部分:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

至此,框架配置結(jié)構(gòu)體已經(jīng)拿到了所有 service 有關(guān)的配置,以及用戶定義好的 rpc-service 實例,它觸發(fā)了 Export 方法,旨在將自己的實例暴露出去。這是 Export 調(diào)用鏈的起始點。

2. 原始 service 封裝入 proxy_invoker

config/service_config.go :: Export()

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

這個函數(shù)進行了一些細碎的操作,比如為不同的協(xié)議分配隨機端口,如果指定了多個中心注冊協(xié)議,則會將服務(wù)通過多個中心注冊協(xié)議的 registryProtocol 暴露出去,我們只關(guān)心對于一個注冊協(xié)議是如何操作的。還有一些操作比如生成調(diào)用 url 和注冊 url,用于為暴露做準備。

1)首先通過配置生成對應(yīng) registryUrl 和 serviceUrl

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

registryUrl 是用來向中心注冊組件發(fā)起注冊請求的,對于 zookeeper 的話,會傳入其 ip 和端口號,以及附加的用戶名密碼等信息。

這個 regUrl 目前只存有注冊(zk)相關(guān)信息,后續(xù)會補寫入 ServiceIvk,即服務(wù)調(diào)用相關(guān)信息,里面包含了方法名,參數(shù)等...

2)對于一個注冊協(xié)議,將傳入的 rpc-service 實例注冊在 common.ServiceMap

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

這個 Register 函數(shù)將服務(wù)實例注冊了兩次,一次是以 Interface 為 key 寫入接口服務(wù)組內(nèi),一次是以 interface 和 proto 為 key 寫入特定的一個唯一的服務(wù)。

后續(xù)會從 common.Map 里面取出來這個實例。

3)獲取默認代理工廠,將實例封裝入代理 invoker

// 拿到一個proxyInvoker,這個invoker的url是傳入的regUrl,這個地方將上面注冊的service實例封裝成了invoker
// 這個GetProxyFactory返回的默認是common/proxy/proxy_factory/default.go
// 這個默認工廠調(diào)用GetInvoker獲得默認的proxyInvoker,保存了當前注冊url
invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl)
// 暴露出來 生成exporter,開啟tcp監(jiān)聽
// 這里就該跳到registry/protocol/protocol.go registryProtocol 調(diào)用的Export,將當前proxyInvoker導出
exporter = c.cacheProtocol.Export(invoker)

這一步的 GetProxyFactory("default") 方法獲取默認代理工廠,通過傳入上述構(gòu)造的 regUrl,將 url 封裝入代理 invoker。

可以進入 common/proxy/proxy_factory/default.go::ProxyInvoker.Invoke() 函數(shù)里,看到對于 common.Map 取用為 svc 的部分,以及關(guān)于 svc 對應(yīng) Method 的實際調(diào)用 Call 的函數(shù)如下:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

到這里,上面 GetInvoker(*regUrl) 返回的 invoker 即為 proxy_invoker,它封裝好了用戶定義的 rpc_service,并將具體的調(diào)用邏輯封裝入了 Invoke 函數(shù)內(nèi)。

為什么使用 Proxy_invoker 來調(diào)用?

通過這個 proxy_invoke 調(diào)用用戶的功能函數(shù),調(diào)用方式將更加抽象化,可以在代碼中看到,通過 ins 和 outs 來定義入?yún)⒑统鰠ⅲ瑢⒄麄€調(diào)用邏輯抽象化為 invocation 結(jié)構(gòu)體,而將具體的函數(shù)名的選擇、參數(shù)向下傳遞和 reflect 反射過程封裝在 invoke 函數(shù)內(nèi),這樣的設(shè)計更有利于之后遠程調(diào)用。個人認為這是 dubbo Invoke 調(diào)用鏈的設(shè)計思想。

至此,實現(xiàn)了圖中對應(yīng)的部分:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

3. registry 協(xié)議在 zkRegistry 上暴露上面的 proxy_invoker

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

這里的 cacheProtocol 為一層緩存設(shè)計,對應(yīng)到原始的 demo 上,這里是默認實現(xiàn)好的 registryProtocol。

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

這個函數(shù)內(nèi)構(gòu)造了多個 EventListener,非常有 java 的設(shè)計感。

我們只關(guān)心服務(wù)暴露的過程,先忽略這些監(jiān)聽器。

1)獲取注冊 url 和服務(wù) url

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

2)獲取注冊中心實例 zkRegistry

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

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

3)zkRegistry 調(diào)用 Registry 方法,在 zookeeper 上注冊 dubboPath

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

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

該結(jié)構(gòu)體組合了 registry.BaseRegistry 結(jié)構(gòu),base 結(jié)構(gòu)定義了注冊器基礎(chǔ)的功能函數(shù),比如 Registry、Subscribe 等,但在這些默認定義的函數(shù)內(nèi)部,還是會調(diào)用 facade 層(zkRegistry 層)的具體實現(xiàn)函數(shù),這一設(shè)計模型能在保證已有功能函數(shù)不需要重復定義的同時,引入外層函數(shù)的實現(xiàn),類似于結(jié)構(gòu)體繼承卻又復用了代碼。這一設(shè)計模式值得學習。

我們查看上述 registry/protocol/protocol.go:: Export() 函數(shù),直接調(diào)用了:

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

將已有 RegistryUrl 注冊到了 zkRegistry 上。

這一步調(diào)用了 baseRegistry 的 Register 函數(shù),進而調(diào)用 zkRegister 的 DoRegister 函數(shù),進而調(diào)用:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

在這個函數(shù)里,將對應(yīng) root 創(chuàng)造一個新的節(jié)點。

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

并且寫入具體 node 信息,node 為 url 經(jīng)過 encode 的結(jié)果,包含了服務(wù)端的調(diào)用方式。

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

至此,將服務(wù)端調(diào)用 url 注冊到了 zookeeper 上,而客戶端如果想獲取到這個 url,只需要傳入特定的 dubboPath,向 zk 請求即可。目前 client 是可以獲取到訪問方式了,但服務(wù)端的特定服務(wù)還沒有啟動,還沒有開啟特定協(xié)議端口的監(jiān)聽,這也是 registry/protocol/protocol.go:: Export() 函數(shù)接下來要做的事情。

4)proxy_invoker 封裝入 wrapped_invoker,得到 filter 調(diào)用鏈

	// invoker封裝入warppedInvoker
	wrappedInvoker := newWrappedInvoker(invoker, providerUrl)
	// 經(jīng)過為invoker增加filter調(diào)用鏈,再使用dubbo協(xié)議Export,開啟service并且返回了Exporter 。
	// export_1
	cachedExporter = extension.GetProtocol(protocolwrapper.FILTER).Export(wrappedInvoker)

新建一個 WrappedInvoker,用于之后鏈式調(diào)用。

拿到提前實現(xiàn)并注冊好的 ProtocolFilterWrapper,調(diào)用 Export 方法,進一步暴露。

protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

protocol/protocolwrapped/protocol_filter_wrapper.go:buildInvokerChain

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

可見,根據(jù)配置的內(nèi)容,通過鏈式調(diào)用的構(gòu)造,將 proxy_invoker 層層包裹在調(diào)用鏈的最底部,最終返回一個調(diào)用鏈 invoker。

對應(yīng)圖中部分:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

至此,我們已經(jīng)拿到 filter 調(diào)用鏈,期待將這個 chain 暴露到特定端口,用于相應(yīng)請求事件。

5)通過 dubbo 協(xié)議暴露 wrapped_invoker

protocol/protocolwrapped/protocol_filter_wrapper.go:Export()

// 通過dubbo協(xié)議Export  dubbo_protocol調(diào)用的 export_2
	return pfw.protocol.Export(invoker)

回到上述 Export 函數(shù)的最后一行,調(diào)用了 dubboProtocol 的 Export 方法,將上述 chain 真正暴露。

該 Export 方法的具體實現(xiàn)在:protocol/dubbo/dubbo_protocol.go: Export()。

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

這一函數(shù)做了兩個事情:構(gòu)造觸發(fā)器、啟動服務(wù)。

  • 將傳入的 Invoker 調(diào)用 chain 進一步封裝,封裝成一個 exporter,再將這個 export 放入 map 保存。注意!這里把 exporter 放入了 SetExporterMap中,在下面服務(wù)啟動的時候,會以注冊事件監(jiān)聽器的形式將這個 exporter 取出!

  • 調(diào)用 dubboProtocol 的 openServer 方法,開啟一個針對特定端口的監(jiān)聽。

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

如上圖所示,一個 Session 被傳入,開啟對應(yīng)端口的事件監(jiān)聽。

至此構(gòu)造出了 exporter,完成圖中部分:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

4. 注冊觸發(fā)動作

上述只是啟動了服務(wù),但還沒有看到觸發(fā)事件的細節(jié),點進上面的 s.newSession 可以看到,dubbo 協(xié)議為一個 getty 的 session 默認使用了如下配置:

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

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

protocol/dubbo/listener.go:OnMessage()

rpcHandler 有一個實現(xiàn)好的 OnMessage 函數(shù),根據(jù) getty 的 API,當 client 調(diào)用該端口時,會觸發(fā) OnMessage。

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

這一函數(shù)實現(xiàn)了在 getty session 接收到 rpc 調(diào)用后的一系列處理:

  • 傳入包的解析

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • 根據(jù)請求包構(gòu)造請求 url

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • 拿到對應(yīng)請求 key,找到要被調(diào)用的 exporter

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • 拿到對應(yīng)的 Invoker

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • 構(gòu)造 invocation

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • 調(diào)用

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

  • 返回

Dubbo-go Server 端開啟服務(wù)過程是怎樣的

整個被調(diào)過程一氣呵成。實現(xiàn)了從 getty.Session 的調(diào)用事件,到經(jīng)過層層封裝的 invoker 的調(diào)用。

至此,一次 rpc 調(diào)用得以正確返回。

  • 關(guān)于 Invoker 的層層封裝

能把一次調(diào)用抽象成一次 invoke;能把一個協(xié)議抽象成針對 invoke 的封裝;能把針對一次 invoke 所做出的特定改變封裝到 invoke 函數(shù)內(nèi)部,可以降低模塊之間的耦合性。層層封裝邏輯更加清晰。

  • 關(guān)于 URL 的抽象

關(guān)于 dubbo 的統(tǒng)一化請求對象 URL 的極度抽象是之前沒有見過的... 個人認為這樣封裝能保證請求參數(shù)列表的簡化和一致。但在開發(fā)的過程中,濫用極度抽象的接口可能造成... debug 的困難?以及不知道哪些字段是當前已經(jīng)封裝好的,哪些字段是無用的。

  • 關(guān)于協(xié)議的理解

之前理解的協(xié)議還是太過具體化了,而關(guān)于 dubbo-go 對于 dubboProtocol 的協(xié)議,我認為是基于 getty 的進一步封裝,它定義了客戶端和服務(wù)端,對于 getty 的 session 應(yīng)該有哪些特定的操作,從而保證主調(diào)和被調(diào)的協(xié)議一致性,而這種保證也是一種協(xié)議的體現(xiàn),是由 dubbo 協(xié)議來規(guī)范的。

看完上述內(nèi)容,你們掌握Dubbo-go Server 端開啟服務(wù)過程是怎樣的的方法了嗎?如果還想學到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI