Go 原生 RPC 與 APRC 的簡單使用

anyanfei發表於2021-10-06

Go 原生 RPC 與 ARPC 的簡單使用

什麼是 RPC?

RPC 叫做遠端過程呼叫,意思是兩臺不同伺服器上的服務,可以互相像呼叫函式一樣呼叫。

我用 HTTP API 不一樣能達到同樣的效果嗎?

其實對於新人來說,兩臺伺服器之間的資料互動,用 HTTP 提供的 API 真的可以解決,但效率不高,延遲也高,且連線不會複用,因為大家都知道 HTTP 是無狀態傳輸協議,每次傳輸都不知道對方是誰,因此,體現在以下方面:

  • 每次要獲取資料前,都會進行三次握手確認與四次揮手的過程。
  • 不能建立長連線進行通訊,多次請求
  • 資料轉換效率低,無論是使用 form 表單或者 json 傳輸,都不如直接傳輸二進位制資料來得快,序列化與反序列化資源佔用高

快速使用原生 RPC

首先通訊雙方都應擁有同樣結構體,所以服務端與客戶端都建立建立 go_rpc -----> RpcParams.go:

package go_rpc

type MyWayRpc struct{ // 客戶端要傳輸的資料,也是服務端要接收的資料
   Name string
   Age int
}

type MyWayRpcReply struct{ // 服務端要返回的資料,也是客戶端想要獲得的結果
   SystemInfo string
}

服務端編寫 main.go 檔案 :

package main

import (
   "go_rpc"
   "fmt"
   "log"
   "net"
   "net/rpc"
   "runtime"
)

// 其實按照web api中,我們應當把TakeMyWayRpc結構體和 GetSystem方法寫在單獨的控制器中,這裡為了程式碼演示就寫在main包下了

type TakeMyWayRpc struct{}

func (t TakeMyWayRpc) GetSystem(arg *go_rpc.MyWayRpc, result *go_rpc.MyWayRpcReply) error {
   fmt.Println("客戶端傳送了:",arg.Name,arg.Age)
   //返回給服務端的
   result.SystemInfo = runtime.GOOS
   return nil
}

func main() {
   tmwr := new(TakeMyWayRpc)
   err := rpc.Register(tmwr) // 註冊RPC可以一次性註冊多個RPC服務,可以用FOR註冊多個結構體,用web api的話就是註冊多個控制器
   if err !=nil{
      log.Fatalln("註冊方法時出現問題:",err)
   }
   l, err := net.Listen("tcp", ":30001") // 從這裡就可以看出,實際上RPC的底層也是基於TCP連結的,我們這裡開放30001埠,供客戶端連入
   defer l.Close()
   if err != nil {
      fmt.Println("監聽失敗,埠可能已經被佔用")
   }
   fmt.Println("正在監聽30001埠")
   for {
      var conn net.Conn
      conn, err = l.Accept()
      if err !=nil{
         log.Fatalln("建立控制程式碼失敗")
      }
      go rpc.ServeConn(conn)
   }
}

開始在另外一臺伺服器上寫客戶端的 main.go:

package main

import (
   "go_rpc"
   "log"
   "net/rpc"
)

func main() {
   client , err := rpc.Dial("tcp","服務端IP:30001")
   if err !=nil{
      log.Fatalln("這裡試試用錯誤的東西:",err)
   }
   defer client.Close()

   var myWayRpcArg go_rpc.MyWayRpc  // 初始化客戶端要傳送的內容
   var myWayRpcReply go_rpc.MyWayRpcReply // 初始化服務端要返回的內容

   // 填客戶端要傳送的內容
   myWayRpcArg.Name = "安彥飛啊"
   myWayRpcArg.Age = 31

   if err = client.Call("TakeMyWayRpc.GetSystem",myWayRpcArg,&myWayRpcReply);err !=nil{ // 直接開始傳送給服務端,並獲得服務端的響應
      log.Fatalln("返回服務端資料錯誤:",err)
   }
   log.Println("返回成功,",myWayRpcReply)
}

RPC 演示:

為什麼需要 APRC:

很好,通過上面的例子,已經可以寫出 RPC 的資訊互動了,但如果遇到需要服務端主動給客戶端發訊息,客戶端非同步呼叫服務端的函式,這些就不是原生 RPC 能夠做到的了

我們做一個 ARPC 的簡單示例:

使用 ARPC 前應當先:

go get github.com/lesismal/arpc

其實 ARPC 看上去更像是服務端定義了一個 ROUTER,客戶端去根據路由定址找到了這個函式

首先還是看服務端的實現,服務端 main.go 如下:

package main

import (
    "github.com/lesismal/arpc"
    "log"
    "runtime"
)

func main() {
    server := arpc.NewServer()
    registerHandler := server.Handler

    testStruct := new(TestArpcStruct)

    registerHandler.Handle( // 若這裡有多個,就像寫router
            "/TestArpcStruct.GetSystem",
        testStruct.GetSystem,
        )
    server.Run(":8888")
}

type TestArpcStruct struct{}

func (TestArpcStruct) GetSystem(ctx *arpc.Context)  {
    var str string
    if err := ctx.Bind(&str);err ==nil{
        log.Println(str)
        ctx.Write(runtime.GOOS)
    }
}

然後寫客戶端的程式碼:

package main

import (
    "github.com/lesismal/arpc"
    "log"
    "net"
    "time"
)

func main() {
    client , err := arpc.NewClient(func() (net.Conn, error) {
        return net.DialTimeout("tcp","localhost:8888",3 * time.Second)
    })
    if err !=nil{
        panic(err)
    }
    defer client.Stop()

    req := "hello"
    resp := ""
    err  = client.Call("/TestArpcStruct.GetSystem",&req,&resp,5 * time.Second)
    if err != nil {
        log.Fatalf("Call failed: %v", err)
    } else {
        log.Printf("Call Response: \"%v\"", resp)
    }
}

最後也能和 RPC 一樣,得到東西:

客戶端發出 hello,服務端 str 獲得了 hello 並列印,且像客戶端傳送了當前伺服器的執行系統名 runtime.GOOS,客戶端:Call Response : windows/linux 這樣的結果

總結

其實 ARPC 還有很多其他功能,比如還可以提供給 WS 的呼叫方法等,官方壓測後效能與 RPC 幾乎持平,甚至比 RPC 更高。

我們在使用一項新技術之前,一定要弄明白這項新技術為什麼會被發明出來,解決了什麼痛點,提升了怎樣的效能,努力思考實際應用場景在哪裡,比如 ARPC 可以用到即時通訊,可以複用連線池,不用資源一直申請與釋放,監控資料的實時展示等。

參考連結

https://github.com/lesismal/arpc

更多原創文章乾貨分享,請關注公眾號
  • Go 原生 RPC 與 APRC 的簡單使用
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章