go-micro開發RPC服務的方法及其執行原理

波斯馬發表於2022-04-24

go-micro-rpc

go-micro是一個知名的golang微服務框架,最新版本是v4,這篇文章將介紹go-micro v4開發RPC服務的方法及其運作原理。

基本概念

go-micro有幾個重要的概念,後邊開發RPC服務和介紹其執行原理的時候會用到,這裡先熟悉下:

  • Service:代表一個go-micro應用程式,Service中包括:Server、Client、Broker、Transport、Registry、Config、Store、Cache等程式執行所需的各個模組。
  • Server:代表一個go-micro伺服器,主要函式包括:Start、Stop、Handle、Subscribe。預設建立的Server是 rpcServer。
  • Broker:用於處理非同步訊息,主要的函式包括:Connect、Publish、Subscribe。預設的Broker是httpBroker。
  • Router:用於訊息處理的路由,內部包括兩種路由方式:RPC服務對映serviceMap和訊息訂閱器subscribers。
  • Codec:用於訊息的編解碼,主要函式包括:Marshal、Unmarshal預設的Codec是json.Marshaler,是基於jsonpb的。RPC服務是根據請求頭中的Content-Type自動建立的。
  • Registry:用於服務發現,主要函式包括:Register、Deregister、GetService、ListServices、Watch。預設的Registry是mdns。
  • Selector: 用於從同一個服務的多個例項之中選擇一個,支援快取,有隨機和輪詢兩種策略。
  • Transport:用於同步通訊,主要函式包括:Dial、Listen。它的底層基於Socket的send、recv語義,有多種實現,包括http、grpc、quic等。預設的Transport是httpTransport。

開發RPC服務

RPC全稱是Remote Procedure Call,翻譯過來是就是:遠端過程呼叫,中心思想是:像呼叫本地函式一樣呼叫遠端函式。常見的Dubbo、Spring Cloud都可以稱為RPC框架,還有最近很流行的gRPC。

使用go-micro建立一個RPC服務很簡單,共分三步走:

1、編寫proto協議檔案

這個服務提供的功能很簡單,名字為Hello,提供一個方法名字為Say,需要傳入一個字串Name,然後返回一個字串Message。這個檔案我命名為 hello.proto,放到了專案中的 proto 資料夾中。

syntax = "proto3";

option go_package="/proto";

package Business;

service Hello {
  rpc Say (SayRequest) returns (SayResponse);
}

message SayResponse {
  string Message = 1;
}

message SayRequest {
  string Name = 1;
}

2、生成go-micro服務端代理

需要首先安裝protoc和兩個程式碼生成外掛。

protoc下載地址:https://github.com/protocolbuffers/protobuf/releases,儲存到 GOPATH/bin目錄中。同時建議將 GOPATH/bin 新增到環境變數 PATH 中,方便直接執行相關命令。

兩個外掛直接通過命令即可安裝:

go install google.golang.org/protobuf/cmd/protoc-gen-go
go install go-micro.dev/v4/cmd/protoc-gen-micro@v4

然後在專案的目錄下執行命令:

protoc --go_out=. --go_opt=paths=source_relative --micro_out=. --micro_opt=paths=source_relative proto/hello.proto

然後會在proto資料夾中生成兩個檔案:hello.pb.go 和 hello.pb.micro.go 。

下個步驟中就要使用它們來建立RPC服務。

3、編寫go-micro服務

這裡先把程式碼貼出來,然後再做一個簡要說明:

package main

import (
	"context"
	"fmt"
	"log"
	"rpchello/proto"

	"go-micro.dev/v4"
	"go-micro.dev/v4/server"
)

type Hello struct{}

func (s *Hello) Say(ctx context.Context, req *proto.SayRequest, rsp *proto.SayResponse) error {
	fmt.Println("request:", req.Name)
	rsp.Message = "Hello " + req.Name
	return nil
}

func main() {
	rpcServer := server.NewServer(
		server.Name("rpchello.service"),
		server.Address("0.0.0.0:8001"),
	)

	proto.RegisterHelloHandler(rpcServer, &Hello{})

	service := micro.NewService(
		micro.Server(rpcServer),
	)

	if err := service.Run(); err != nil {
		log.Fatal(err)
	}
}

上邊我們建立了一個 Hello 型別,然後給它繫結了一個名為Say的函式。這個是和proto協議對應的,其實是實現了生成程式碼 hello.pb.micro.go 中的HelloHandler介面:

type HelloHandler interface {
	Say(context.Context, *SayRequest, *SayResponse) error
}

然後main函式中是我們的重頭戲:先建立一個Server,預設情況下就是rpc Server,設定它的名字、監聽地址等引數;然後建立一個Service,並繫結剛剛建立的Server;然後使用生成的服務端代理函式將我們編寫的Hello服務註冊到Server中;最後開啟執行Service。

img

當然只有一個服務端沒有什麼意義,還得有客戶端來訪問它。這裡也給一個例子:

package main

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"rpchello/proto"

	"go-micro.dev/v4"
	"go-micro.dev/v4/client"
)

func main() {

	service := micro.NewService(
		micro.Client(client.NewClient()),
	)

	service.Init()
	client := proto.NewHelloService("rpchello.service", service.Client())

	rsp, err := client.Say(context.TODO(), &proto.SayRequest{Name: "BOSSMA"})
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(rsp)

	fmt.Println("Press Enter key to exit the program...")
	in := bufio.NewReader(os.Stdin)
	_, _, _ = in.ReadLine()
}

這裡呼叫服務的時候沒有指定服務的地址和埠,因為內部走了服務發現,服務端會自動註冊服務,客戶端會根據服務名稱查詢到對應的地址和埠。預設的服務發現機制使用的是mdns。

RPC服務的執行原理

這裡從服務端的角度進行介紹,先來看一張圖:

img

請大家參考程式碼從上往下看。

NewServer 時建立一個rpcServer,這個rpcServer還會建立一個httpTransport用於程式間網路通訊,並繫結到當前rpcServer。

RegisterXXXHandler 時使用我們編寫的Handler建立一個內部的service例項,然後註冊這個service例項到rpcServer內部的router中,客戶端請求時會用到它。這裡其實可以註冊任意一個帶方法的型別,並不一定要定義proto協議,定義它只是為了協作更方便。

Service.Run 時會呼叫rpcServer的Start方法,這個方法內部會呼叫其繫結的httpTransport的Listen方法,然後在其建立的Listener上接收客戶端連線,接收方法Accept傳入了當前rpcServer的連線處理方法:rpcServer.ServeConn,有連線到來時會呼叫它。

當客戶端請求來臨時,客戶端連線被交給rpcServer的ServeConn方法,然後又呼叫到HandleEvent方法。

然後進入rpcServer內部的router的函式ServeRequest中,通過分析請求訊息,找到請求的服務名字和方法名字,在router中找到前面註冊過的service,通過servcie.call,再進入function.call,最終通過反射呼叫到我們編寫的Handler的業務方法。

有的同學可能會想,反射不是效能很低嗎?!反射效能低主要是查詢方法和欄位的時候,呼叫方法的效能並不低,而查詢方法和欄位等的操作已經在RegisterXXXHandler的步驟中做了,並且快取到了router中,所以效能並不受影響。


以上就是本文的主要內容了,如有問題,歡迎交流。演示程式碼已釋出到Github:https://github.com/bosima/go-demo/tree/main/go-micro-rpc-hello

收穫更多架構知識,請關注微信公眾號 螢火架構。原創內容,轉載請註明出處。
掃描二維碼關注公眾號

相關文章