Cretezy / dSock

Distributed WebSocket broker
MIT License
220 stars 23 forks source link
distributed go redis websocket

dSock

dSock is a distributed WebSocket broker (in Go, using Redis).

Clients can authenticate & connect, and you can send text/binary message as an API.

Features

Multiple clients per user & authentication

dSock can broadcast a message to all clients for a certain user (identified by user ID and optionally session ID) or a certain connection (by ID). Users can be authenticated using claims or JWTs (see below).

Distributed

dSock can be scaled up easily as it uses Redis as a central database & pub/sub, with clients connecting to worker. It's designed to run on the cloud using scalable platforms such as Kubernetes or Cloud Run.

Text & binary messaging

dSock is designed for text and binary messaging, enabling JSON (UTF-8), Protocol Buffers, or any custom protocol.

Lightweight & fast

dSock utilized Go's concurrency for great performance at scale, with easy distribution and safety. It is available as Docker images for convenience.

Disconnects

Disconnect clients from an external event (logout) from a session ID or for all user connections.

Uses

The main use case for dSock is having stateful WebSocket connections act as a stateless API.

This enables you to not worry about your connection handling and simply send messages to all (or some) of a user's clients as any other HTTP API.

Chat service

Clients connect to dSock, and your back-end can broadcast messages to a specific user's clients

More!

Clients

Use a client to interact with the dSock API easily. Your language missing? Open a ticket!

Architecture

dSock is separated into 2 main services:

This allows the worker (connections) and API (gateway) to scale independently and horizontally.

dSock uses Redis as a backend data store, to store connection locations and claims.

Terminology

Word
WebSocket Sockets "over" HTTP(S)
JWT JSON Web Token
Claim dSock authentication mention using a pre-registered claim ("token")
Redis Open-source in-memory key-value database

Flow

Setup

Installation

dSock is published as binaries and as Docker images.

Binaries

Binaries are available on the releases pages.

You can simply run the binary for your architecture/OS.

You can configure dSock using environment variables or a config (see below).

Docker images

Docker images are published on Docker Hub:

The images are small (~15MB) and expose on port 80 by default (controllable by setting the PORT environment variable).

It is recommended to use the environment variables to configure dSock instead of a config when using the images. Configs are still supported (can be mounted to /config.toml or /config.$EXT, see below).

Options

dSock can be configured using a config file or using environment variables.

Worker only

You can write your config file in TOML (recommended), JSON, YAML, or any format supported by viper

Configs are loaded from (in order):

A default config will be created at $PWD/config.toml if no config is found.

Usage

All API calls will return a success boolean. If it is false, it will also add error (message) and errorCode (constant from common/errors.go).

All API calls (excluding /connect endpoint) requires authentication with a token query parameter, or set as a Authorization header in the format of: Bearer $TOKEN.

Having an invalid or missing token will result in the INVALID_AUTHORIZATION error code.

Most errors starting with ERROR_ are downstream errors, usually from Redis. Check if your Redis connection is valid!

When targeting, the precedence order is: id, channel, user.

Client authentication

Claims

Claims are the recommended way to authenticate with dSock. Before a client connects, they should hit your API (which you can use your usual authentication), and your API requests the dSock API to create a "claim", which you then return to the client.

Once a client has a claim, it can then connect to the worker using the claim query parameter.

You can create them by accessing the API as POST /claim with the following query options:

The returned body will contain the following keys:

A claim is single-use, so once a client connects, it will instantly expire.

Examples

Create a claim for a user (1) expiring in 10 seconds, with 2 channels:

POST /claim?token=abcxyz&user=1&duration=10&channels=group-1,group-2

Create a claim for a user (1) with a session (a) with a claim ID (a1b2c3) expiring at some time:

POST /claim?user=1&session=a&expiration=1588473164&id=a1b2c3
Authorization: Bearer abcxyz
Errors

Creating a claim has the follow possible errors:

JWT

To authenticate a client, you can also create a JWT token and deliver it to the client before connecting. To enable this, set the jwt_secret to with your JWT secret (HMAC signature secret)

Payload options:

Client connections

Connect using a WebSocket to ws://worker/connect with the one of the following query parameter options:

You can load-balance a cluster of workers, as long as the load-balancer supports WebSockets.

Errors

The following errors can happen during connection:

Sending message

Sending a message is done through the POST /send API endpoint.

Query param options:

The body of the request is used as the message. This can be text/binary, and the Content-Type header is not used internally (only type is used).

Examples

Send a JSON message to a user (1)

POST /send?token=abcxyz&user=1&type=text

