Skip to content

Commit

Permalink
improve doc, add example; close #2
Browse files Browse the repository at this point in the history
  • Loading branch information
powerman committed Feb 11, 2016
1 parent d3b4477 commit dd1bd07
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 8 deletions.
8 changes: 3 additions & 5 deletions jsonrpc2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)}
}
Expand Down
61 changes: 61 additions & 0 deletions jsonrpc2/doc.go
Original file line number Diff line number Diff line change
@@ -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
143 changes: 143 additions & 0 deletions jsonrpc2/example_test.go
Original file line number Diff line number Diff line change
@@ -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=<nil>
// Err3(): code=42 msg="some issue" data=map[one:1 two:2]
}
6 changes: 3 additions & 3 deletions jsonrpc2/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions jsonrpc2/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit dd1bd07

Please sign in to comment.