使用golang 實現JSON-RPC2.0

本文同時發佈於個人博客 yeqown.github.io

什麼是RPC?

遠程過程調用(英語:Remote Procedure Call,縮寫爲 RPC)是一個計算機通訊協議。該協議容許運行於一臺計算機的程序調用另外一臺計算機的子程序,而程序員無需額外地爲這個交互做用編程。若是涉及的軟件採用面向對象編程,那麼遠程過程調用亦可稱做遠程調用或遠程方法調用。git

遠程過程調用是一個分佈式計算的客戶端-服務器(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-RPCgithub

問題

服務端註冊及調用

約定如net/rpcgolang

  • 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.
// 這就是約定
func (t *T) MethodName(argType T1, replyType *T2) error

那麼問題來了:編程

問題1: Server怎麼來註冊`t.Methods`?
    問題2: JSON-RPC請求參數裏面的Params給到args?

server端類型定義:json

type methodType struct {
    method     reflect.Method // 用於調用
    ArgType    reflect.Type
    ReplyType  reflect.Type
}

type service struct {
    name   string                 // 服務的名字, 通常爲`T`
    rcvr   reflect.Value          // 方法的接受者, 即約定中的 `t`
    typ    reflect.Type           // 註冊的類型, 即約定中的`T`
    method map[string]*methodType // 註冊的方法, 即約定中的`MethodName`的集合
}

// Server represents an RPC Server.
type Server struct {
    serviceMap sync.Map   // map[string]*service
}

解決問題1,參考了net/rpc中的註冊調用。主要使用reflect這個包。代碼以下:數組

// 解析傳入的類型及相應的可導出方法,將rcvr的type,Methods的相關信息存放到Server.m中。
// 若是type是不可導出的,則會報錯
func (s *Server) Register(rcvr interface{}) error {
    _service := new(service)
    _service.typ = reflect.TypeOf(rcvr)
    _service.rcvr = reflect.ValueOf(rcvr)
    sname := reflect.Indirect(_service.rcvr).Type().Name()

    if sname == "" {
        err_s := "rpc.Register: no service name for type " + _service.typ.String()
        log.Print(err_s)
        return errors.New(err_s)
    }

    if !isExported(sname) {
        err_s := "rpc.Register: type " + sname + " is not exported"
        log.Print(err_s)
        return errors.New(err_s)
    }
    _service.name = sname
    _service.method = suitableMethods(_service.typ, true)

    // sync.Map.LoadOrStore
    if _, dup := s.m.LoadOrStore(sname, _service); dup {
        return errors.New("rpc: service already defined: " + sname)
    }
    return nil
}

// 關於suitableMethods,也是使用reflect,
// 來獲取_service.typ中的全部可導出方法及方法的相關參數,保存成*methodType

suitableMethods代碼由此去:https: //github.com/yeqown/rpc/blob/master/server.go#L105服務器

解決問題2,要解決問題2,且先看如何調用Method,代碼以下:app

// 約定:    func (t *T) MethodName(argType T1, replyType *T2) error
// s.rcvr: 即約定中的 t
// argv:   即約定中的 argType
// replyv: 即約定中的 replyType
func (s *service) call(mtype *methodType, req *Request, argv, replyv reflect.Value) *Response {
    function := mtype.method.Func
    returnValues := function.Call([]reflect.Value{s.rcvr, argv, replyv})
    errIter := returnValues[0].Interface()

    errmsg := ""
    if errIter != nil {
        errmsg = errIter.(error).Error()
        return NewResponse(req.ID, nil, NewJsonrpcErr(InternalErr, errmsg, nil))
    }

    return NewResponse(req.ID, replyv.Interface(), nil)
}

看了如何調用,再加上JSON-RPC的約定,知道了傳給服務端的是一個JSON,並且其中的Params是一個json格式的數據。那就變成了:interface{} - req.Params 到reflect.Value - argv。那麼怎麼轉換呢?看代碼:分佈式

func (s *Server) call(req *Request) *Response {
    // ....
    // 根據req.Method來查詢method
    // req.Method 格式如:"ServiceName.MethodName"
    // mtype *methodType
    mtype := svc.method[methodName]

    // 根據註冊時候的mtype.ArgType來生成一個reflect.Value
    argIsValue := false // if true, need to indirect before calling.
    var argv reflect.Value
    if mtype.ArgType.Kind() == reflect.Ptr {
        argv = reflect.New(mtype.ArgType.Elem())
    } else {
        argv = reflect.New(mtype.ArgType)
        argIsValue = true
    }

    if argIsValue {
        argv = argv.Elem()
    }

    // 爲argv參數生成了一個reflect.Value,可是argv目前爲止都仍是是0值。
    // 那麼怎麼把req.Params 複製給argv ?
    // 我嘗試過,argv = reflect.Value(req.Params),可是在調用的時候 會提示說:「map[string]interface{} as main.*Args」,
    // 這也就是說,並無將參數的值正確的賦值給argv。
    // 後面才又了這個convert函數:
    // bs, _ := json.Marshal(req.Params)
    // json.Unmarshal(bs, argv.Interface())
    // 所以有一些限制~,就很少說了 
    convert(req.Params, argv.Interface())

    // Note: 約定中ReplyType是一個指針類型,方便賦值。
    // 根據註冊時候的mtype.ReplyType來生成一個reflect.Value
    replyv := reflect.New(mtype.ReplyType.Elem())
    switch mtype.ReplyType.Elem().Kind() {
    case reflect.Map:
        replyv.Elem().Set(reflect.MakeMap(mtype.ReplyType.Elem()))
    case reflect.Slice:
        replyv.Elem().Set(reflect.MakeSlice(mtype.ReplyType.Elem(), 0, 0))
    }

    return svc.call(mtype, req, argv, replyv)
}

支持HTTP調用

已經完成了上述的部分,再來談支持HTTP就很是簡單了。實現http.Handler接口就行啦~。以下:

// 支持之POST & json
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var resp *Response

    w.Header().Set("Content-Type", "application/json")

    if r.Method != http.MethodPost {
        resp = NewResponse("", nil, NewJsonrpcErr(
            http.StatusMethodNotAllowed, "HTTP request method must be POST", nil),
        )
        response(w, resp)
        return
    }
    // 解析請求參數到[]*rpc.Request
    reqs, err := getRequestFromBody(r)
    if err != nil {
        resp = NewResponse("", nil, NewJsonrpcErr(InternalErr, err.Error(), nil))
        response(w, resp)
        return
    }

    // 處理請求,包括批量請求
    resps := s.handleWithRequests(reqs)

    if len(resps) > 1 {
        response(w, resps)
    } else {
        response(w, resps[0])
    }
    return
}

