使用go reflect实现一套简易的rpc框架

本文介绍了一种基于Go语言的RPC框架实现,详细讨论了包协议的设计、服务端RPC函数的注册与调用、客户端的函数替换及错误处理等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

转载于:https://www.cnblogs.com/xiaowangba/p/6313772.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值