{"message":"Hello world!","from":"Charles"}

Send a text value to a user (1) with a session (a)

POST /send?user=1&session=a&type=text
Authorization: Bearer abcxyz

<Cretezy> Hey!

Send a binary value to all clients subscribed in a channel:

POST /send?channel=group-1&type=binary
Authorization: Bearer abcxyz

# Binary...

Errors

The following errors can happen during sending a message:

Disconnecting

You can disconnect a client by user (and optionally session) ID.

This is useful when logging out a user, to make sure it also disconnects any connections. Make sure to include a session in your claim/JWT to be able to disconnect only some of a user's connections.

The API endpoint is POST /disconnect, with the following query params:

Examples

Disconnect a user (1) with a session (a):

POST /send?token=abcxyz&user=1&session=a

Errors

The following errors can happen during disconnection:

Info

You can access info about connections and claims using the GET /info API endpoint. The following query params are supported:

The API will return all opened connections and non-expired claims for the target.

The returned object contains:

Examples

Get info for a user (1) with a session (a):

GET /info?token=abcxyz&user=1&session=a

Errors

The following errors can happen during getting info:

Channels

You can subscribe/unsubscribe clients to a channel using POST /channel/subscribe/$CHANNEL or POST /channel/unsubscribe/$CHANNEL.

This will subscribe the connections and claims (optional) for the target provided.

The follow query parameters are accepted:

Examples

Subscribe a user (1) to a channel (a):

POST /channel/subscribe/a?token=abcxyz&user=1

Unsubscribe all clients in a channel from a channel (a):

POST /channel/unsubscribe/a?token=abcxyz&channel=a

Errors

The following errors can happen during channel subscription/unsubscription:

Internals

dSock uses Redis as it's database (for claims and connection information) and for it's publish/subscribe capabilities. Redis was chosen because it is widely used, is performant, and supports all requried features.

Claims

When creating a claim, dSock does the following operations:

When a user connects, dSock retrieves the claim by ID and validates it's expiration. It then removes the claim from the user and user session storages.

When getting information or disconnecting, it retrieves or deletes the claim(s).

Connections

When a user connects and authenticates, dSock does the following operations:

When receiving a ping or pong from the client, it updates the last ping time. A ping is sent from the server every minute.

Connections are kept alive until a client disconnects, or is forcibly disconnected using POST /disconnect

Sending

When sending a message, the API resolves of all of the workers that hold connections for the target user/session/connection, and sends the message through Redis to that worker's channel (worker:$id).

API to worker messages are encoded using Protocol Buffer for efficiency; they are fast to encode/decode, and binary messages to not need to be encoded as strings during communication.

Channels

Channels are assosiated to claims/JWTs (before a client connects) and connections.

When (un)subscribing a target to a channel, it looks up all of the target's claims and adds the claim (if ignoreClaim is not set), and broadcasts to workers with connections that are connected through the $workerId:channel Redis channel.

The worker then resolves all connections for the target and adds them to the channel.

Channels are found under channel:$channel and contain the list of connection IDs which are subscribed.

Claim channels are found under claim-channel:$channel and contain the list of claim IDs which will become subscribed, and is also stored under channels in the claim.

FAQ

Why is built-in HTTPS not supported?

To remove complexity inside dSock, TLS is not implemented. It is expected that the API and worker nodes are behind load-balancers, which would be able to do TLS termination.

If you need TLS, you can either add a TLS-terminating load-balancer, or a reverse proxy (such as nginx or Caddy).

How can I do a health-check on dSock?

You can use the /ping endpoint on the API & worker to monitor if the service is up. It will response pong.

Development

Setup

Protocol Buffers

If making changes to the Protocol Buffer definitions (under protos), make sure you have the protoc compiler and protoc-gen-go.

Once changes are done to the definitions, run task build:protos to generate the associated Go code.

Docker

You can build the Docker images by running task build:docker. This will create the dsock-worker and dsock-api images.

Tests

dSock has multiple types of tests to ensure stability and maximum coverage.

You can run all tests by running task tests. You can also run individual test suites (see below)

End-to-end (E2E)

You can run the E2E tests by running task tests:e2e. The E2E tests are located inside the e2e directory.

Unit

You can run the unit tests by running task tests:unit. The units tests are located inside the common/api/worker directories.

Contributing

Pull requests are encouraged!

License & support

dSock is MIT licensed (see license).

Community support is available through GitHub issues. For professional support, please contact charles@cretezy.com.

Credit

Icon made by Freepik from flaticon.com.

Project was created & currently maintained by Charles Crete.