zoukankan      html  css  js  c++  java
  • 使用go reflect实现一套简易的rpc框架

    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的完整实现

  • 相关阅读:
    【转】 Linux进程间通信
    Django中的Templates
    Django中的应用
    url的使用
    Django框架的使用
    Django的安装
    文件上传
    flask中的request和response
    模板
    静态文件处理
  • 原文地址:https://www.cnblogs.com/xiaowangba/p/6313772.html
Copyright © 2011-2022 走看看