rpc: implement full bi-directional communication (#18471)
New APIs added: client.RegisterName(namespace, service) // makes service available to server client.Notify(ctx, method, args...) // sends a notification ClientFromContext(ctx) // to get a client in handler method This is essentially a rewrite of the server-side code. JSON-RPC processing code is now the same on both server and client side. Many minor issues were fixed in the process and there is a new test suite for JSON-RPC spec compliance (and non-compliance in some cases). List of behavior changes: - Method handlers are now called with a per-request context instead of a per-connection context. The context is canceled right after the method returns. - Subscription error channels are always closed when the connection ends. There is no need to also wait on the Notifier's Closed channel to detect whether the subscription has ended. - Client now omits "params" instead of sending "params": null when there are no arguments to a call. The previous behavior was not compliant with the spec. The server still accepts "params": null. - Floating point numbers are allowed as "id". The spec doesn't allow them, but we handle request "id" as json.RawMessage and guarantee that the same number will be sent back. - Logging is improved significantly. There is now a message at DEBUG level for each RPC call served.
This commit is contained in:
76
rpc/http.go
76
rpc/http.go
@ -37,38 +37,39 @@ import (
|
||||
|
||||
const (
|
||||
maxRequestContentLength = 1024 * 512
|
||||
contentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
// https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13
|
||||
acceptedContentTypes = []string{"application/json", "application/json-rpc", "application/jsonrequest"}
|
||||
contentType = acceptedContentTypes[0]
|
||||
nullAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:0")
|
||||
)
|
||||
// https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13
|
||||
var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"}
|
||||
|
||||
type httpConn struct {
|
||||
client *http.Client
|
||||
req *http.Request
|
||||
closeOnce sync.Once
|
||||
closed chan struct{}
|
||||
closed chan interface{}
|
||||
}
|
||||
|
||||
// httpConn is treated specially by Client.
|
||||
func (hc *httpConn) LocalAddr() net.Addr { return nullAddr }
|
||||
func (hc *httpConn) RemoteAddr() net.Addr { return nullAddr }
|
||||
func (hc *httpConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (hc *httpConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
func (hc *httpConn) SetDeadline(time.Time) error { return nil }
|
||||
func (hc *httpConn) Write([]byte) (int, error) { panic("Write called") }
|
||||
|
||||
func (hc *httpConn) Read(b []byte) (int, error) {
|
||||
<-hc.closed
|
||||
return 0, io.EOF
|
||||
func (hc *httpConn) Write(context.Context, interface{}) error {
|
||||
panic("Write called on httpConn")
|
||||
}
|
||||
|
||||
func (hc *httpConn) Close() error {
|
||||
func (hc *httpConn) RemoteAddr() string {
|
||||
return hc.req.URL.String()
|
||||
}
|
||||
|
||||
func (hc *httpConn) Read() ([]*jsonrpcMessage, bool, error) {
|
||||
<-hc.closed
|
||||
return nil, false, io.EOF
|
||||
}
|
||||
|
||||
func (hc *httpConn) Close() {
|
||||
hc.closeOnce.Do(func() { close(hc.closed) })
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hc *httpConn) Closed() <-chan interface{} {
|
||||
return hc.closed
|
||||
}
|
||||
|
||||
// HTTPTimeouts represents the configuration params for the HTTP RPC server.
|
||||
@ -114,8 +115,8 @@ func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
|
||||
req.Header.Set("Accept", contentType)
|
||||
|
||||
initctx := context.Background()
|
||||
return newClient(initctx, func(context.Context) (net.Conn, error) {
|
||||
return &httpConn{client: client, req: req, closed: make(chan struct{})}, nil
|
||||
return newClient(initctx, func(context.Context) (ServerCodec, error) {
|
||||
return &httpConn{client: client, req: req, closed: make(chan interface{})}, nil
|
||||
})
|
||||
}
|
||||
|
||||
@ -184,17 +185,30 @@ func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadClos
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// httpReadWriteNopCloser wraps a io.Reader and io.Writer with a NOP Close method.
|
||||
type httpReadWriteNopCloser struct {
|
||||
// httpServerConn turns a HTTP connection into a Conn.
|
||||
type httpServerConn struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
r *http.Request
|
||||
}
|
||||
|
||||
// Close does nothing and returns always nil
|
||||
func (t *httpReadWriteNopCloser) Close() error {
|
||||
return nil
|
||||
func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
|
||||
body := io.LimitReader(r.Body, maxRequestContentLength)
|
||||
conn := &httpServerConn{Reader: body, Writer: w, r: r}
|
||||
return NewJSONCodec(conn)
|
||||
}
|
||||
|
||||
// Close does nothing and always returns nil.
|
||||
func (t *httpServerConn) Close() error { return nil }
|
||||
|
||||
// RemoteAddr returns the peer address of the underlying connection.
|
||||
func (t *httpServerConn) RemoteAddr() string {
|
||||
return t.r.RemoteAddr
|
||||
}
|
||||
|
||||
// SetWriteDeadline does nothing and always returns nil.
|
||||
func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
// NewHTTPServer creates a new HTTP RPC server around an API provider.
|
||||
//
|
||||
// Deprecated: Server implements http.Handler
|
||||
@ -226,7 +240,7 @@ func NewHTTPServer(cors []string, vhosts []string, timeouts HTTPTimeouts, srv ht
|
||||
}
|
||||
|
||||
// ServeHTTP serves JSON-RPC requests over HTTP.
|
||||
func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Permit dumb empty requests for remote health-checks (AWS)
|
||||
if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
|
||||
return
|
||||
@ -249,12 +263,10 @@ func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx = context.WithValue(ctx, "Origin", origin)
|
||||
}
|
||||
|
||||
body := io.LimitReader(r.Body, maxRequestContentLength)
|
||||
codec := NewJSONCodec(&httpReadWriteNopCloser{body, w})
|
||||
defer codec.Close()
|
||||
|
||||
w.Header().Set("content-type", contentType)
|
||||
srv.ServeSingleRequest(ctx, codec, OptionMethodInvocation)
|
||||
codec := newHTTPServerConn(r, w)
|
||||
defer codec.Close()
|
||||
s.serveSingleRequest(ctx, codec)
|
||||
}
|
||||
|
||||
// validateRequest returns a non-zero response code and error message if the
|
||||
|
Reference in New Issue
Block a user