texto

package module
v0.0.0-...-c12b46c Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2017 License: MIT Imports: 12 Imported by: 0

README

texto - Simple Messaging Service CircleCI

Texto is a simple directed chat server written in Go. Clients communicate with the server using the WebSocket Protocol.

Running

Prerequisites:

  • docker & docker-compose 1.13+
$ git clone https://github.com/kureuil/texto.git
$ cd texto
$ docker-compose build
$ docker-compose up

The texto server should now be running. The server is bundled with a sample client written in JS, which if you used docker, should be accessible here: http://localhost:8398/

Architecture

This messaging server is built to be as flexible as possible. For this reason, there is an nginx proxy in front of the application, allowing for the load balancing of the requests. Also, the messaging server doesn't store the messages (if you send a message to an inexistant user, it won't ever be sent to anyone), and relies on Redis for the message dispatching.

When a message is sent to the messaging server, it is transformed into a simpler message and published on the recipient's redis channel. All messaging servers listen on every channel, and if a message to meant for a user known on the current node, it is relayed.

This makes the system resilient to failure, if a messaging server is malfunctioning or stops you just have to start a new one and register it into your load balancer (probably via your service discovery daemon). On the database side, Redis provides a Sentinel mode which allow for easy replication and master-reelection in case of failure.

Finally, splitting the messaging server from the database allow for easier experimentation and gradual deployment. You can easily migrate the instance one-by-one and see the effects of your upgrade in real-time, and easily rollback in case of malfunction.

                               +------------------+          +------------------------+
                               |                  |          |                        |
                               |                  |          |                        |
                        +------> Messaging Server <-----+    | Redis Server           |
                        |      |                  |     |    | Replica 1              |
                        |      |                  |     |    |                        |
                        |      +------------------+     |    +-----------^------------+
                        |                               |                |
+-------------------+   |      +------------------+     |    +-----------v------------+
|                   <---+      |                  |     +---->                        |
|                   |          |                  |          |                        |
| NGINX             <----------> Messaging Server <----------> Redis Server           |
| Load Balancer     |          |                  |          | Master                 |
|                   <---+      |                  |     +---->                        |
+-------------------+   |      +------------------+     |    +-----------^------------+
                        |                               |                |
                        |      +------------------+     |    +-----------v------------+
                        |      |                  |     |    |                        |
                        |      |                  |     |    |                        |
                        +------> Messaging Server <-----+    | Redis Server           |
                               |                  |          | Replica 2              |
                               |                  |          |                        |
                               +------------------+          +------------------------+

Example deployment

API Endpoints

If you wish to create your own client, you first need to be able to establish a WebSocket connection, as this is the main channel of communication between a client and a server.

All the exemples are written in Javascript, except when stated otherwise.

/v1/texto

This is the first version of the Texto Messaging Protocol. It relies on JSON messages sent through a WebSocket connection.

Message Schema

Every JSON message follows the same schema:

{
    // The client_id field stores the UUID of the current client/session.
    "client_id": "754cd3a0-27b3-4c51-a66e-466fed82b667",
    // The id field stores the UUID of the current message. When the server sends a response, it will use the id of the
    // request message.
    "id": "8a15b000-02d7-4823-8336-0cd0b0b13ae9",
    // The kind field indicates the type of the current message.
    "kind": "ack",
    // The data field is a dynamically shaped field which content depends on the kind field.
    "data": null
}
Message Kinds
error

The error message kind indicates that an error was encountered when processing a request.

Payload

{
    // The code field stores the internal code of the error, which is intended for programmatic use.
    "code": "ENOMEM",
    // The description field stores a human readable of the error and can be displayed safely to a user or logged.
    "description": "Out-of-memory"
}
registration

The registration message kind is sent by the client when it wants to fetch information about its current session.

In the future, it could be used for authentication.

Payload

null
connection

The connection message kind is sent in response to a registration request.

Payload

{
    // The client_id field stores the UUID of the current client/session.
    "client_id": "754cd3a0-27b3-4c51-a66e-466fed82b667"
}
send

The send message kind is sent when a client wants to send a message to another client.

Payload

{
    // The receiver_id field stores the UUID of the message's recipient.
    "receiver_id": "754cd3a0-27b3-4c51-a66e-466fed82b667",
    // The text of the message
    "text": "Lorem ipsum dolor sit amet..."
}
receive

