使用golang 实现JSON-RPC2.0
Posted Golang语言社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用golang 实现JSON-RPC2.0相关的知识,希望对你有一定的参考价值。
什么是RPC?
远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用。
远程过程调用是一个分布式计算的客户端-服务器(Client/Server)的例子,它简单而又广受欢迎。远程过程调用总是由客户端对服务器发出一个执行若干过程请求,并用客户端提供的参数。执行结果将返回给客户端。由于存在各式各样的变体和细节差异,对应地派生了各式远程过程调用协议,而且它们并不互相兼容。——————源自维基百科
什么又是JSON-RPC?
JSON-RPC,是一个无状态且轻量级的远程过程调用(RPC)传送协议,其传递内容通过 JSON 为主。相较于一般的 REST 通过网址(如 GET /user)调用远程服务器,JSON-RPC 直接在内容中定义了欲调用的函数名称(如 {“method”: “getUser”}),这也令开发者不会陷于该使用 PUT 或者 PATCH 的问题之中。
更多JSON-RPC约定参见:https://zh.wikipedia.org/wiki/JSON-RPC
问题
服务端注册及调用
约定如net/rpc
:
the method’s type is exported.
the method is exported.
the method has two arguments, both exported (or builtin) types.
the method’s second argument is a pointer.
the method has return type error.
1// 这就是约定
2func (t *T) MethodName(argType T1, replyType *T2) error
3
那么问题来了:
1问题1: Server怎么来注册`t.Methods`?
2问题2: JSON-RPC请求参数里面的Params给到args?
server端类型定义:
1type methodType struct {
2 method reflect.Method // 用于调用
3 ArgType reflect.Type
4 ReplyType reflect.Type
5}
6type service struct {
7 name string // 服务的名字, 一般为`T`
8 rcvr reflect.Value // 方法的接受者, 即约定中的 `t`
9 typ reflect.Type // 注册的类型, 即约定中的`T`
10 method map[string]*methodType // 注册的方法, 即约定中的`MethodName`的集合
11}
12// Server represents an RPC Server.
13type Server struct {
14 serviceMap sync.Map // map[string]*service
15}
解决问题1,参考了net/rpc
中的注册调用。主要使用reflect
这个包。代码如下:
1// 解析传入的类型及相应的可导出方法,将rcvr的type,Methods的相关信息存放到Server.m中。
2// 如果type是不可导出的,则会报错
3func (s *Server) Register(rcvr interface{}) error {
4 _service := new(service)
5 _service.typ = reflect.TypeOf(rcvr)
6 _service.rcvr = reflect.ValueOf(rcvr)
7 sname := reflect.Indirect(_service.rcvr).Type().Name()
8 if sname == "" {
9 err_s := "rpc.Register: no service name for type " + _service.typ.String()
10 log.Print(err_s)
11 return errors.New(err_s)
12 }
13 if !isExported(sname) {
14 err_s := "rpc.Register: type " + sname + " is not exported"
15 log.Print(err_s)
16 return errors.New(err_s)
17 }
18 _service.name = sname
19 _service.method = suitableMethods(_service.typ, true)
20 // sync.Map.LoadOrStore
21 if _, dup := s.m.LoadOrStore(sname, _service); dup {
22 return errors.New("rpc: service already defined: " + sname)
23 }
24 return nil
25}
26// 关于suitableMethods,也是使用reflect,
27// 来获取_service.typ中的所有可导出方法及方法的相关参数,保存成*methodType
suitableMethods代码由此去:https: //github.com/yeqown/rpc/blob/master/server.go#L105
解决问题2,要解决问题2,且先看如何调用Method,代码如下:
1// 约定: func (t *T) MethodName(argType T1, replyType *T2) error
2// s.rcvr: 即约定中的 t
3// argv: 即约定中的 argType
4// replyv: 即约定中的 replyType
5func (s *service) call(mtype *methodType, req *Request, argv, replyv reflect.Value) *Response {
6 function := mtype.method.Func
7 returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
8 errIter := returnValues[0].Interface()
9 errmsg := ""
10 if errIter != nil {
11 errmsg = errIter.(error).Error()
12 return NewResponse(req.ID, nil, NewJsonrpcErr(InternalErr, errmsg, nil))
13 }
14 return NewResponse(req.ID, replyv.Interface(), nil)
15}
1 |
看了如何调用,再加上JSON-RPC的约定,知道了传给服务端的是一个JSON,而且其中的Params
是一个json格式的数据。那就变成了:interface{}
- req.Params 到reflect.Value
- argv。那么怎么转换呢?看代码:
1func (s *Server) call(req *Request) *Response {
2 // ....
3 // 根据req.Method来查询method
4 // req.Method 格式如:"ServiceName.MethodName"
5 // mtype *methodType
6 mtype := svc.method[methodName]
7 // 根据注册时候的mtype.ArgType来生成一个reflect.Value
8 argIsValue := false // if true, need to indirect before calling.
9 var argv reflect.Value
10 if mtype.ArgType.Kind() == reflect.Ptr {
11 argv = reflect.New(mtype.ArgType.Elem())
12 } else {
13 argv = reflect.New(mtype.ArgType)
14 argIsValue = true
15 }
16 if argIsValue {
17 argv = argv.Elem()
18 }
19 // 为argv参数生成了一个reflect.Value,但是argv目前为止都还是是0值。
20 // 那么怎么把req.Params 复制给argv ?
21 // 我尝试过,argv = reflect.Value(req.Params),但是在调用的时候 会提示说:“map[string]interface{} as main.*Args”,
22 // 这也就是说,并没有将参数的值正确的赋值给argv。
23 // 后面才又了这个convert函数:
24 // bs, _ := json.Marshal(req.Params)
25 // json.Unmarshal(bs, argv.Interface())
26 // 因此有一些限制~,就不多说了
27 convert(req.Params, argv.Interface())
28 // Note: 约定中ReplyType是一个指针类型,方便赋值。
29 // 根据注册时候的mtype.ReplyType来生成一个reflect.Value
30 replyv := reflect.New(mtype.ReplyType.Elem())
31 switch mtype.ReplyType.Elem().Kind() {
32 case reflect.Map:
33 replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem()))
34 case reflect.Slice:
35 replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0))
36 }
37 return svc.call(mtype, req, argv, replyv)
38}
|
支持HTTP调用
已经完成了上述的部分,再来谈支持HTTP就非常简单了。实现http.Handler
接口就行啦~。如下:
1// 支持之POST & json
2func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3 var resp *Response
4 w.Header().Set("Content-Type", "application/json")
5 if r.Method != http.MethodPost {
6 resp = NewResponse("", nil, NewJsonrpcErr(
7 http.StatusMethodNotAllowed, "HTTP request method must be POST", nil),
8 )
9 response(w, resp)
10 return
11 }
12 // 解析请求参数到[]*rpc.Request
13 reqs, err := getRequestFromBody(r)
14 if err != nil {
15 resp = NewResponse("", nil, NewJsonrpcErr(InternalErr, err.Error(), nil))
16 response(w, resp)
17 return
18 }
19 // 处理请求,包括批量请求
20 resps := s.handleWithRequests(reqs)
21 if len(resps) > 1 {
22 response(w, resps)
23 } else {
24 response(w, resps[0])
25 }
26 return
27}
|
使用示例
服务端使用
1// test_server.go
2package main
3import (
4 // "fmt"
5 "net/http"
6 "github.com/yeqown/rpc"
7)
8type Int int
9type Args struct {
10 A int `json:"a"`
11 B int `json:"b"`
12}
13func (i *Int) Sum(args *Args, reply *int) error {
14 *reply = args.A + args.B
15 return nil
16}
17type MultyArgs struct {
18 A *Args `json:"aa"`
19 B *Args `json:"bb"`
20}
21type MultyReply struct {
22 A int `json:"aa"`
23 B int `json:"bb"`
24}
25func (i *Int) Multy(args *MultyArgs, reply *MultyReply) error {
26 reply.A = (args.A.A * args.A.B)
27 reply.B = (args.B.A * args.B.B)
28 return nil
29}
30func main() {
31 s := rpc.NewServer()
32 mine_int := new(Int)
33 s.Register(mine_int)
34 go s.HandleTCP("127.0.0.1:9999")
35 // 开启http
36 http.ListenAndServe(":9998", s)
37}
客户端使用
1// test_client.go
2package main
3import (
4 "github.com/yeqown/rpc"
5)
6type Args struct {
7 A int `json:"a"`
8 B int `json:"b"`
9}
10type MultyArgs struct {
11 A *Args `json:"aa"`
12 B *Args `json:"bb"`
13}
14type MultyReply struct {
15 A int `json:"aa"`
16 B int `json:"bb"`
17}
18func main() {
19 c := rpc.NewClient()
20 c.DialTCP("127.0.0.1:9999")
21 var sum int
22 c.Call("1", "Int.Sum", &Args{A: 1, B: 2}, &sum)
23 println(sum)
24 c.DialTCP("127.0.0.1:9999")
25 var reply MultyReply
26 c.Call("2", "Int.Multy", &MultyArgs{A: &Args{1, 2}, B: &Args{3, 4}}, &reply)
27 println(reply.A, reply.B)
28}
1 |
运行截图
server
client
http-support
实现
缺陷
只支持JSON-RPC, 且还没有完全实现JSON-RPC的约定。譬如批量调用中:
若批量调用的 RPC 操作本身非一个有效 JSON 或一个至少包含一个值的数组,则服务端返回的将单单是一个响应对象而非数组。若批量调用没有需要返回的响应对象,则服务端不需要返回任何结果且必须不能返回一个空数组给客户端。
阅读参考中的两个RPC,发现两者都是使用的codec
的方式来提供扩展。因此以后可以考虑使用这种方式来扩展。
参考
net/rpc
grollia/rpc
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
Golang语言社区
ID:Golangweb
游戏服务器架构丨分布式技术丨大数据丨游戏算法学习
以上是关于使用golang 实现JSON-RPC2.0的主要内容,如果未能解决你的问题,请参考以下文章
.NET 开源项目 StreamJsonRpc 介绍[上篇]