groWs

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 5, 2024 License: MIT Imports: 13 Imported by: 0

README

⇢ groWs ⇠

Framework for building Structured and scalable Websocket Servers

Support Support Support Support Support Support Support Support

Table of Contents

Introduction

groWs is a framework for building scalable Websocket Servers. It is built on top of the gobwas/ws library, that provides a performant API for working with Websocket connections.

The Idea behind groWs is to provide a simple and easy to use API for building Websocket Servers, that are Structured, Horizontally Scalable by default, and Maintainable. Additionally, users should not have to worry about the underlying implementation details, and should be able to focus on the business logic of their application.

Ths is achieved by providing:

  • Handshake, Send, and Receive middlewares supported.
  • Multi-Router support, that allows you to create multiple routers for different purposes.
  • Handlers for handling events, that are triggered when a connection is opened, closed, or when a message is received.
  • Rooms support, that allows you to group connections into rooms, and broadcast messages to them.
  • Redis Pub-Sub support, that allows you to broadcast messages to multiple servers.

Installation

go get github.com/kesimo/groWs

Quick Start

This is a minimal example of a groWs server, that listens on port 8080, and handles a single route /example. if a client connects to this route, it will be added to the testRoom room and will be able to receive messages from other clients in the same room.

  • If the client sends data like this: {"event": "testRoom", "data": "..."}, a message gets broadcast to all clients in the testRoom room.
  • If the client sends the raw messagetest, the server will respond with test back.
package main

import (
	"github.com/kesimo/grows"
	"log"
	"net/http"
)

func main() {
	config := groWs.Config{Host: "localhost", Port: 8080}
	app := groWs.NewApp(config)
	
	// Create Handler
	handler := groWs.NewClientHandler()
	handler.OnConnect(func(client *groWs.Client) error {
		groWs.AddClientToRoom(client, "testRoom")
		return nil
	})
	handler.On("test", func(client *groWs.Client, data []byte) error {
		return client.Write([]byte("test back"))
	})
	handler.OnEvent("Broadcast", func(client *groWs.Client, data any) error {
		return groWs.BroadcastEvent("testRoom", groWs.Event{
			Identifier: "broadcast-event",
			Data:       "...",
		})
	})

	// Create a new router
	router := groWs.NewRouter()
	// Add the handler to the router
	router.AddRoute("/example", handler)
	app.AddRouter(router)
	// Add a handshake middleware (for more use app.AddSendMiddleware and app.AddReceiveMiddleware)
	app.AddHandshakeMiddleware("/example", func(r *http.Request, client *groWs.Client) bool {
		log.Println("New connection")
		return true
	})
	// Start the server
	log.Fatalln(app.ListenAndServe())
}

Usage

Server configuration

To configure the server, you can use the groWs.Config struct and pass it to the groWs.NewApp function.

config := groWs.Config{Host: "localhost", Port: 4321}
app := groWs.NewApp(config)

The groWs.Config struct has the following fields:

Field Type Description Default
Host string The host to listen on. localhost
Port int The port to listen on. 8080
UseTLS bool Whether to use TLS or not. false
Cert string The path to the certificate file. (if UseTLS is true) ""
Key string The path to the key file. (if UseTLS is true) ""
EnablePubSub bool Whether to enable Redis Pub-Sub or not. false
RedisHost string The host of the Redis server. localhost
RedisPort int The port of the Redis server. 6379

Creating a Router

To create a router, you can use the groWs.NewRouter function.

router := groWs.NewRouter()

add routes to the router using the AddRoute function. The first argument is the route equal to the pattern used in the "net/http" package. The second argument is the handler that will be called when a client connects to the route.

router.AddRoute("/example", handler)
router.AddRoute("/user/:id", handler2)

Adding Middlewares

You can add middlewares to the router using the AddHandshakeMiddleware, AddSendMiddleware, and AddReceiveMiddleware functions.

NOTE: At the moment, multiple middlewares of the same type are not sorted by the order they were added, if different patterns matches the same route, so the order of execution is not guaranteed.

app.AddHandshakeMiddleware("/example", func(r *http.Request, client *groWs.Client) bool {
    log.Println("New connection")
    return true
})
app.AddSendMiddleware("/example", func(client *groWs.Client, data []byte) ([]byte, error) {
    ...
    return data, nil
})
app.AddReceiveMiddleware("/example", func(client *groWs.Client, data []byte) ([]byte, error) {
    ...
    return data, nil
})

