diff --git a/jsonrpc2/client.go b/jsonrpc2/client.go index da4064c..ef51b5c 100644 --- a/jsonrpc2/client.go +++ b/jsonrpc2/client.go @@ -3,8 +3,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package jsonrpc2 implements a JSON-RPC 2.0 ClientCodec and ServerCodec -// for the net/rpc package. package jsonrpc2 import ( @@ -36,8 +34,8 @@ type clientCodec struct { pending map[uint64]string // map request id to method name } -// NewClientCodec returns a new rpc.ClientCodec using JSON-RPC 2.0 on conn. -func NewClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec { +// newClientCodec returns a new rpc.ClientCodec using JSON-RPC 2.0 on conn. +func newClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec { return &clientCodec{ dec: json.NewDecoder(conn), enc: json.NewEncoder(conn), @@ -241,7 +239,7 @@ func (c Client) Notify(serviceMethod string, args interface{}) error { // NewClient returns a new Client to handle requests to the // set of services at the other end of the connection. func NewClient(conn io.ReadWriteCloser) *Client { - codec := NewClientCodec(conn) + codec := newClientCodec(conn) client := rpc.NewClientWithCodec(codec) return &Client{client, codec.(*clientCodec)} } diff --git a/jsonrpc2/doc.go b/jsonrpc2/doc.go new file mode 100644 index 0000000..18a89d4 --- /dev/null +++ b/jsonrpc2/doc.go @@ -0,0 +1,61 @@ +/* +Package jsonrpc2 implements a JSON-RPC 2.0 ClientCodec and ServerCodec +for the net/rpc package and HTTP transport for JSON-RPC 2.0. + + +RPC method's signature + +JSON-RPC 2.0 support positional and named parameters. Which one should be +used when calling server's method depends on type of that method's first +parameter: if it is an Array or Slice then positional parameters should be +used, if it is a Map or Struct then named parameters should be used. (Also +any method can be called without parameters at all.) If first parameter +will be of custom type with json.Unmarshaler interface then it depends on +what is supported by that type - this way you can even implement method +which can be called both with positional and named parameters. + +JSON-RPC 2.0 support result of any type, so method's result (second param) +can be a reference of any type supported by json.Marshal. + +JSON-RPC 2.0 support error codes and optional extra error data in addition +to error message. If method returns error of standard error type (i.e. +just error message without error code) then error code -32000 will be +used. To define custom error code (and optionally extra error data) method +should return jsonrpc2.Error. + + +Using positional parameters of different types + +If you'll have to provide method which should be called using positional +parameters of different types then it's recommended to implement this +using first parameter of custom type with json.Unmarshaler interface. + +To call such a method you'll have to use client.Call() with []interface{} +in args. + + +Decoding errors on client + +Because of net/rpc limitations client.Call() can't return JSON-RPC 2.0 +error with code, message and extra data - it'll return either one of +rpc.ErrShutdown or io.ErrUnexpectedEOF errors, or encoded JSON-RPC 2.0 +error, which have to be decoded using jsonrpc2.ServerError to get error's +code, message and extra data. + + +Limitations + +HTTP does not support Pipelined Requests/Responses. + +HTTP does not support GET Request. + +Because of net/rpc limitations RPC method MUST NOT return standard +error which begins with '{' and ends with '}'. + +Because of net/rpc limitations there is no way to provide +transport-level details (like client's IP) to RPC method. + +Current implementation does a lot of sanity checks to conform to +protocol spec. Making most of them optional may improve performance. +*/ +package jsonrpc2 diff --git a/jsonrpc2/example_test.go b/jsonrpc2/example_test.go new file mode 100644 index 0000000..053b41a --- /dev/null +++ b/jsonrpc2/example_test.go @@ -0,0 +1,143 @@ +package jsonrpc2_test + +import ( + "errors" + "fmt" + "io" + "net" + "net/http" + "net/rpc" + + "github.com/powerman/rpc-codec/jsonrpc2" +) + +// A server wishes to export an object of type ExampleSvc: +type ExampleSvc struct{} + +// Method with positional params. +func (*ExampleSvc) Sum(vals [2]int, res *int) error { + *res = vals[0] + vals[1] + return nil +} + +// Method with positional params. +func (*ExampleSvc) SumAll(vals []int, res *int) error { + for _, v := range vals { + *res += v + } + return nil +} + +// Method with named params. +func (*ExampleSvc) MapLen(m map[string]int, res *int) error { + *res = len(m) + return nil +} + +type NameArg struct{ Fname, Lname string } +type NameRes struct{ Name string } + +// Method with named params. +func (*ExampleSvc) FullName(t NameArg, res *NameRes) error { + *res = NameRes{t.Fname + " " + t.Lname} + return nil +} + +// Method returns error with code -32000. +func (*ExampleSvc) Err1(struct{}, *struct{}) error { + return errors.New("some issue") +} + +// Method returns error with code 42. +func (*ExampleSvc) Err2(struct{}, *struct{}) error { + return jsonrpc2.NewError(42, "some issue") +} + +// Method returns error with code 42 and extra error data. +func (*ExampleSvc) Err3(struct{}, *struct{}) error { + return &jsonrpc2.Error{42, "some issue", map[string]int{"one": 1, "two": 2}} +} + +func Example() { + // Server export an object of type ExampleSvc. + rpc.Register(&ExampleSvc{}) + + // Server provide a TCP transport. + lnTCP, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + defer lnTCP.Close() + go func() { + for { + conn, err := lnTCP.Accept() + if err != nil { + return + } + go jsonrpc2.ServeConn(conn) + } + }() + + // Server provide a HTTP transport on /rpc endpoint. + http.Handle("/rpc", jsonrpc2.HTTPHandler(nil)) + lnHTTP, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + panic(err) + } + defer lnHTTP.Close() + go http.Serve(lnHTTP, nil) + + // Client use TCP transport. + clientTCP, err := jsonrpc2.Dial("tcp", lnTCP.Addr().String()) + if err != nil { + panic(err) + } + defer clientTCP.Close() + + // Client use HTTP transport. + clientHTTP := jsonrpc2.NewHTTPClient("http://" + lnHTTP.Addr().String() + "/rpc") + defer clientHTTP.Close() + + var reply int + + // Synchronous call using positional params and TCP. + err = clientTCP.Call("ExampleSvc.Sum", [2]int{3, 5}, &reply) + fmt.Printf("Sum(3,5)=%d\n", reply) + + // Synchronous call using positional params and HTTP. + err = clientHTTP.Call("ExampleSvc.SumAll", []int{3, 5, -2}, &reply) + fmt.Printf("SumAll(3,5,-2)=%d\n", reply) + + // Asynchronous call using named params and TCP. + startCall := clientTCP.Go("ExampleSvc.MapLen", + map[string]int{"a": 10, "b": 20, "c": 30}, &reply, nil) + replyCall := <-startCall.Done + fmt.Printf("MapLen({a:10,b:20,c:30})=%d\n", *replyCall.Reply.(*int)) + + // Notification using named params and HTTP. + clientHTTP.Notify("ExampleSvc.FullName", NameArg{"First", "Last"}) + + // Correct error handling. + err = clientTCP.Call("ExampleSvc.Err1", nil, nil) + if err == rpc.ErrShutdown || err == io.ErrUnexpectedEOF { + fmt.Printf("Err1(): %q\n", err) + } else if err != nil { + rpcerr := jsonrpc2.ServerError(err) + fmt.Printf("Err1(): code=%d msg=%q data=%v\n", rpcerr.Code, rpcerr.Message, rpcerr.Data) + } + + err = clientHTTP.Call("ExampleSvc.Err3", nil, nil) + if err == rpc.ErrShutdown || err == io.ErrUnexpectedEOF { + fmt.Printf("Err3(): %q\n", err) + } else if err != nil { + rpcerr := jsonrpc2.ServerError(err) + fmt.Printf("Err3(): code=%d msg=%q data=%v\n", rpcerr.Code, rpcerr.Message, rpcerr.Data) + } + + // Output: + // Sum(3,5)=8 + // SumAll(3,5,-2)=6 + // MapLen({a:10,b:20,c:30})=3 + // Err1(): code=-32000 msg="some issue" data= + // Err3(): code=42 msg="some issue" data=map[one:1 two:2] +} diff --git a/jsonrpc2/http.go b/jsonrpc2/http.go index 1643843..769c012 100644 --- a/jsonrpc2/http.go +++ b/jsonrpc2/http.go @@ -36,11 +36,11 @@ type httpHandler struct { } // HTTPHandler returns handler for HTTP requests which will execute -// incoming RPC using srv. If srv is nil then use rpc.DefaultServer. +// incoming JSON-RPC 2.0 over HTTP using srv. +// +// If srv is nil then rpc.DefaultServer will be used. // // Specification: http://www.simple-is-better.org/json-rpc/transport_http.html -// - Pipelined Requests/Responses not supported. -// - GET Request not supported. func HTTPHandler(srv *rpc.Server) http.Handler { if srv == nil { srv = rpc.DefaultServer diff --git a/jsonrpc2/server.go b/jsonrpc2/server.go index 19d1c7f..80c528e 100644 --- a/jsonrpc2/server.go +++ b/jsonrpc2/server.go @@ -38,6 +38,12 @@ type serverCodec struct { // which will use srv to execute batch requests. // // If srv is nil then rpc.DefaultServer will be used. +// +// For most use cases NewServerCodec is too low-level and you should use +// ServeConn instead. You'll need NewServerCodec if you wanna register +// your own object of type named "JSONRPC2" (same as used internally to +// process batch requests) or you wanna use custom rpc server object +// instead of rpc.DefaultServer to process requests on conn. func NewServerCodec(conn io.ReadWriteCloser, srv *rpc.Server) rpc.ServerCodec { if srv == nil { srv = rpc.DefaultServer