使用示例

服務端使用

// test_server.go
package main

import (
    // "fmt"
    "net/http"
    "github.com/yeqown/rpc"
)

type Int int

type Args struct {
    A int `json:"a"`
    B int `json:"b"`
}

func (i *Int) Sum(args *Args, reply *int) error {
    *reply = args.A + args.B
    return nil
}

type MultyArgs struct {
    A *Args `json:"aa"`
    B *Args `json:"bb"`
}

type MultyReply struct {
    A int `json:"aa"`
    B int `json:"bb"`
}

func (i *Int) Multy(args *MultyArgs, reply *MultyReply) error {
    reply.A = (args.A.A * args.A.B)
    reply.B = (args.B.A * args.B.B)
    return nil
}

func main() {
    s := rpc.NewServer()
    mine_int := new(Int)
    s.Register(mine_int)
    go s.HandleTCP("127.0.0.1:9999")

    // 開啓http
    http.ListenAndServe(":9998", s)
}

客戶端使用

// test_client.go
package main

import (
    "github.com/yeqown/rpc"
)

type Args struct {
    A int `json:"a"`
    B int `json:"b"`
}

type MultyArgs struct {
    A *Args `json:"aa"`
    B *Args `json:"bb"`
}

type MultyReply struct {
    A int `json:"aa"`
    B int `json:"bb"`
}

func main() {
    c := rpc.NewClient()
    c.DialTCP("127.0.0.1:9999")

    var sum int
    c.Call("1", "Int.Sum", &Args{A: 1, B: 2}, &sum)
    println(sum)

    c.DialTCP("127.0.0.1:9999")
    var reply MultyReply
    c.Call("2", "Int.Multy", &MultyArgs{A: &Args{1, 2}, B: &Args{3, 4}}, &reply)
    println(reply.A, reply.B)
}

運行截圖

server
client
http-support


實現

上面只挑了我以爲比較重要的部分,講了實現,更多如客戶端的支持,JSON-RPC的請求響應定義,能夠在項目中裏查閱。目前基於TCP和HTTP實現了JSON-RPC,項目地址:github.com/yeqown/rpc

缺陷

只支持JSON-RPC, 且尚未徹底實現JSON-RPC的約定。譬如批量調用中:

若批量調用的 RPC 操做自己非一個有效 JSON 或一個至少包含一個值的數組,則服務端返回的將單單是一個響應對象而非數組。若批量調用沒有須要返回的響應對象,則服務端不須要返回任何結果且必須不能返回一個空數組給客戶端。

閱讀參考中的兩個RPC,發現二者都是使用的codec的方式來提供擴展。所以之後能夠考慮使用這種方式來擴展。


參考

相關文章
相關標籤/搜索