go_chat_i_guess

package module
v0.0.0-...-39bf8bd Latest Latest
Warning

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

Go to latest
Published: Sep 11, 2021 License: Zlib Imports: 8 Imported by: 0

README

Go chat, I guess?

A generic, connection-agnostic chat server.

The server support multiple channels/chat rooms, to which users may connect. Both the channel and users must be uniquely identified by their name, although the calling application is responsible by authenticating if those are valid values.

Prerequisites

  • Go 1.15 (this package hasn't been made in a module, so more recent versions probably won't work)

Documentation

The documentation may be generated using godoc:

go get golang.org/x/tools/cmd/godoc
godoc .

Example chat

To build a simple WebSocket-based example chat:

go get github.com/gorilla/websocket
go build ./cmd/chat-server
./chat-server

Then, simply open http://localhost:8888 in a browser.

Documentation

Overview

Package go_chat_i_guess implements a generic, connection-agnostic chat server.

The chat is divided into three components:

  • `ChatServer`: The interface for the actual server
  • `ChatChannel`: The interface for a given chat channel/room
  • `Conn`: A connection to the remote client

Internally, there's also a fourth component, the `user`, but that's never exported by the API. The user associates a `Conn` to a username, which must be unique on that `ChatChannel`.

The first step to start a Chat Server is to instantiate it through one of `NewServer`, `NewServerWithTimeout` or `NewServerConf`. The last one should be the preferred variant, as it's the one that allows the most customization:

conf := go_chat_i_guess.GetDefaultServerConf()
// Modify 'conf' as desired
server := go_chat_i_guess.NewServerConf(conf)

A `ChatServer` by itself doesn't do anything. It simply manages `ChatChannels` and connection tokens. Instantiating a new `ChatServer` will start its cleanup goroutine, so idle `ChatChannel`s and expired tokens may be automatically released (more on those in a moment).

`ChatChannel`s may be created on that `ChatServer`. Each channel is uniquely identified by its name and runs its own goroutine to broadcast messages to every connect user.

// This should only fail if the channel's name has already been taken
err := server.CreateChannel("channel-name")
if err != nil {
    // Handle the error
}

By default, a channel must receive a connection within 5 minutes of its creation, otherwise it will automatically close. Two things are required to connect a remote client to a channel: a authentication token and a connection.

Since the `ChatServer` doesn't implement any authentication mechanism, the caller is responsible by determining whether the requester is allowed to connect to a given channel with the requested username. If the caller accepts the requester's authentication (whichever it may be), it must generate a token within the `ChatServer` for that username/channel pair:

// XXX: Authenticate the user somehow

token, err := server.RequestToken("the-user", "channel-name")
if err != nil {
    // Handle the error
}

// XXX: Return this token to the requester

Then, the user must connect to the `ChatServer` using the retrieved token and something that implements the `Conn` interface. `conn_test.c` implements `mockConn`, which uses chan string to send and receive messages. Another option could be to implement an interface for a WebSocket connection. The user is added to the channel by calling either `Connect`, which spawns a goroutine to wait for messages from the user, or `ConnectAndWait`, which blocks until the `Conn` gets closed. This second options may be useful if the server already spawns a goroutine to handle requests.

var conn Conn
err := server.Connect(token, conn)
if err != nil {
    // Handle the error
}

From this point onward, `Conn.Recv` blocks waiting for a message, which then gets forwarded to the `ChatChannel`. The `ChatChannel` then broadcasts this message to every connected user, including the sender, by calling their `Conn.SendStr`.

It's important to note that the although any `Conn` may be used for the connection, it's also used to identify the user within the `ChatChannel`. This may either be done by using a transport that maintains a connection (TCP, WebSocket etc) or by manually associating this `Conn` to its associated user.

By default, a Chat Server simply broadcasts the received messages as strings. This behaviour may be expanded by defining a `Encoder` in the server's `ServerConf`. This `MessageEncoder` could be used to process messages (for example, listing the users in a given channel) and/or to encode the message into a JSON object.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ChannelController