List of middleware types:

Type Description alias for
HandshakeMiddleware Handle new Client Connection (Auth,RBAC,...) func(r *http.Request, client *Client) bool
SendMiddleware Handle outgoing messages (Compression,...) func(*Client, []byte) ([]byte, error)
ReceiveMiddleware Handle incoming messages (Decompression,...) func(*Client, []byte) ([]byte, error)
  • The SendMiddleware and ReceiveMiddleware functions should return the modified message and an error if any.

  • The HandshakeMiddleware function should return a boolean value indicating whether the connection should be accepted or not.

  • Only the first path-matching HandshakeMiddleware will be executed. Meaning that if you have multiple middlewares for the same route, only the first defined will be executed.

Handlers

To create a handler, you can use the groWs.NewClientHandler function.

handler := groWs.NewClientHandler()
Handle incoming messages

You can add multiple incoming raw messages handlers using the On function. The raw message received from the client will be passed to the handler function.

NOTE: The first argument is a Regex, so you can use regex to match the message.

usage:

handler.On("test", func(client *groWs.Client, data []byte) error {
	log.Println("test message received")
})
Handle incoming events

You can add multiple incoming events handlers using the OnEvent function. For more information about events, see the Events section.

NOTE: The data argument is an already unmarshalled JSON object, so you can use it directly.

usage:

handler.OnEvent("test", func(client *groWs.Client, data any) error {
    log.Println("test event received")
})
Handle new connections

You can add a handler for new connections using the OnConnect function.

  • The OnConnect function is called once after the handshake is done.
  • If the OnConnect function returns an error, the connection will be closed.

usage:

handler.OnConnect(func(client *groWs.Client) error {
    log.Println("New connection")
    return nil
})
Handle disconnections

You can add a handler for disconnections using the OnDisconnect function.

usage:

handler.OnDisconnect(func(client *groWs.Client) error {
    log.Println("Client disconnected")
    return nil
})

Documentation

Client

A groWs.Client represents a client connection and can be used to send messages to the client or store client data. The Client holds an internal used unique ID, that can be accessed using client.GetID().

Send a message

To send a message to the client, you can use the Write, WriteJSON, or WriteEvent functions.

// Write a raw message
err := client.Write([]byte("test"))

// Write a JSON message
err := client.WriteJSON(map[string]interface{}{"test": "test"})

// Write an event
err := client.WriteEvent(groWs.Event{
    Identifier: "anyevent",
    Data:       "anydata",
})
Store Metadata

You can store and access metadata in the client using the SetMeta and GetMeta functions.

  • Set the same key multiple times to update the value.
  • The metadata is stored in a map, so the order of the keys is not guaranteed.
  • The metadata is stored in the client, so it will be lost if the client disconnects.
  • The metadata is available to all middlewares and handlers.

Usage:

// set new metadata
client.SetMeta("key", "value")

// get metadata (returns an `groWs.ErrMetaNotFound` error if the key is not found)
value, err := client.GetMeta("key")
Get current joined rooms

You can get the list of rooms the client is currently joined in using the GetRooms function.

rooms := client.GetRooms()
Close the connection

You can close the connection using the Close function.

err := client.Close()

Events

An groWs.Event represents an event sent by the client. It can be used as a wrapper for the data sent by the client. If you want to send an event to the client, you can use the client.WriteEvent(event Event) function.

RECOMMENDED: Use events to send data from and to the client, it makes it easier to handle multiple actions on both sides.

The structure of an event is as follows:

type Event struct {
// Event identifier used to identify the event on the client and server side
// The ClientHandler.OnEvent() method uses this identifier to match the event
Identifier string `json:"event"`
// Data is the data that is sent with the event and can be of any type
// On send and receive the data is converted from JSON to any type
Data       any    `json:"data"`
}

(Coming soon: Client side library to send and receive events)

Utils

The groWs package contains functions to Send Messages to a Client and to Broadcast Messages to a list of Clients.

NOTE: All listed functions are working out of the box with the Redis Pub/Sub implementation, if configured.

Broadcasting Messages
Function Description
groWs.Broadcast(roomId string, message []byte) Broadcast a raw message to all clients in a room
groWs.BroadcastToAll(message []byte) Broadcast a raw message to all clients
groWs.BroadcastEvent(roomId string, event Event) Broadcast a event to all clients in a room
groWs.BroadcastEventToAll(event Event) Broadcast a event to all clients
groWs.BroadcastExcept(id string, message []byte) Broadcast a raw message to all clients except the client with the given id (client.GetID())
groWs.BroadcastEventExcept(id string, event Event) Broadcast a event to all clients except the client with the given id (client.GetID())
groWs.BroadcastByMeta(key string, value interface{}, message []byte) Broadcast a raw message to clients with the given metadata key and value
groWs.BroadcastEventByMeta(key string, value interface{}, event Event) Broadcast a event to clients with the given metadata key and value
--- Send to single Client by internal ID --- ----------------------------------------
groWs.BroadcastToClient(id string, message []byte) Broadcast a raw message to a client with the given id (client.GetID())
groWs.BroadcastEventToClient(id string, event Event) Broadcast a event to a client with the given id (client.GetID())
Get Informations about Clients
Function Description
groWs.GetConnectedClientIds() Get a list of all connected client ids
groWs.GetConnectedClientIdsByMeta(key string, value interface{}) Get a list of all connected client ids with the given metadata key and value
groWs.GetConnectedClientIdsByRoom(roomId string) Get a list of all connected client ids in the given room
groWs.GetClient(id string) Access a client by its internal id (NOTE: This function is not available with the Redis Pub/Sub implementation)
Handling Rooms
Function Description
groWs.AddClientToRoom(client *Client, roomId string) Add a client to a room
groWs.RemoveClientFromRoom(client *Client, roomId string) Remove a client from a room
groWs.RemoveClientFromAllRooms(client *Client) Remove a client from all joined rooms
groWs.GetClientRooms(client *Client) Get a list of all rooms the client is currently joined in

Examples

A full example can be found in the /examples directory.

The example gives a suggestion on how to use the library and structure your code.

Contributing

Contributions are welcome, feel free to open an issue or a pull request.

License

This project is licensed under the MIT License - see the LICENSE file for details

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrMetaNotFound = errors.New("metadata not found")
View Source
var (
	ErrPubSubIsNil = errors.New("pub/sub client is nil")
)

Functions

func AddClientToRoom

func AddClientToRoom(client *Client, roomId string)

AddClientToRoom adds a client to a room

func Broadcast

func Broadcast(roomId string, message []byte)

Broadcast sends a Message to all clients in a room

func BroadcastByMeta

func BroadcastByMeta(key string, value interface{}, message []byte)

BroadcastByMeta sends a Message to all clients with a specific metadata TODO: implement pub/sub

func BroadcastEvent

func BroadcastEvent(roomId string, event Event) error

BroadcastEvent sends an event to all clients in a room

func BroadcastEventByMeta

func BroadcastEventByMeta(key string, value interface{}, event Event)

BroadcastEventByMeta sends an event to all clients with a specific metadata TODO: implement pub/sub

func BroadcastEventExcept

func BroadcastEventExcept(id string, event Event) error

BroadcastEventExcept sends an event to all clients except the client with the given Id

func BroadcastEventToAll

func BroadcastEventToAll(event Event) error

BroadcastEventToAll sends an event to all clients

func BroadcastEventToClient

func BroadcastEventToClient(id string, event Event) error

BroadcastEventToClient sends an event to a client with the given Id

func BroadcastExcept

func BroadcastExcept(id string, message []byte) error

BroadcastExcept sends a Message to all clients except the client with the given id

func BroadcastToAll

func BroadcastToAll(message []byte)

BroadcastToAll sends a Message to all clients connected

func BroadcastToClient

func BroadcastToClient(id string, message []byte) error

BroadcastToClient sends a Message to a client with the given Id

func GetClientRooms

func GetClientRooms(client *Client) []string

GetClientRooms returns a list of all rooms the client is in

func GetConnectedClientIds

func GetConnectedClientIds() []string

GetConnectedClientIds returns a list of all connected client ids

func GetConnectedClientIdsByMeta

func GetConnectedClientIdsByMeta(key string, value interface{}) []string

GetConnectedClientIdsByMeta returns a list of all connected client ids with a specific metadata

func GetConnectedClientIdsByRoom

func GetConnectedClientIdsByRoom(roomId string) []string

func IsEvent

func IsEvent(data []byte) bool

IsEvent checks if the data is an Event by trying to unmarshal a JSON to an Event

func IsJSONObject

func IsJSONObject(data []byte) bool

IsJSONObject checks if the data is JSON This uses the first and last character of the data to check if it is JSON and is not a 100% accurate way to check if the data is JSON (e.g. for an array) but is faster

func RemoveClientFromAllRooms

func RemoveClientFromAllRooms(client *Client)

RemoveClientFromAllRooms removes a client from all rooms

func RemoveClientFromRoom

func RemoveClientFromRoom(client *Client, roomId string)

RemoveClientFromRoom removes a client from a room

Types

type App

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

func NewApp

func NewApp(config Config) *App

func (*App) AddHandshakeMiddleware

func (a *App) AddHandshakeMiddleware(route string, middleware HandshakeMiddleware)

AddHandshakeMiddleware adds a middleware that is called before the websocket handshake only one per route allowed if multiple regex match the same route the first one will be used

func (*App) AddReceiveMiddleware

func (a *App) AddReceiveMiddleware(route string, middleware ReceiveMiddleware)

AddReceiveMiddleware adds a middleware to the route regex (e.g. "/test" or "/test/:Id") multiple middlewares can be added to the same route (Caution: not ordered by adding order if multiple regex match the same route) Example: - "/test" will match "/test" - * will match everything

func (*App) AddRouter

func (a *App) AddRouter(router *Router)

func (*App) AddSendMiddleware

func (a *App) AddSendMiddleware(route string, middleware SendMiddleware)

AddSendMiddleware adds a middleware to the route regex (e.g. "/test" or ".*")

func (*App) ListenAndServe

func (a *App) ListenAndServe() error

ListenAndServe starts the server and listens for incoming connections It will use TLS if the config.UseTLS is set to true and a cert and key are provided It will panic if no router is added (or for TLS no cert or key is provided)

type Client

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

func GetClient

func GetClient(id string) *Client

GetClient returns a client with the given ID

func NewClient

func NewClient(conn net.Conn, middlewares []SendMiddleware) *Client

func (*Client) Close

func (c *Client) Close() error

Close closes the connection of the client

func (*Client) GetID

func (c *Client) GetID() string

GetID returns the ID of the client

func (*Client) GetMeta

func (c *Client) GetMeta(key string) (interface{}, error)

GetMeta returns the metadata of the client by key

func (*Client) GetRooms

func (c *Client) GetRooms() []string

GetRooms returns all rooms the client is in

func (*Client) SetMeta

func (c *Client) SetMeta(key string, value interface{})

SetMeta adds metadata to a key value map Can be used to store data about the client (e.g. username, password, etc.) To get the metadata, use the GetMeta function

func (*Client) Write

func (c *Client) Write(data []byte) error

Write writes data to the client

func (*Client) WriteEvent

func (c *Client) WriteEvent(event Event) error

WriteEvent writes an event to the client as JSON

func (*Client) WriteJSON

func (c *Client) WriteJSON(data interface{}) error

WriteJSON writes JSON data to the client

type ClientHandler

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

func NewClientHandler

func NewClientHandler() ClientHandler

func (*ClientHandler) On

func (ch *ClientHandler) On(event string, f func(client *Client, data []byte) error)

On sets the on function

func (*ClientHandler) OnConnect

func (ch *ClientHandler) OnConnect(f func(client *Client) error)

OnConnect sets the onConnect function

func (*ClientHandler) OnDisconnect

func (ch *ClientHandler) OnDisconnect(f func(client *Client) error)

OnDisconnect sets the onDisconnect function

func (*ClientHandler) OnEvent

func (ch *ClientHandler) OnEvent(event string, f func(client *Client, data interface{}) error)

OnEvent sets the onEvent function

type ClientPool

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

func GetClientPool

func GetClientPool() *ClientPool

func (*ClientPool) AddClient

func (cp *ClientPool) AddClient(c *Client)

AddClient adds a client to the pool

func (*ClientPool) AddClientToRoom

func (cp *ClientPool) AddClientToRoom(c *Client, roomId string)

AddClientToRoom adds a client to a Id

func (*ClientPool) GetClient

func (cp *ClientPool) GetClient(id string) *Client

GetClient returns a client by Id

func (*ClientPool) GetClients

func (cp *ClientPool) GetClients() map[string]*Client

GetClients returns all clients

func (*ClientPool) GetClientsByMeta

func (cp *ClientPool) GetClientsByMeta(key string, value interface{}) []*Client

GetClientsByMeta returns all clients with a specific metadata

func (*ClientPool) GetRoom

func (cp *ClientPool) GetRoom(roomId string) *Room

GetRoom returns a room by identifier

func (*ClientPool) GetRooms

func (cp *ClientPool) GetRooms() map[string]*Room

GetRooms returns all rooms

func (*ClientPool) RemoveClient

func (cp *ClientPool) RemoveClient(c *Client)

RemoveClient removes a client from the pool

func (*ClientPool) RemoveClientFromAllRooms

func (cp *ClientPool) RemoveClientFromAllRooms(c *Client, rooms []string)

RemoveClientFromAllRooms removes a client from all rooms

func (*ClientPool) RemoveClientFromRoom

func (cp *ClientPool) RemoveClientFromRoom(c *Client, roomId string)

RemoveClientFromRoom removes a client from a Id

func (*ClientPool) SendToAll

func (cp *ClientPool) SendToAll(message []byte)

SendToAll sends a Message to all clients

func (*ClientPool) SendToAllByMeta

func (cp *ClientPool) SendToAllByMeta(key string, value interface{}, message []byte)

SendToAllByMeta sends a Message to all clients with a specific metadata

func (*ClientPool) SendToAllExcept

func (cp *ClientPool) SendToAllExcept(id string, message []byte)

SendToAllExcept sends a Message to all clients except the client with the given identifier

func (*ClientPool) SendToClient

func (cp *ClientPool) SendToClient(id string, message []byte) error

SendToClient sends a Message to a client with the given Id

func (*ClientPool) SendToRoom

func (cp *ClientPool) SendToRoom(roomId string, message []byte)

SendToRoom sends a Message to all clients in a Id

type Config

type Config struct {
	// Server
	Host string `json:"host"`
	Port int    `json:"port"`
	// tls
	UseTLS bool   `json:"use_tls"`
	Cert   string `json:"cert"`
	Key    string `json:"key"`
	// PubSub
	EnablePubSub bool   `json:"enable_pub_sub"`
	RedisHost    string `json:"pub_sub_host"`
	RedisPort    int    `json:"pub_sub_port"`
}

type Event

type Event struct {
	// Event identifier used to identify the event on the client and server side
	// The ClientHandler.OnEvent() method uses this identifier to match the event
	Identifier string `json:"event"`
	// Data is the data that is sent with the event and can be of any type
	// On send and receive the data is converted from JSON to any type
	Data any `json:"data"`
}

func FromJSON

func FromJSON(data []byte) (Event, error)

FromJSON converts JSON data to an Event

func (Event) ToJSON

func (e Event) ToJSON() ([]byte, error)

ToJSON converts an Event to JSON data

type HandlerFunc

type HandlerFunc func(http.ResponseWriter, *http.Request)

type HandshakeMiddleware

type HandshakeMiddleware = func(r *http.Request, client *Client) bool

type Payload

type Payload struct {
	Id      string `json:"Id"`
	Message []byte `json:"Message"`
	Event   Event  `json:"event"`
}

type ReceiveMiddleware

type ReceiveMiddleware func(*Client, []byte) ([]byte, error)

type Room

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

type Route

type Route struct {
	// Name of the route
	Path string
	// Handler function
	Handler ClientHandler
}

type Router

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

func NewRouter

func NewRouter() *Router

func (*Router) AddRoute

func (r *Router) AddRoute(path string, handler ClientHandler)

AddRoute adds a route to the router

func (*Router) GetRoutes

func (r *Router) GetRoutes() []*Route

GetRoutes returns all routes

type SendMiddleware

type SendMiddleware func(*Client, []byte) ([]byte, error)

type Server

type Server struct {
	Server *http.Server
	// contains filtered or unexported fields
}

func NewServer

func NewServer(addr string) *Server

func (*Server) AddHandleFunc

func (s *Server) AddHandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

func (*Server) ListenAndServeTLS

func (s *Server) ListenAndServeTLS(certFile, keyFile string) error

Jump to

Keyboard shortcuts

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