249 lines
8.6 KiB
Go

/*
Package jrpc2 implements a server and a client for the JSON-RPC 2.0 protocol
defined by http://www.jsonrpc.org/specification.
Servers
The *Server type implements a JSON-RPC server. A server communicates with a
client over a channel.Channel, and dispatches client requests to user-defined
method handlers. Handlers satisfy the jrpc2.Handler interface by exporting a
Handle method with this signature:
Handle(ctx Context.Context, req *jrpc2.Request) (interface{}, error)
The handler package helps adapt existing functions to this interface.
A server finds the handler for a request by looking up its method name in a
jrpc2.Assigner provided when the server is set up.
For example, suppose we would like to export the following Add function as a
JSON-RPC method:
// Add returns the sum of a slice of integers.
func Add(ctx context.Context, values []int) int {
sum := 0
for _, v := range values {
sum += v
}
return sum
}
To convert Add to a jrpc2.Handler, call handler.New, which uses reflection to
lift its argument into the jrpc2.Handler interface:
h := handler.New(Add) // h is a jrpc2.Handler that invokes Add
We will advertise this function under the name "Add". For static assignments
we can use a handler.Map, which finds methods by looking them up in a Go map:
assigner := handler.Map{
"Add": handler.New(Add),
}
Equipped with an Assigner we can now construct a Server:
srv := jrpc2.NewServer(assigner, nil) // nil for default options
To serve requests, we need a channel.Channel. The channel package exports
functions to adapt various input and output streams. For this example, we'll
use a channel that delimits messages by newlines, and communicates on os.Stdin
and os.Stdout:
ch := channel.Line(os.Stdin, os.Stdout)
srv.Start(ch)
Once started, the running server handles incoming requests until the channel
closes, or until it is stopped explicitly by calling srv.Stop(). To wait for
the server to finish, call:
err := srv.Wait()
This will report the error that led to the server exiting. The code for this
example is availabe from cmd/examples/adder/adder.go:
$ go run cmd/examples/adder/adder.go
Interact with the server by sending JSON-RPC requests on stdin, such as for
example:
{"jsonrpc":"2.0", "id":1, "method":"Add", "params":[1, 3, 5, 7]}
Clients
The *Client type implements a JSON-RPC client. A client communicates with a
server over a channel.Channel, and is safe for concurrent use by multiple
goroutines. It supports batched requests and may have arbitrarily many pending
requests in flight simultaneously.
To create a client we need a channel:
import "net"
conn, err := net.Dial("tcp", "localhost:8080")
...
ch := channel.RawJSON(conn, conn)
cli := jrpc2.NewClient(ch, nil) // nil for default options
To send a single RPC, use the Call method:
rsp, err := cli.Call(ctx, "Add", []int{1, 3, 5, 7})
Call blocks until the response is received. Any error returned by the server,
including cancellation or deadline exceeded, has concrete type *jrpc2.Error.
To issue a batch of requests, use the Batch method:
rsps, err := cli.Batch(ctx, []jrpc2.Spec{
{Method: "Math.Add", Params: []int{1, 2, 3}},
{Method: "Math.Mul", Params: []int{4, 5, 6}},
{Method: "Math.Max", Params: []int{-1, 5, 3, 0, 1}},
})
Batch blocks until all the responses are received. An error from the Batch
call reflects an error in sending the request: The caller must check each
response separately for errors from the server. Responses are returned in the
same order as the Spec values, save that notifications are omitted.
To decode the result from a successful response use its UnmarshalResult method:
var result int
if err := rsp.UnmarshalResult(&result); err != nil {
log.Fatalln("UnmarshalResult:", err)
}
To close a client and discard all its pending work, call cli.Close().
Notifications
The JSON-RPC protocol also supports notifications. Notifications differ from
calls in that they are one-way: The client sends them to the server, but the
server does not reply.
Use the Notify method of a jrpc2.Client to send notifications:
err := cli.Notify(ctx, "Alert", handler.Obj{
"message": "A fire is burning!",
})
A notification is complete once it has been sent.
On server, notifications are handled identically to ordinary requests, except
that the return value is discarded once the handler returns. If a handler does
not want to do anything for a notification, it can query the request:
if req.IsNotification() {
return 0, nil // ignore notifications
}
Cancellation
The *jrpc2.Client and *jrpc2.Server types support a non-standard cancellation
protocol, consisting of a notification method "rpc.cancel" taking an array of
request IDs to be cancelled. The server cancels the context of each method
handler whose ID is named.
When the context associated with a client request is cancelled, the client
sends an "rpc.cancel" notification to the server for that request's ID. The
"rpc.cancel" method is automatically handled (unless disabled) by the
*jrpc2.Server implementation from this package.
Services with Multiple Methods
The example above shows a server with one method using handler.New. To
simplify exporting multiple methods, the handler.NewService function applies
handler.New to all the relevant exported methods of a concrete value, returning
a handler.Map for those methods:
type math struct{}
func (math) Add(ctx context.Context, vals ...int) int { ... }
func (math) Mul(ctx context.Context, vals []int) int { ... }
assigner := handler.NewService(math{})
This assigner maps the name "Add" to the Add method, and the name "Mul" to the
Mul method, of the math value.
This may be further combined with the handler.ServiceMap type to allow
different services to work together:
type status struct{}
func (status) Get(context.Context) (string, error) {
return "all is well", nil
}
assigner := handler.ServiceMap{
"Math": handler.NewService(math{}),
"Status": handler.NewService(status{}),
}
This assigner dispatches "Math.Add" and "Math.Mul" to the math value's methods,
and "Status.Get" to the status value's method. A ServiceMap splits the method
name on the first period ("."), and you may nest ServiceMaps more deeply if you
require a more complex hierarchy.
Concurrency
A Server processes requests concurrently, up to the Concurrency limit given in
its ServerOptions. Two requests (either calls or notifications) are concurrent
if they arrive as part of the same batch. In addition, two calls are concurrent
if the time intervals between the arrival of the request objects and delivery
of the response objects overlap.
The server may issue concurrent requests to their handlers in any order.
Otherwise, requests are processed in order of arrival. Notifications, in
particular, can only be concurrent with other notifications in the same batch.
This ensures a client that sends a notification can be sure its notification
was fully processed before any subsequent calls are issued.
Non-Standard Extension Methods
By default, a *jrpc2.Server exports the following built-in non-standard
extension methods:
rpc.serverInfo(null) ⇒ jrpc2.ServerInfo
Returns a jrpc2.ServerInfo value giving server metrics.
rpc.cancel([]int) [notification]
Request cancellation of the specified in-flight request IDs.
The rpc.cancel method works only as a notification, and will report an error if
called as an ordinary method.
These extension methods are enabled by default, but may be disabled by setting
the DisableBuiltin server option to true when constructing the server.
Server Push
The AllowPush option in jrpc2.ServerOptions allows a server to "push" requests
back to the client. This is a non-standard extension of JSON-RPC used by some
applications such as the Language Server Protocol (LSP). If this feature is
enabled, the server's Notify and Callback methods send requests back to the
client. Otherwise, those methods will report an error:
if err := s.Notify(ctx, "methodName", params); err == jrpc2.ErrPushUnsupported {
// server push is not enabled
}
if rsp, err := s.Callback(ctx, "methodName", params); err == jrpc2.ErrPushUnsupported {
// server push is not enabled
}
A method handler may use jrpc2.PushNotify and jrpc2.PushCall functions to
access these methods from its context.
On the client side, the OnNotify and OnCallback options in jrpc2.ClientOptions
provide hooks to which any server requests are delivered, if they are set.
*/
package jrpc2
// Version is the version string for the JSON-RPC protocol understood by this
// implementation, defined at http://www.jsonrpc.org/specification.
const Version = "2.0"