wsrpc

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2023 License: MIT Imports: 10 Imported by: 0

README

wsrpc

Go Reference

A simple package to make it easier to handle RPC over websockets.

Reads/writes should be thread safe.

Usage

Websocket clients can send commands as JSON messages (see the end of readme for info about how to push messages to the clients (otherwise you could just use HTTP requests...)):

{
  "id": "<message id, optional, will appear in the response if set>",
  "command": "<command name>",
  "request": {
    /* request contents, depending on the command, see golang examples below */
  }
}

The server responds with:

// success
{
    "id": "<message id from the request>",
    "command": "<command name>",
    "ok": true,
    "response": {
        /* response content, depending on the command, see Golang examples below */
    }
}

// failure
{

    "id": "<message id from the request>",
    "command": "<command name>",
    "ok": false,
    "error": "<error message>"
}

To use the package, first, define the commands (this code is also awailable as a runnable example):

type SumRequest struct {
	A int `json:"a"`
	B int `json:"b"`
}

// doesn't have to be a struct, it can be anything that serializes to JSON
type SumResponse struct {
	Sum int `json:"sum"`
}

type SumCommand = wsrpc.Command[SumRequest, SumResponse]
// the request and response types do not have to be structs
type NegateCommand = wsrpc.Command[int, int]

// define the mapping from type names to Golang types
var wsCommands = wsrpc.CommandPalette{
	"sum":    SumCommand{},
	"negate": NegateCommand{},
}

Then, set up an HTTP handler:

// now you can use wsrpc.NewConn in a HTTP request handler
// the connection upgrade to a websocket will be handled for you
func WebsocketHandler(w http.ResponseWriter, r *http.Request) {
	conn, err := wsrpc.NewConn(w, r, wsCommands)
	if err != nil {
		wsrpc.WriteHTTPError(w, 500, "failed to upgrade connection")
		return
	}
	defer conn.Close() // make sure to clean up after yourself :)

	for {
		cmd, err := conn.Decode()
		if err != nil {
			log.Printf("error: %s", err.Error())
			return
		}

		switch cmd := cmd.(type) {
		case SumCommand:
			sum := cmd.Request.A + cmd.Request.B
			if sum == 0x09F91102 {
				// 09 F9 11 02 is the beginning of the AACS encryption
				// key[1], which is considered to be an illegal number[2]

				// 1. https://en.wikipedia.org/wiki/AACS_encryption_key_controversy
				// 2. https://en.wikipedia.org/wiki/Illegal_number

				// cmd.Err will reply with all the IDs etc set correctly, as
				// described in the JSON examples above
				cmd.Err("illegal number detected")
				continue
			}

			// cmd.Ok also fills the IDs and such automatically
			cmd.OK(SumResponse{sum})

		case NegateCommand:
			cmd.OK(-cmd.Request)

		// if a command is not defined in the wsCommands palette, wsrpc will
		// respond with an error, so we don't need a default case, but one
		// might include it anyway to ensure unhandled commands that are in
		// the palette also get a response
		default:
			cmd.Err("not implemented")
		}
	}
}

Aaaand, you're done!

func main() {
	http.HandleFunc("/ws", WebsocketHandler)
	panic(http.ListenAndServe(":8080", nil))
}

Example communication:

client: {"command": "negate", "request": 2137}
server: {"id":"","command":"negate","ok":true,"response":-2137}
client: {"command": "sum", "request": {"a": 1, "b": 2}}
server: {"id":"","command":"sum","ok":true,"response":{"sum":3}}
client: {"command": "sum", "request": {"a": 0, "b": 167317762}}
server: {"id":"","command":"sum","ok":false,"error":"illegal number detected"}

You can also push messages without a request from the client:


type HelloMessage struct {
    Hiiii string `json:"hiiii"`
}

// implement the wsrpc.Message interface
func (h HelloMessage) Type() string { return "hello" }

