synapse

package module
v0.0.0-...-6ebcb98 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2015 License: MIT Imports: 16 Imported by: 1

README

Synapse

What is this?

Synapse is:

  • An RPC wire protocol based on MessagePack
  • A library implementation of that protocol in go

This library was designed to work with tinylib/msgp for serialization.

Why not MessagePack RPC?

When we first started writing code for this library, it was supposed to be a MessagePack RPC implementation. However, we found that the nature of the protocol makes it difficult to recover from malformed or unexpected messages on the wire. The primary difference between the Synapse and MessagePack RPC protocols is that synapse uses length-prefixed frames to indicate the complete size of the message before it is read off of the wire. This has some benefits:

  • Serialization is done inside handlers, not in an I/O loop. The request/response body is copied into a reserved chunk of memory and then serialization is done in a separate goroutine. Thus, serialization can fail without interrupting other messages.
  • An unexpected or unwanted message can be efficiently skipped, because we do not have to traverse it to know its size. Being able to quickly discard unwanted or unexpected messages improves security against attacks designed to exhaust server resources.
  • Message size has a hard limit at 65535 bytes, which makes it significantly more difficult to exhaust server resources (either because of traffic anomalies or a deliberate attack.)

Additionally, synapse includes a message type (called "command") that allows client and server implementations to probe one another programatically, which will allows us to add features (like service discovery) as the protocol evolves.

Goals

Synapse is designed to make it easy to write network applications that have request-response semantics, much like HTTP and (some) RPC protocols. Like net/rpc, synapse can operate over most network protocols (or any net.Conn), and, like net/http, provides a standardized way to write middlewares and routers around services. As an added bonus, synapse has a much smaller per-request and per-connection memory footprint than net/rpc or net/http.

This repository contains only the "core" of the Synapse project. Over time, we will release middlewares in other repositories, but our intention is to keep the core as small as possible.

Non-goals

Synapse is not designed for large messages (there is a hard limit at 65kB), and it does not provide strong ordering guarantees. At the protocol level, there is no notion of CRUD operations or any other sort of stateful semantics; those are features that developers should provide at the application level. The same goes for auth. All of these features can effectively be implemented as wrappers of the core library.

Hello World

As a motivating example, let's consider a "hello world" program. (You can find the complete files in _examples/hello_world/.)

Here's what the client code looks like:

func main() {
	// This sets up a TCP connection to
	// localhost:7000 and attaches a client
	// to it. Client creation fails if it
	// can't ping the server on the other
	// end. Additionally, calls will fail
	// if a response isn't received in one second.
	client, err := synapse.Dial("tcp", "localhost:7000", time.Second)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	// Here we make a remote call to
	// the method called "hello," and
	// we pass an object for the
	// response to be decoded into.
	// synapse.String is a convenience
	// provided for sending strings
	// back and forth.
	var res synapse.String
	err = client.Call("hello", nil, &res)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}

	fmt.Println("response from server:", string(res))
}

And here's the server code:

func main() {
	// Like net/http, synapse uses
	// routers to send requests to
	// the appropriate handler(s).
	// Here we'll use the one provided
	// by the synapse package, although
	// users can write their own.
	router := synapse.NewRouter()

	// Here we're registering the "hello"
	// route with a function that logs the
	// remote address of the caller and then
	// responds with "Hello, World!"
	router.HandleFunc("hello", func(req synapse.Request, res synapse.ResponseWriter) {
		log.Println("received request from client at", req.RemoteAddr())
		res.Send(synapse.String("Hello, World!"))
	})

	// ListenAndServe blocks forever
	// serving the provided handler.
	log.Fatalln(synapse.ListenAndServe("tcp", "localhost:7000", router))
}

Project Status

Very alpha. Expect frequent breaking changes to the API. We're actively looking for community feedback.

Performance, etc.

Synapse is optimized for throughput over latency; in general, synapse is designed to perform well in adverse (high-load/high-concurrency) conditions over simple serial conditions. There are a number of implementation details that influence performance:

  • De-coupling of message body serialization/de-serialization from the main read/write loops reduces the amount of time spend in critical (blocking) sections, meaning that time spent blocking is both lower and more consistent. Additionally, malformed handlers cannot corrupt the state of the network I/O loop. However, this hurts performance in the simple (serial) case, because lots of time is spent copying memory rather than making forward progress.
  • Opportunistic coalescing of network writes reduces system call overhead, without dramatically affecting latency.
  • The objects used to maintain per-request state are arena allocated during initialization. Practically speaking, this means that synapse does 0 allocations per request on the client side, and 1 allocation on the server side (for request.Name()).
  • tinylib/msgp serialization is fast, compact, and versatile.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrClosed is returns when a call is attempted
	// on a closed client.
	ErrClosed = errors.New("synapse: client is closed")

	// ErrTimeout is returned when a server
	// doesn't respond to a request before
	// the client's timeout scavenger can
	// free the waiting goroutine
	ErrTimeout = errors.New("synapse: the server didn't respond in time")

	// ErrTooLarge is returned when the message
	// size is larger than 65,535 bytes.
	ErrTooLarge = errors.New("synapse: message body too large")
)

Functions

func JSPipe

func JSPipe(w io.Writer) msgp.Unmarshaler

JSPipe is a decoder that can be used to translate a messagepack object directly into JSON as it is being decoded.

For example, you can trivially print the response to a request as JSON to stdout by writing something like the following:

client.Call("name", in, synapse.JSPipe(os.Stdout))

