Skip to content

Instantly share code, notes, and snippets.

@mediocregopher
Created June 15, 2016 23:40
Show Gist options
  • Save mediocregopher/545f47ab5eef1d129ef865a2814d2334 to your computer and use it in GitHub Desktop.
Save mediocregopher/545f47ab5eef1d129ef865a2814d2334 to your computer and use it in GitHub Desktop.
An RPC system from Go inspired by the http.Handler system
// 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
}
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