type ChannelController interface {
	MessageEncoder

	// OnConnect is called whenever a channel detects that a user has just
	// joined it.
	//
	// If the channel doesn't have a `ChannelController`, it broadcasts the
	// message:
	//
	//     channel.NewSystemBroadcast(username + " entered " + channel.Name() + "...")
	OnConnect(channel ChatChannel, username string)

	// OnDisconnect is called whenever a channel detects that a user has
	// disconnected.
	//
	// Since this may be called with some internal objects locked, only a
	// few channel methods may be called from this function. For this
	// reason, the `RestrictedChatChannel` SHALL NOT be cast and used as a
	// `ChatChannel`!
	//
	// If the channel doesn't have a `ChannelController`, it broadcasts the
	// message:
	//
	//     channel.NewSystemBroadcast(username + " exited " + channel.Name() + "...")
	OnDisconnect(channel RestrictedChatChannel, username string)
}

ChannelController processes events received by the channel.

type ChatChannel

type ChatChannel interface {
	io.Closer

	RestrictedChatChannel

	// GetUsers retrieve the list of connected users in this channel. If
	// `list` is supplied, the users are appended to the that list, so be
	// sure to empty it before calling this function.
	GetUsers(list []string) []string

	// Remove the user `username` from this channel.
	RemoveUser(username string) error

	// ConnectUser add a new user to the channel.
	//
	// It's entirely up to the caller to initialize the connection used by
	// this user, for example upgrading a HTTP request to a WebSocket
	// connection.
	//
	// The `channel` does properly synchronize this function, so it may be
	// called by different goroutines concurrently.
	//
	// On error, `conn` is left unchanged and must be closed by the caller.
	//
	// If `conn` is nil, then this function will panic!
	ConnectUser(username string, conn Conn) error

	// ConnectUser add a new user to the channel and blocks until the
	// user closes the connection to the server.
	//
	// The `channel` does properly synchronize this function, so it may be
	// called by different goroutines concurrently.
	//
	// On error, `conn` is left unchanged and must be closed by the caller.
	//
	// Differently from `ConnectUser`, this function handles messages
	// from the remote client in the calling goroutine. This may be
	// advantageous if the external server already spawns a new goroutine
	// to handle each new connection.
	//
	// If `conn` is nil, then this function will panic!
	ConnectUserAndWait(username string, conn Conn) error
}

The public interface for a chat channel.

type ChatError

type ChatError uint

Error type for this package.

const (
	// Invalid token. Either the token doesn't exist, it has already been used
	// or it has already expired.
	InvalidToken ChatError = iota
	// Channel did not receive any connections in a timely manner.
	IdleChannel
	// There's already another channel with the requested name.
	DuplicatedChannel
	// Invalid Channel. Either the channel doesn't exist or it has already
	// expired (or been closed).
	InvalidChannel
	// The channel was closed before the operation completed.
	ChannelClosed
	// The requesting user is already connected to the channel
	UserAlreadyConnected
	// The connection was closed
	ConnEOF
	// A test connection timed out
	TestTimeout
	// Invalid user.
	InvalidUser
)

func (ChatError) Error

func (c ChatError) Error() string

type ChatServer

type ChatServer interface {
	io.Closer

	// GetConf retrieve a copy of the server's configuration. As such,
	// changing it won't cause any change to the configurations of the
	// running server.
	GetConf() ServerConf

	// RequestToken generate a token temporarily associating the user identified
	// by `username` may connect to a `channel`.
	//
	// This token should be requested from an authenticated and secure channel.
	// Then, the returned token may be sent in a 'Connect()' to identify the
	// user and the desired channel.
	//
	// RequestToken should only fail if it somehow fails to generate a token.
	RequestToken(username, channel string) (string, error)

	// CreateChannel create and start the channel with the given `name`.
	//
	// Channels are uniquely identified by their names. Also, the chat
	// server automatically removes a closed channel, regardless whether
	// it was manually closed or whether it timed out.
	CreateChannel(name string) error

	// GetChannel retrieve the channel named `name`.
	GetChannel(name string) (ChatChannel, error)

	// Connect a user to a channel, previously associated to `token`, using
	// `conn` to communicate with this user.
	//
	// On error, the token must be re-generated.
	//
	// If `conn` is nil, then this function will panic!
	Connect(token string, conn Conn) error

	// ConnectAndWait connect a user to a channel, previously associated to
	// `token`, using `conn` to communicate with this user.
	//
	// Differently from `Connect`, this blocks until `conn` gets closed.
	// This may be usefull if the called already spawns a new goroutine to
	// handle each new incoming connection.
	//
	// On error, the token must be re-generated.
	//
	// If `conn` is nil, then this function will panic!
	ConnectAndWait(token string, conn Conn) error
}

The public interfacer of the chat server.

func NewServer

func NewServer(readBuf, writeBuf int) ChatServer

