Created
June 15, 2016 23:40
-
-
Save mediocregopher/545f47ab5eef1d129ef865a2814d2334 to your computer and use it in GitHub Desktop.
An RPC system from Go inspired by the http.Handler system
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Package brpc, stands for better-rpc. It's better than normal RPC, because it | |
// allows for saner chaining of rpc handlers | |
package brpc | |
import ( | |
"encoding/json" | |
"net/http" | |
"golang.org/x/net/context" | |
) | |
// Call represents an RPC call currently being processed. | |
type Call interface { | |
// Context returns a context object for the rpc call. The context may | |
// already have a deadline set on it, or other key/value information, | |
// depending on the underlying transport/codec | |
Context() context.Context | |
// Method returns the name of the method being called | |
Method() string | |
// UnmarshalArgs takes in an interface pointer and unmarshals the call's | |
// arguments into it using the underlying codec | |
UnmarshalArgs(interface{}) error | |
// MarshalResponse takes in an interface pointer and writes it to the | |
// underlying response receiver (e.g. the http.ResponseWriter) | |
MarshalResponse(interface{}) error | |
} | |
// Handler describes a type which can process incoming RPC requests and return a | |
// response to them | |
type Handler interface { | |
ServeRPC(Call) interface{} | |
} | |
// HandleFunc is a wrapper around a simple ServeRPC style function to make it | |
// actually implement the interface | |
type HandleFunc func(Call) interface{} | |
// ServeRPC implements the Handler interface, it simply calls the callee | |
// HandleFunc | |
func (hf HandleFunc) ServeRPC(c Call) interface{} { | |
return hf(c) | |
} | |
// TODO make a ServeMux to multiplex Calls to different Handlers mased on their | |
// method name | |
//////////////////////////////////////////////////////////////////////////////// | |
// Everything below is specific to supporting RPC over HTTP. The Call and | |
// Handler themselves don't actually care about the transport mechanism | |
// HTTPCodec describes a type which can translate an incoming http request into | |
// an rpc request | |
type HTTPCodec interface { | |
NewCall(http.ResponseWriter, *http.Request) (Call, error) | |
} | |
// HTTPHandler takes a Codec which can translate http requests to rpc calls, and | |
// a handler for those calls, and returns an http.Handler which puts it all | |
// together. | |
// | |
// If there is an error calling NewCall on the HTTPCodec that error will be | |
// returned as a 400 | |
func HTTPHandler(c HTTPCodec, h Handler) http.Handler { | |
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
c, err := c.NewCall(w, r) | |
if err != nil { | |
http.Error(w, err.Error(), 400) | |
return | |
} | |
// TODO set extra context info on Call, like original http request and | |
// response objects | |
res := h.ServeRPC(c) | |
if err := c.MarshalResponse(res); err != nil { | |
// this probably won't ever go through, but might as well try | |
http.Error(w, err.Error(), 500) | |
return | |
} | |
}) | |
} | |
//////////////////////////////////////////////////////////////////////////////// | |
// Below is an example implementation of an HTTPCodec for testing. It's loosely | |
// based on json rpc, but barely | |
type httpJSONReq struct { | |
Method string `json:"method"` | |
Args *json.RawMessage `json:"args"` | |
} | |
type httpJSONRes struct { | |
Error error `json:"error,omitempty"` | |
Res interface{} `json:"res,omitempty"` | |
} | |
type httpJSONCall struct { | |
w http.ResponseWriter | |
r *http.Request | |
httpJSONReq | |
} | |
func (hjc *httpJSONCall) Context() context.Context { | |
// TODO use the http.Request's context when 1.7 is stable | |
return context.Background() | |
} | |
func (hjc *httpJSONCall) Method() string { | |
return hjc.httpJSONReq.Method | |
} | |
func (hjc *httpJSONCall) UnmarshalArgs(i interface{}) error { | |
return json.Unmarshal(*hjc.httpJSONReq.Args, i) | |
} | |
func (hjc *httpJSONCall) MarshalResponse(i interface{}) error { | |
var res httpJSONRes | |
if err, ok := i.(error); ok { | |
res.Error = err | |
} else { | |
res.Res = i | |
} | |
return json.NewEncoder(hjc.w).Encode(&res) | |
} | |
type httpJSONCodec struct{} | |
func (httpJSONCodec) NewCall(w http.ResponseWriter, r *http.Request) (Call, error) { | |
c := httpJSONCall{r: r, w: w} | |
if err := json.NewDecoder(r.Body).Decode(&c.httpJSONReq); err != nil { | |
return nil, err | |
} | |
return &c, nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package brpc | |
import ( | |
"bytes" | |
"net/http" | |
"net/http/httptest" | |
"strings" | |
. "testing" | |
"github.com/stretchr/testify/assert" | |
"github.com/stretchr/testify/require" | |
) | |
// echo is a simple RPC method which simply returns its arguments. It doesn't | |
// even care about the method name | |
var echo = HandleFunc(func(c Call) interface{} { | |
var in interface{} | |
if err := c.UnmarshalArgs(&in); err != nil { | |
return err | |
} | |
return in | |
}) | |
// Test that RPC works over HTTP | |
func TestHTTPRPC(t *T) { | |
httpHandler := HTTPHandler(httpJSONCodec{}, echo) | |
body := bytes.NewBufferString(`{"method":"doesntmatter","args":{"hello":"world"}} `) | |
r, err := http.NewRequest("GET", "/", body) | |
require.Nil(t, err) | |
w := httptest.NewRecorder() | |
httpHandler.ServeHTTP(w, r) | |
assert.Equal(t, `{"res":{"hello":"world"}}`, strings.TrimSpace(w.Body.String())) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment