使用go reflect實現一套簡易的rpc框架

jopen 10年前發布 | 31K 次閱讀 Go語言 Google Go/Golang開發

go jsonrpc

在實際項目中,我們經常會碰到服務之間交互的情況,如何方便的與遠端服務進行交互,就是一個需要我們考慮的問題。

通常,我們可以采用restful的編程方式,各個服務提供相應的web接口,相互之間通過http方式進行調用。或者采用rpc方式,約定json格式進行數據交互。

在我們的項目中,服務端對用戶客戶端提供的是restful的接口方式,而在服務器內部,我們則采用rpc方式進行服務之間的交互。

go語言本來就提供了jsonrpc的支持,所以自然開始我們就直接使用jsonrpc。jsonrpc的使用非常簡單,對于調用端來說,就如同一個函數調用,如下:

args := &Args{7, 8}
reply := new(Reply)
err := client.Call("Arith.Add", args, reply)

上面是go jsonrpc自帶的一個例子,可以看到,雖然我們通過call(rpcName, inParams, outParams)這樣的形式可以很方便的進行rpc的調用,但是跟go實際的函數調用還是稍微有一點區別,對我來說,這么使用總覺得很別扭。

我覺得方便的rpc使用方式

對于go jsonrpc來說,它的調用格式是這樣的

err := call(name, in, out)

但是對我來說,我希望采用這樣的調用方式:

out, err := RpcFunc(in)

假設server端有如下的一個RPC函數,注冊的rpc name為testrpc。

func Test(id int) (int, error)

對于client端來說,我希望的使用方式是這樣:

var rpcTest func(id int) (int, error)
MakeRpc("testrpc", &rpcTest)

id, err := rpcTest(10)

在client端,我們首先聲明了跟server Test類型完全一致的一個函數變量。然后通過MakeRpc接口將其命名為testrpc,并且將rpcTest綁定到實際的rpc函數上面,最后rpcTest就跟普通函數的調用一樣。

可以看到,這種使用方式跟jsonrpc最大的不同在于rpc的返回值直接就是函數自身的返回值。而這個可能更符合我使用go函數的習慣。

實現

實現一套rpc框架需要考慮server,client以及包協議的問題。

包協議

我使用了最簡單的包頭 + 實際數據的做法,包頭使用一個4字節的int表示后續數據的長度。而對于實際的rpc數據,我采用的是gob進行打包解包。

為什么選用gob而不是json?主要在于我不想自己做數據類型的轉換,在json中,int類型的encode,decode會變成float類型的,如果函數需要的參數是int,json decode之后還需要我們自己根據參數實際的類型進行轉換。增加了復雜度。而gob則在encode時候會加上實際的數據類型,這樣decode之后我就能直接使用。

而且gob還支持注冊自定義的類型,但是為了簡單,建議只支持基本的數據類型,因為對于rpc來說,傳遞復雜的數據類型進行函數調用,我總覺得有點復雜,這在設計上面已經有問題了。

server

在server需要解決的問題就是rpc函數注冊并通過名字能進行該rpc函數調用。而這個通過reflect就能非常方便的實現,一個通過函數名字進行函數調用的例子:

func Test(id int) (string, error) {
    return "abc", nil
}

funcmap  = map[string]reflect.Value{}

v := reflect.ValueOf(Test)

funcmap["test_rpc"] = v

args := []reflect.Value{reflect.ValueOf(10)}

funcmap["test_rpc"](args)

client

在client層,我們需要關注在聲明一個rpc原型的函數變量之后,如何將其替換成另一個函數進行rpc調用。我們可以通過reflect的MakeFunc函數方便的做到,go自身的例子:

swap := func(in []reflect.Value) []reflect.Value {
    return []reflect.Value{in[1], in[0]}
}

 makeSwap := func(fptr interface{}) {
    fn := reflect.ValueOf(fptr).Elem()
    v := reflect.MakeFunc(fn.Type(), swap)
    fn.Set(v)
}

var intSwap func(int, int) (int, int)
makeSwap(&intSwap)
fmt.Println(intSwap(0, 1))

MakeFunc的原理在于,根據傳入的函數變量的類型,創建一個新的函數,該函數調用的是我們指定的另一個函數。

同時,我們得到傳入變量的指針,并用新的函數重新給該變量賦值。

error處理

因為rpc調用可能會出現其他錯誤,譬如網絡斷線,gob encode錯誤等,client在調用的時候必須得處理這些錯誤,暴力的作法就是如果是這種內部錯誤,我們直接panic,但是我覺得太不友好,所以我們約定,所有的rpc函數在最后一個返回值必須是error。這樣就是是rpc內部的錯誤,我們也能夠通過error返回。

在注冊rpc的時候,我們可以通過判斷最后一個返回值是否是interface,同時是否具有Error函數來強制要求必須為error。如下

v := reflect.ValueOf(rpcFunc)

nOut := v.Type().NumOut()

if nOut == 0 || v.Type().Out(nOut-1).Kind() != reflect.Interface {
    err = fmt.Errorf("%s return final output param must be error interface", name)
    return
}

_, b := v.Type().Out(nOut - 1).MethodByName("Error")
if !b {
    err = fmt.Errorf("%s return final output param must be error interface", name)
    return
}

但是,如果在MakeFunc里面直接返回error,會出現“reflect: function created by MakeFunc using closure returned wrong type: have *errors.errorString for error”這樣的問題,主要在于reflect.Value需要知道我們error的接口類型,參考這里

所以,我們通過如下方式對error進行處理,轉成相應的reflect.Value

v := reflect.ValueOf(&e).Elem()

nil處理

在實際rpc中,我們可能還會面臨參數為nil的問題,如果直接對nil進行reflect.ValueOf,是得不到我們期望的類型的,這時候的Kind是0,reflect壓根不能將其正確的轉換成函數實際的類型。

當碰到nil的情況,我們只需要根據當前函數參數實際的類型,生成一個Zero Value,就可以很方便的解決這個問題:

假設函數第一個返回值為nil,那么我們這樣

v := reflect.Zero(fn.Type().Out(0))

代碼

最開始寫了一個代碼片段驗證自己的想法,在這里

一個rpc frame的完整實現

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!