使用golang 實現JSON-RPC2.0

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文同時發佈於我的部落格 yeqown.github.io

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

那麼問題來了:

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

server端類型定義:

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,代碼如下:

// 約定:    func (t *T) MethodName(argType T1, replyType *T2) error// s.rcvr: 即約定中的 t// argv:   即約定中的 argType// replyv: 即約定中的 replyTypefunc (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 & jsonfunc (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.gopackage mainimport (    // "fmt"    "net/http"    "github.com/yeqown/rpc")type Int inttype 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.gopackage mainimport (    "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)}

運行



實現

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

缺陷

只支援JSON-RPC, 且還沒有完全實現JSON-RPC的約定。譬如批量調用中:

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

閱讀參考中的兩個RPC,發現兩者都是使用的codec的方式來提供擴充。因此以後可以考慮使用這種方式來擴充。

參考

  • net/rpc
  • grollia/rpc
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.