func ListenAndServe

func ListenAndServe(network string, laddr string, h Handler) error

ListenAndServe opens up a network listener on the provided network and local address and begins serving with the provided handler. ListenAndServe blocks until there is a fatal listener error.

func ListenAndServeTLS

func ListenAndServeTLS(network, laddr string, certFile, keyFile string, h Handler) error

ListenAndServeTLS acts identically to ListenAndServe, except that it expects connections over TLS1.2 (see crypto/tls). Additionally, files containing a certificate and matching private key for the Server must be provided. If the certificate is signed by a certificate authority, the certFile should be the concatenation of the server's certificate followed by the CA's certificate.

func RegisterName

func RegisterName(m Method, name string)

func Serve

func Serve(l net.Listener, h Handler) error

Serve starts a Server on 'l' that serves the supplied handler. It blocks until the listener closes.

func ServeConn

func ServeConn(c net.Conn, h Handler)

ServeConn serves an individual network connection. It blocks until the connection is closed or it encounters a fatal error.

Types

type Client

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

Client is a client to a single synapse server.

func Dial

func Dial(network string, raddr string, timeout time.Duration) (*Client, error)

Dial creates a new client by dialing the provided network and remote address. The provided timeout is used as the timeout for requests, in milliseconds.

func DialTLS

func DialTLS(network, raddr string, timeout time.Duration, config *tls.Config) (*Client, error)

DialTLS acts identically to Dial, except that it dials the connection over TLS using the provided *tls.Config.

func NewClient

func NewClient(c net.Conn, timeout time.Duration) (*Client, error)

NewClient creates a new client from an existing net.Conn. Timeout is the maximum time, in milliseconds, to wait for server responses before sending an error to the caller. NewClient fails with an error if it cannot ping the server over the connection.

func (*Client) Call

func (c *Client) Call(method Method, in msgp.Marshaler, out msgp.Unmarshaler) error

Call sends a request to the server with 'in' as the body, and then decodes the response into 'out'. Call is safe to call from multiple goroutines simultaneously.

func (*Client) Close

func (c *Client) Close() error

Close idempotently closes the client's connection to the server. Close returns once every waiting goroutine has either received a response or timed out; goroutines with requests in progress are not interrupted.

type Handler

type Handler interface {

	// ServeCall handles a synapse request and
	// writes a response. It should be safe to
	// call ServeCall from multiple goroutines.
	// Both the request and response interfaces
	// become invalid after the call is returned;
	// no implementation of ServeCall should
	// maintain a reference to either object
	// after the function returns.
	ServeCall(req Request, res ResponseWriter)
}

Handler is the interface used to register server response callbacks.

func Debug

func Debug(h Handler, l *log.Logger) Handler

Debug wraps a handler and logs all of the incoming and outgoing data using the provided logger.

type Method

type Method uint32

func (Method) String

func (m Method) String() string

type Request

type Request interface {
	// Method returns the
	// request method.
	Method() Method

	// RemoteAddr returns the remote
	// address that made the request.
	RemoteAddr() net.Addr

	// Decode reads the data of the request
	// into the argument.
	Decode(msgp.Unmarshaler) error

	// IsNil returns whether or not
	// the body of the request is 'nil'.
	IsNil() bool
}

Request is the interface that Handlers use to interact with requests.

type ResponseError

type ResponseError struct {
	Code Status
	Expl string
}

ResponseError is the type of error returned by the client when the server sends a response with ResponseWriter.Error()

func (*ResponseError) Error

func (e *ResponseError) Error() string

Error implements error

type ResponseWriter

type ResponseWriter interface {
	// Error sets an error status
	// to be returned to the caller,
	// along with an explanation
	// of the error.
	Error(Status, string)

	// Send sets the body to be
	// returned to the caller.
	Send(msgp.Marshaler) error
}

A ResponseWriter is the interface through which Handlers write responses. Handlers can either return a body (through Send) or an error (through Error); whichever call is made first should 'win'. Subsequent calls to either method should no-op.

type RouteTable

type RouteTable []Handler

RouteTable is the simplest and fastest form of routing. Request methods map directly to an index in the routing table.

Route tables should be used by defining your methods in a const-iota block, and then using those as indices in the routing table.

func (*RouteTable) ServeCall

func (r *RouteTable) ServeCall(req Request, res ResponseWriter)

type Status

type Status int

Status represents a response status code

const (
	StatusInvalid     Status = iota // zero-value for Status
	StatusOK                        // OK
	StatusNotFound                  // no handler for the request method
	StatusCondition                 // precondition failure
	StatusBadRequest                // mal-formed request
	StatusNotAuthed                 // not authorized
	StatusServerError               // server-side error
	StatusOther                     // other error
)

These are the status codes that can be passed to a ResponseWriter as an error to send to the client.

func (Status) String

func (s Status) String() string

String returns the string representation of the status

type String

type String string

String is a convenience type for reading and writing go strings to the wire.

It can be used like:

router.HandleFunc("ping", func(_ synapse.Request, res synapse.Response) {
    res.Send(synapse.String("pong"))
})

func (String) MarshalMsg

func (s String) MarshalMsg(b []byte) ([]byte, error)

MarshalMsg implements msgp.Marshaler

func (*String) UnmarshalMsg

func (s *String) UnmarshalMsg(b []byte) ([]byte, error)

UnmarshalMsg implements msgp.Unmarshaler

Directories

Path Synopsis
_examples

Jump to

Keyboard shortcuts

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