使用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的完整实现。
使用go reflect实现一套简易的rpc框架的更多相关文章
- 一个最最简易的RPC框架雏形---转载自梁飞的博客
查阅RPC与HTTP区别的时候, 无意间发现一篇博客,内容是一个简易的RPC服务框架, 仔细一看, 不得了,博主竟然就是阿里dubbo的作者. 原文链接在此: http://javatar.iteye ...
- 手写简易版RPC框架基于Socket
什么是RPC框架? RPC就是远程调用过程,实现各个服务间的通信,像调用本地服务一样. RPC有什么优点? - 提高服务的拓展性,解耦.- 开发人员可以针对模块开发,互不影响.- 提升系统的可维护性及 ...
- 程序员修神之路--设计一套RPC框架并非易事
菜菜哥,我最近终于把Socket通信调通了 这么底层的东西你现在都会了,恭喜你离涨薪又进一步呀 http协议不也是利用的Socket吗 可以这么说,http协议是基于TCP协议的,底层的数据传输可以说 ...
- RPC框架简易实现
RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.RPC协议假定某些 ...
- 自行实现一个简易RPC框架
10分钟写一个RPC框架 1.RpcFramework package com.alibaba.study.rpc.framework; import java.io.ObjectInputStrea ...
- 简易RPC框架-学习使用
*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...
- 简易RPC框架-心跳与重连机制
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
- 简易RPC框架-客户端限流配置
*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...
- 简易RPC框架-SPI
*:first-child { margin-top: 0 !important; } body > *:last-child { margin-bottom: 0 !important; } ...
随机推荐
- Goland 提示 :configuration is still incorrect 的解决
安装好 Goland 后,调试编译的时候提示 goland configuration is still incorrect,百度 和 Google 都没有明确答案 Google 上有一些提示,但是也 ...
- Oracle中备份用户对象的两种方法
方法1: 执行步骤: exp userid=用户名/密码@数据库名 file=c:\emp.dmp 使用当前用户导出 exp userid=sys/sys@数据库名 file=c:\emp.dmp o ...
- kexec 内核快速启动流程分析
一.命令行 1. kexec -l $kpwd --append="$arg" 其中$kpwd =目标内核的路径 $arg =传给内核的参数,与/proc/cmdline一致时表示 ...
- 百钱买百鸡问题Java
//百钱买百鸡public class baiqianbaiji { static void BQBJ(int m,int n)//m为钱的总数,n为鸡数 { int z; for(int x = 0 ...
- ListView常见的优化方式简述
ListView的优化 对于ListView来说,应该算是布局中几种最常用的组件之一了,使用也十分方便,下面个大家介绍一下两种常见的优化方式. 1.条目复用优化 其实listview的工作原理就是,l ...
- 计算机网络之IP地址
IP地址的分类 整个的因特网就是一个单一的.抽象的网络.IP地址就是给因特网上的每一个主机(或路由器)的每一个接口分配一个在全世界范围内唯一的32位的标识符. 所谓分类的IP地址,就是将IP地址划分为 ...
- Spark-SQL之DataFrame操作大全
Spark SQL中的DataFrame类似于一张关系型数据表.在关系型数据库中对单表或进行的查询操作,在DataFrame中都可以通过调用其API接口来实现.可以参考,Scala提供的DataFra ...
- 23 服务音乐的启动Demo4
注意如果音乐服务和Activity在一个应用中那么将不会因为绑定的Activity销毁而关闭音乐 MainActivity.java package com.qf.day23_service_demo ...
- UIKit视图动画的微扩展
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 好久没写blog了,还不快快写来- ;] 我们知道在UIKit ...
- 有两个序列a,b,大小都为n,序列元素的值是任意整数,无序。
要求:通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小. 例如: var a=[100,99,98,1,2, 3]; var b=[1, 2, 3, 4,5,40]. in ...