The receive message kind is sent by the server when a client is receiving a message.

Payload

{
    // The sender_id field stores the UUID of the message's sender.
    "sender_id": "754cd3a0-27b3-4c51-a66e-466fed82b667",
    // The text of the message
    "text": "Lorem ipsum dolor sit amet..."
}
ack

The ack message kind is sent to acknowledge of the reception of a send or a receive message.

Payload

null
Examples
// Establishing a WebSocket connection with
let ws = new WebSocket("ws://localhost:8398/v1/texto");
let sessionId = null;
ws.onmessage = function(event) {
    let message = JSON.parse(event.data)
    // Treat first received message independently
    if (sessionId === null && message.kind === 'connection') {
        sessionId = message.client_id;
        return;
    }
    // Process incoming message...
};

Once a connection is established with a client, the server sends a message containing the ID of the current session.

Documentation

Index

Constants

View Source
const (
	// ErrorMessageKind is returned whenever an error prevents the node from processing a request.
	ErrorMessageKind = "error"
	// RegistrationKind is sent by a Client on its first connection to a server.
	RegistrationKind = "registration"
	// ConnectionMessageKind is sent a Client in response to a RegisterRequestKind. The response includes its ClientID.
	ConnectionMessageKind = "connection"
	// SendMessageKind is sent by a Client whenever it wants to transmit a message to another Client.
	SendMessageKind = "send"
	// ReceiveMessageKind is sent by a Server to a Client, when another Client wants to transmit a message to them.
	ReceiveMessageKind = "receive"
	// AcknowledgeMessageKind is sent after a SendMessageKind or a ReceiveMessageKind was properly processed by the node.
	AcknowledgeMessageKind = "ack"
)
View Source
const RedisBrokerPrefix = "texto:"

RedisBrokerPrefix is the prefix used for all keys registered by the RedisBroker.

Variables

This section is empty.

Functions

This section is empty.

Types

type Broker

type Broker interface {
	// Regsiter adds a Client to the Broker
	Register(client *Client) error
	// Unregister removes a client from the Broker
	Unregister(client *Client) error
	// Send sends the given message to the Client associated to the given ID.
	Send(receiverID uuid.UUID, message *BrokerMessage) error
	// Poll reads all incoming messages and transmit them to known Clients.
	Poll(ctx context.Context) error
}

A Broker is responsible for transmitting messages between users.

type BrokerMessage

type BrokerMessage struct {
	SenderID    uuid.UUID
	RecipientID uuid.UUID
	Text        string
}

A BrokerMessage is sent between Brokers to transmit the messages to the right user.

type ChatHandler

type ChatHandler struct {
	Log      *logrus.Logger
	Broker   Broker
	Upgrader websocket.Upgrader
	Timeout  time.Duration
}

ChatHandler is the HTTP Handler responsible for upgrading connections to the WebSocket Protocol and managing them.

func (*ChatHandler) ServeHTTP

func (h *ChatHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP is the http.Handler implementation for ChatHandler.

type ChatMessage

type ChatMessage struct {
	// Each client is assigned a UUID when it connects to a server.
	ClientID uuid.UUID `json:"client_id"`
	// Each message is identified by its own UUID
	ID uuid.UUID `json:"id"`
	// The kind of the message.
	Kind string `json:"kind"`
	// The actual content of the message, if any.
	Data interface{} `json:"data"`
}

A ChatMessage conforms to the schema that the chat clients and servers use to communicate.

func NewAckMessage

func NewAckMessage(messageID *uuid.UUID, clientID uuid.UUID) *ChatMessage

NewAckMessage creates a new ChatMessage of kind "acknowledge".

func NewConnectionMessage

func NewConnectionMessage(messageID *uuid.UUID, clientID uuid.UUID, payload ConnectionMessagePayload) *ChatMessage

NewConnectionMessage creates a new ChatMessage of kind "connection", with a ConnectionMessagePayload.

func NewErrorMessage

func NewErrorMessage(messageID *uuid.UUID, clientID uuid.UUID, payload ErrorMessagePayload) *ChatMessage

NewErrorMessage creates a new ChatMessage of kind "error", with an ErrorMessagePayload.

func NewReceiveMessage

func NewReceiveMessage(messageID *uuid.UUID, clientID uuid.UUID, payload ReceiveMessagePayload) *ChatMessage

NewReceiveMessage creates a new ChatMessage of kind "send", with a ReceiveMessagePayload.

func NewRegistrationMessage

func NewRegistrationMessage(messageID *uuid.UUID, clientID uuid.UUID) *ChatMessage

NewRegistrationMessage creates a new ChatMessage of kind "registration".

func NewSendMessage

func NewSendMessage(messageID *uuid.UUID, clientID uuid.UUID, payload SendMessagePayload) *ChatMessage

NewSendMessage creates a new ChatMessage of kind "send", with a SendMessagePayload.

func (*ChatMessage) UnmarshalJSON

func (m *ChatMessage) UnmarshalJSON(input []byte) error

MessageUnmarshalJSON unmarshals a JSON description of a ChatMessage into the given instance.

type Client

type Client struct {
	// The universally unique ID of the user on this node.
	ID uuid.UUID
	// contains filtered or unexported fields
}

A Client represents an open WebSocket connection with a user.

func NewClient

func NewClient(log *logrus.Logger, conn *websocket.Conn, broker Broker) *Client

NewClient creates a new Client from an open WebSocket connection.

func (*Client) HandleMessage

func (c *Client) HandleMessage(msg *ChatMessage) *ChatMessage

HandleMessage processes the given message and returns the ChatMessage that should be send back to the user.

func (*Client) Run

func (c *Client) Run(timeout time.Duration)

Run listens on the inboundChan and outboundChan for new messages to process or send. It timeouts after 5 minutes of inactivity.

type ConnectionMessagePayload

type ConnectionMessagePayload struct {
	ClientID uuid.UUID `json:"client_id"`
}

A ConnectionMessagePayload contains the id of a newly registered client.

type ErrorMessagePayload

type ErrorMessagePayload struct {
	Code        string `json:"code"`
	Description string `json:"description"`
}

An ErrorMessagePayload contains the code and the human-readable description of an error.

type ReceiveMessagePayload

type ReceiveMessagePayload struct {
	SenderID uuid.UUID `json:"sender_id"`
	Text     string    `json:"text"`
}

A ReceiveMessagePayload contains the sender's ID and the content of the message.

type RedisBroker

type RedisBroker struct {
	Log *logrus.Logger
	// contains filtered or unexported fields
}

A RedisBroker transmits messages between users using Redis as its backend.

func NewRedisBroker

func NewRedisBroker(log *logrus.Logger, addr string) (*RedisBroker, error)

NewRedisBroker creates a new RedisBroker instance, connecting to the Redis server using the given TCP address.

func (*RedisBroker) Poll

func (b *RedisBroker) Poll(ctx context.Context) error

Poll reads all messages published on the Redis server. If a message is intended to a known user, the Broker will send it into the recipient's outboundChan.

func (*RedisBroker) PumpMessages

func (b *RedisBroker) PumpMessages(channelsPattern string, out chan *redis.PMessage)

PumpMessages subscribe to Redis channels, reads all incoming messages and sends them into the given channel. If an error is encountered while reading the messages, the channel is closed and the function exits.

func (*RedisBroker) Register

func (b *RedisBroker) Register(client *Client) error

Register registers a Client in the internal Client map of the Broker.

func (*RedisBroker) Send

func (b *RedisBroker) Send(receiverID uuid.UUID, message *BrokerMessage) error

Send publishes the given message on the Redis server.

func (*RedisBroker) Unregister

func (b *RedisBroker) Unregister(client *Client) error

Unregister removes a Client from the internal Client map of the Broker.

type SendMessagePayload

type SendMessagePayload struct {
	ReceiverID uuid.UUID `json:"receiver_id"`
	Text       string    `json:"text"`
}

A SendMessagePayload contains the receiver's ID and the content of the message.

type Server

type Server struct {
	Log        *logrus.Logger
	Broker     Broker
	HTTPServer http.Server
	// contains filtered or unexported fields
}

A Server bundles an HTTP Server and all the configuration required at runtime.

func NewServer

func NewServer(parent context.Context, log *logrus.Logger, addr string, broker Broker) (*Server, error)

NewServer returns an initialized Server.

func (*Server) Run

func (s *Server) Run() error

Run tells the Server to start listening for incoming HTTP connections.

func (*Server) Stop

func (s *Server) Stop() error

Stop gracefully stops the server.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

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