golang反射学习后做一个小练习,使用100行代码实现一个通用的RPC服务。
简要说明
golang 的RPC框架还是非常丰富的,比如 gRPC,go-zero, go-dubbo 等都是使用非常普遍的rpc框架。在go语言实现的RPC客户端中,大部分RPC框架采用的是使用生成代码的方式来构建RPC服务。即:定义好相应的接口后,需要通过命令生成相应的代码。采用这种方式的优点在于可以减少不必要的类型转换;而麻烦之处也显而易见,需要在每次结构发生改变时,重新生成对应的代码。那么,如果不采用命令行生成的方式来调用RPC该怎么做呢?经过对golang反射的学习后,让我们用100行代码来小试牛刀,实现一个极简版的RPC。
协议的定义
由于极简,我们采用HTTP协议,数据传输采用最常见的json结构。 服务请求,通过http 请求路径判断调用哪个方法。
- • 输入参数定义如下:
["参数1", "参数2"]
其中参数1,参数2 采用Json编码,最终的请求参数在做一次编码。 例如:Do("abc", 123)
,其post请求body为["\"abc\"", 123]
- • 输出参数定义与输入参数定义格式相同
如何使用
服务端:
服务端需要实现每一个接口,并把接口绑定到对应的路由上。
package main
import "github.com/lpflpf/rpc"
import "strconv"
func main() {
serv := rpc.NewRpcServ("127.0.0.1:18080")
serv.Impl("/conv/int2str", strconv.Itoa) // 路由绑定到方法
serv.Impl("/conv/str2int", strconv.Atoi)
serv.Impl("/math/add", func(a, b int) int { return a + b })
serv.Start()
}
客户端调用
客户端仅需要定义对应的rpc服务的方法,并通过struct tag的方式指定路由即可
package main
import "fmt"
import "github.com/lpflpf/rpc"
type Conv struct {
Int2Str func(int) string `rpc:"conv/int2str"`
Str2Int func(input string) (int, error) `rpc:"conv/str2int"`
}
type Math struct {
Add func(int, int) int `rpc:"math/add"`
}
func main() {
conv := &Conv{}
rpc.Connect("http://127.0.0.1:18080", conv) // 连接RPC 服务
fmt.Println(conv.Int2Str(123), conv.Int2Str(456)) // 123 456
fmt.Println(conv.Str2Int("1234")) // 1234
math := &Math{}
rpc.Connect("http://127.0.0.1:18080", math) // 连接 RPC 服务
fmt.Println(math.Add(1, 2)) // 3
}
Server 端的实现
服务端主要是将注册路由。在处理请求时,需要将请求的数据转化为注册句柄的参数,并将句柄的处理结果编码,并返回给客户端。代码如下:
package rpc
import "net/http"
import "reflect"
import "encoding/json"
import "io/ioutil"
type RpcServ struct {
serv *http.Server
mux *http.ServeMux
}
func (rs *RpcServ) Impl(router string, f interface{}) {
rs.mux.HandleFunc(router, func(rw http.ResponseWriter, request *http.Request) {
rt := reflect.TypeOf(f)
requestBody, _ := ioutil.ReadAll(request.Body)
requestData := []string{}
_ = json.Unmarshal(requestBody, &requestData)
params := []reflect.Value{}
num := rt.NumIn()
if rt.IsVariadic() {
num = num - 1
}
for i := 0; i