jsonrpc2

package
v1.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 23, 2020 License: MIT Imports: 16 Imported by: 77

Documentation

Overview

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.

Using context to provide transport-level details with parameters

If you want to have access to transport-level details (or any other request context data) in RPC method then first parameter of that RPC method should provide WithContext interface. (If first parameter is a struct then you can just embed Ctx into your struct as shown in example.)

This way you can get access to client IP address or details of client HTTP request etc. in RPC method.

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 client&server does not support Pipelined Requests/Responses.

HTTP client&server does not support GET Request.

HTTP client does not support Batch Request.

Because of net/rpc limitations RPC method MUST NOT return standard error which begins with '{' and ends with '}'.

Current implementation does a lot of sanity checks to conform to protocol spec. Making most of them optional may improve performance.

Example
// nolint:errcheck
package main

import (
	"context"
	"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
}

type exampleContextKey string

var RemoteAddrContextKey exampleContextKey = "RemoteAddr"

type NameArgContext struct {
	Fname, Lname string
	jsonrpc2.Ctx
}

// Method with named params and TCP context.
func (*ExampleSvc) FullName2(t NameArgContext, res *NameRes) error {
	host, _, _ := net.SplitHostPort(t.Context().Value(RemoteAddrContextKey).(*net.TCPAddr).String())
	fmt.Printf("FullName2(): Remote IP is %s\n", host)
	*res = NameRes{t.Fname + " " + t.Lname}
	return nil
}

// Method with named params and HTTP context.
func (*ExampleSvc) FullName3(t NameArgContext, res *NameRes) error {
	host, _, _ := net.SplitHostPort(jsonrpc2.HTTPRequestFromContext(t.Context()).RemoteAddr)
	fmt.Printf("FullName3(): Remote IP is %s\n", host)
	*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", []string{"one", "two"}}
}

func main() {
	// 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
			}
			ctx := context.WithValue(context.Background(), RemoteAddrContextKey, conn.RemoteAddr())
			go jsonrpc2.ServeConnContext(ctx, 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()

	// Custom client use HTTP transport.
	clientCustomHTTP := jsonrpc2.NewCustomHTTPClient(
		"http://"+lnHTTP.Addr().String()+"/rpc",
		jsonrpc2.DoerFunc(func(req *http.Request) (*http.Response, error) {
			// Setup custom HTTP client.
			client := &http.Client{}
			// Modify request as needed.
			req.Header.Set("Content-Type", "application/json-rpc")
			return client.Do(req)
		}),
	)
	defer clientCustomHTTP.Close()

	var reply int

	// Synchronous call using positional params and TCP.
	err = clientTCP.Call("ExampleSvc.Sum", [2]int{3, 5}, &reply) // nolint:ineffassign
	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) // nolint:ineffassign
	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"})

	// Synchronous call using named params and TCP with context.
	clientTCP.Call("ExampleSvc.FullName2", NameArg{"First", "Last"}, nil)

	// Synchronous call using named params and HTTP with context.
	clientHTTP.Call("ExampleSvc.FullName3", NameArg{"First", "Last"}, nil)

	// Correct error handling.
	err = jsonrpc2.WrapError(clientTCP.Call("ExampleSvc.Err1", nil, nil))
	fmt.Printf("Err1(): %q\n", err)

	err = jsonrpc2.WrapError(clientCustomHTTP.Call("ExampleSvc.Err2", nil, nil))
	if rpcerr := new(jsonrpc2.Error); errors.As(err, &rpcerr) {
		fmt.Printf("Err2(): code=%d msg=%q data=%v\n", rpcerr.Code, rpcerr.Message, rpcerr.Data)
	} else if err != nil {
		fmt.Printf("Err2(): %q\n", err)
	}

	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
FullName2(): Remote IP is 127.0.0.1
FullName3(): Remote IP is 127.0.0.1
Err1(): "-32000 some issue"
Err2(): code=-32603 msg="bad HTTP Status: 415 Unsupported Media Type" data=<nil>
Err3(): code=42 msg="some issue" data=[one two]

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func HTTPHandler

func HTTPHandler(srv *rpc.Server) http.Handler

HTTPHandler returns handler for HTTP requests which will execute 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

func HTTPRequestFromContext

func HTTPRequestFromContext(ctx context.Context) *http.Request

HTTPRequestFromContext returns HTTP request related to this RPC (if you use HTTPHander to serve JSON RPC 2.0 over HTTP) or nil otherwise.

func NewClientCodec

func NewClientCodec(conn io.ReadWriteCloser) rpc.ClientCodec

NewClientCodec returns a new rpc.ClientCodec using JSON-RPC 2.0 on conn.

func NewServerCodec

func NewServerCodec(conn io.ReadWriteCloser, srv *rpc.Server) rpc.ServerCodec