NewServer create a new chat server with the requested size for the `readBuf` and for the `writeBuf`.

See `NewServerConf()` for more details.

func NewServerConf

func NewServerConf(conf ServerConf) ChatServer

NewServerConf create a new chat server, as specified by `conf`.

When a new chat server starts, a clean up goroutine is spawned to check and release expired resources periodically. This goroutine is stopped, and every resource is released, when the ChatServer gets `Close()`d.

func NewServerWithTimeout

func NewServerWithTimeout(readBuf, writeBuf int,
	tokenDeadline, tokenCleanupDelay time.Duration) ChatServer

NewServerWithTimeout create a new chat server with the requested size for the `readBuf` and for the `writeBuf`. Additionally, the access `tokenDeadline` and `tokenCleanupDelay` may be configured.

See `NewServerConf()` for more details.

type Conn

type Conn interface {
	io.Closer

	// Recv blocks until a new message was received.
	Recv() (string, error)

	// SendStr send `msg`, previously formatted by the caller.
	//
	// Note that the server may send an empty message to check if this
	// connection is still active.
	SendStr(msg string) error
}

Conn is a generic interface for sending and receiving messages.

type MessageEncoder

type MessageEncoder interface {
	// Encode the message described in the parameter into the string that
	// will be sent to the channel.
	//
	// Returning the empty string will cancel sending the message, which
	// may be useful for command processing. It may also be used to reply
	// directly, and exclusively, to the sender after processing the
	// message.
	//
	// `from` is set internally by the `ChatChannel`, based on the `Conn`
	// that received this message and forwarded it to the server. Using it
	// to determine whether the requesting user is allowed to do some
	// operation should be safe, as long as users are properly
	// authenticated before generating their connection token.
	Encode(channel ChatChannel, date time.Time, msg, from, to string) string
}

MessageEncoder encodes a given message into the string that will be sent by the channel.

type RestrictedChatChannel

type RestrictedChatChannel interface {
	// Name retrieve the channel's name.
	Name() string

	// NewBroadcast queue a new broadcast message from a specific sender,
	// setting its `Date` to the current time and setting the message's
	// `Message` and sender (its `From`) as `msg` and `from`, respectively.
	NewBroadcast(msg, from string)

	// NewSystemBroadcast queue a new system message (i.e., a message
	// without a sender), setting its `Date` to the current time and
	// setting `Message` to `msg`.
	NewSystemBroadcast(msg string)

	// NewSystemWhisper queue a new system message (i.e., a message without
	// a sender) to a specific receiver, setting its `Date` to the current
	// time and setting `Message` to `msg`.
	NewSystemWhisper(msg, to string)

	// IsClosed check if the channel is closed.
	IsClosed() bool
}

Restricted public interface for a chat channel, usable while handling channel events.

type ServerConf

type ServerConf struct {
	// Size for the read buffer on new connections.
	ReadBuf int

	// Size for the write buffer on new connections.
	WriteBuf int

	// For how long a given token should exist before being used or expiring.
	TokenDeadline time.Duration

	// Delay between executions of the token cleanup routine.
	TokenCleanupDelay time.Duration

	// For how long a given channel may stay idle (without receiving any
	// connection). After this timeout expires, the channel sends a message
	// to every user, to check whether they are still connected.
	//
	// If no user is connected to the channel when it times out, the
	// channel will be automatically closed!
	ChannelIdleTimeout time.Duration

	// Delay between executions of the channel cleanup routine.
	ChannelCleanupDelay time.Duration

	// Controller optionally processes and encodes messages received by
	// this server's channels.
	//
	// This may optionally implement `ChannelController` as well!
	Controller MessageEncoder

	// Logger used by the chat server to report events. If this is nil, no
	// message shall be logged!
	Logger *log.Logger

	// Whether debug messages should be logged.
	DebugLog bool
}

ServerConf define various parameters that may be used to configure the server.

func GetDefaultServerConf

func GetDefaultServerConf() ServerConf

GetDefaultServerConf retrieve a fully initialized `ServerConf`, with all fields set to some default, and non-zero, value.

Directories

Path Synopsis
cmd
Package gorilla_ws_conn implements the Conn interface from https://github.com/SirGFM/go-chat-i-guess over a WebSocket connection from https://github.com/gorilla/websocket.
Package gorilla_ws_conn implements the Conn interface from https://github.com/SirGFM/go-chat-i-guess over a WebSocket connection from https://github.com/gorilla/websocket.

Jump to

Keyboard shortcuts

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