// ...in the ws handler function
conn.SendMessage(HelloMessage{Hiiii: ":3"})

This would send the following JSON message to the client:

{ "type": "hello", "message": { "hiiii": ":3" } }

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func WriteHTTPError

func WriteHTTPError(w http.ResponseWriter, statusCode int, message string) error

WriteHTTPError can be used to write a HTTP error response with structure that matches the one used by the websocket.

Types

type Command

type Command[RequestT any, ReplyT any] struct {
	CommandMeta
	Request RequestT `json:"request"`
	// contains filtered or unexported fields
}

Command is used in tandem with CommandPalette to define your commands

func (Command[RequestT, ReplyT]) Err

func (c Command[RequestT, ReplyT]) Err(format string, args ...interface{}) error

Err sends an error response, taking care of setting the correct ID and Command values.

func (Command[RequestT, ReplyT]) FromRaw

func (c Command[RequestT, ReplyT]) FromRaw(r RawCommand) (Errable, error)

FromRaw tries to create a command with the same type as the receiver, by decoding a RawCommand struct. This is mostly an implementation detail which makes it possible to instantiate commands with the correct types, based on a CommandPalette.

func (Command[RequestT, ReplyT]) OK

func (c Command[RequestT, ReplyT]) OK(reply ReplyT) error

OK sends a success response, taking care of setting the correct ID and Command values.

type CommandDecoder

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

CommandDecoder handles decoding the commands.

func (*CommandDecoder) Decode

func (c *CommandDecoder) Decode() (Errable, error)

Decode reads and decodes a single command from the websocket in a blocking manner. Decode can be called safely from multiple threads at once.

type CommandMeta

type CommandMeta struct {
	ID      string `json:"id"`
	Command string `json:"command"`
}

CommandMeta describes command metadata - request ID and the type name

type CommandPalette

type CommandPalette map[string]fromRawable

CommandPalette maps type names to actual Command types. Use the zero value of your command types as the values.

type Conn

type Conn struct {
	CommandDecoder
	// contains filtered or unexported fields
}

func NewConn

func NewConn(w http.ResponseWriter, r *http.Request, palette CommandPalette) (*Conn, error)

NewConn upgrades the connection to a WebSocket and returns a *Conn which can be used for further communication. The palette argument is used to register command types that the server is supposed to understand.

func (*Conn) Close

func (c *Conn) Close() error

Close closes the underlying websocket connection.

func (*Conn) Pump added in v0.2.0

func (c *Conn) Pump(ctx context.Context) Pump

Pump starts a pump goroutine that decodes the messages into a Go channel and returns an object wrapping the channel. See: the Pump interface.

The goroutine stops once the context expires/gets cancelled.

Useful when one needs to read from multiple sources using a select{} statement.

func (*Conn) SendMessage

func (c *Conn) SendMessage(m Message) error

SendMessage pushes a message (any type implementing the Message interface) to the client. SendMessage can be used from multiple threads simultaneously.

type Errable

type Errable interface {
	Err(format string, args ...interface{}) error
}

type Message

type Message interface {
	Type() string
}

Message needs to be implemented by any type that the server wants to push to the client.

type Pump added in v0.2.0

type Pump interface {
	// Ch returns the channel to which the commands get pumped. If an error is
	// encountered during decoding, the channel is closed and Pump.Err() returns
	// the error.
	Ch() <-chan Errable

	// Err returns the error encountered when decoding, if any.
	Err() error
}

Pump represents a pump which pumps commands from the websocket to a Go channel.

type RawCommand

type RawCommand = Command[json.RawMessage, any]

RawCommand's request won't be decoded, and it's response can be anything

type Response

type Response[ResponseT any] struct {
	CommandMeta
	OK       bool      `json:"ok"`
	Error    string    `json:"error,omitempty"`
	Response ResponseT `json:"response,omitempty"`
}

Response defines the structure of the RPC response.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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