NewServerCodec returns a new rpc.ServerCodec using JSON-RPC 2.0 on conn, 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 NewServerCodecContext

func NewServerCodecContext(ctx context.Context, conn io.ReadWriteCloser, srv *rpc.Server) rpc.ServerCodec

NewServerCodecContext is NewServerCodec with given context provided within parameters for compatible RPC methods.

func ServeConn

func ServeConn(conn io.ReadWriteCloser)

ServeConn runs the JSON-RPC 2.0 server on a single connection. ServeConn blocks, serving the connection until the client hangs up. The caller typically invokes ServeConn in a go statement.

func ServeConnContext

func ServeConnContext(ctx context.Context, conn io.ReadWriteCloser)

ServeConnContext is ServeConn with given context provided within parameters for compatible RPC methods.

func WrapError added in v1.2.0

func WrapError(err error) error

WrapError handles any error returned by Client.Call() by wrapping Error returned by ServerError or returning non-ServerError errors as is. Wrapped Error is stringified to "<code> <message>" instead of JSON.

Types

type BatchArg

type BatchArg struct {
	Ctx
	// contains filtered or unexported fields
}

BatchArg is a param for internal RPC JSONRPC2.Batch.

type Client

type Client struct {
	*rpc.Client
	// contains filtered or unexported fields
}

Client represents a JSON RPC 2.0 Client. There may be multiple outstanding Calls associated with a single Client, and a Client may be used by multiple goroutines simultaneously.

It also provides all methods of net/rpc Client.

func Dial

func Dial(network, address string) (*Client, error)

Dial connects to a JSON-RPC 2.0 server at the specified network address.

func NewClient

func NewClient(conn io.ReadWriteCloser) *Client

NewClient returns a new Client to handle requests to the set of services at the other end of the connection.

func NewClientWithCodec

func NewClientWithCodec(codec rpc.ClientCodec) *Client

NewClientWithCodec returns a new Client using the given rpc.ClientCodec.

func NewCustomHTTPClient

func NewCustomHTTPClient(url string, doer Doer) *Client

NewCustomHTTPClient returns a new Client to handle requests to the set of services at the given url using provided doer (&http.Client{} by default).

Use doer to customize HTTP authorization/headers/etc. sent with each request (it method Do() will receive already configured POST request with url, all required headers and body set according to specification).

func NewHTTPClient

func NewHTTPClient(url string) *Client

NewHTTPClient returns a new Client to handle requests to the set of services at the given url.

func (Client) Notify

func (c Client) Notify(serviceMethod string, args interface{}) error

Notify try to invoke the named function. It return error only in case it wasn't able to send request.

type Ctx

type Ctx struct {
	// contains filtered or unexported fields
}

Ctx can be embedded into your struct with RPC method parameters (if that method parameters type is a struct) to make it implement WithContext interface and thus have access to request context.

func (*Ctx) Context

func (c *Ctx) Context() context.Context

Context returns ctx given to preceding SetContext call or context.Background() otherwise.

func (*Ctx) SetContext

func (c *Ctx) SetContext(ctx context.Context)

SetContext saves ctx for succeeding Context calls.

type Doer

type Doer interface {
	Do(req *http.Request) (resp *http.Response, err error)
}

Doer is an interface for doing HTTP requests.

type DoerFunc

type DoerFunc func(req *http.Request) (resp *http.Response, err error)

The DoerFunc type is an adapter to allow the use of ordinary functions as HTTP clients. If f is a function with the appropriate signature, DoerFunc(f) is a client that calls f.

func (DoerFunc) Do

func (f DoerFunc) Do(req *http.Request) (resp *http.Response, err error)

Do calls f(req).

type Error

type Error struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

Error represent JSON-RPC 2.0 "Error object".

func NewError

func NewError(code int, message string) *Error

NewError returns an Error with given code and message.

func ServerError

func ServerError(rpcerr error) *Error

ServerError convert errors returned by Client.Call() into Error. User should check for rpc.ErrShutdown and io.ErrUnexpectedEOF before calling ServerError.

func (*Error) Error

func (e *Error) Error() string

Error returns JSON representation of Error.

type JSONRPC2

type JSONRPC2 struct{}

JSONRPC2 is an internal RPC service used to process batch requests.

func (JSONRPC2) Batch

func (JSONRPC2) Batch(arg BatchArg, replies *[]*json.RawMessage) (err error)

Batch is an internal RPC method used to process batch requests.

type WithContext

type WithContext interface {
	Context() context.Context
	SetContext(ctx context.Context)
}

WithContext is an interface which should be implemented by RPC method parameters type if you need access to request context in RPC method.

Request context will be same as was provided to corresponding ServeConnContext/NewServerCodecContext or context.Background otherwise.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL