Basic Redis Chat App Demo
A basic chat application built with Golang, Websocket and Redis.
Overview video
Here's a short video that explains the project and how it uses Redis:
Technical Stacks
- Frontend - React, Socket
- Backend - Go, Redis (go-redis/redis)
How it works?
Client Server
Communication build on websocket messages.
Client should handle messages from websocket with onmessage(event)
event and processed it, where event.data
is stringify JSON.
Send request to server available with websocket send(data)
function, where data
is JSON.stringify(Object)
.
Server receive and response stringify JSON object.
Usually for not atomic message sent with type:"example"
server respond result will be with
same message type: type:"example"
, for details see Chat with websocket
block below.
Server not guarantee that responses is ordered as requests order.
As example:
Client requests
>>> Request A
>>> Request B
>>> Request C
May have responses in other order
<<< Response C
<<< Response A
<<< Response B
For complicated throws client should receive response for previous sent message before send next message.
As example in a pseudocode:
>>> Open websocket
<<< Waiting for message type ready
>>> Send message type signIn
<<< Waiting for message type authorized
>>> Send message type channelJoin
<<< Waiting for message type channelJoin
>>> Now send multiple message to websocket and multiple receive messages
Registration
User sign in
Login for chatting, if user not exist it will be created.
Send a message to websocket:
{
type: "signIn",
signIn: {
username: "Username",
password: "Password"
}
}
Receive a message from websocket:
{
type: "authorized",
authorized: {
userUUID: "123e4567-e89b-12d3-a456-426614174000",
accessKey: "generated session access key"
}
}
Send system message to all connected users on success:
{
type: "sys",
sys: {
type: "signIn",
signIn: {
uuid: "123e4567-e89b-12d3-a456-426614174000",
username: "Username"
}
}
}
Redis Commands
- Key for store user index by user UUID:
usersUUIDListIndex:<UserUUID>
- E.g
usersUUIDListIndex:123e4567-e89b-12d3-a456-426614174000
- E.g
How the data is stored:
User structure:
{
UUID: "123e4567-e89b-12d3-a456-426614174000",
Username: "User name",
Password: "Password",
AccessKey: "Access key",
OnLine: true
}
-
Add user to the end redis list, will return number of elements in success:
RPUSH users <JSON Stringify user structure>
. User index in the list is<number of elements in list> - 1
- E.g
RPUSH users "{\"UUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"Username\":\"User name\",\"Password\":\"Password\",\"AccessKey\":\"Access key\",\"OnLine\":true}"
- E.g
-
Save index for search user by Username:
- E.g
SET usersUsernameListIndex:123e4567-e89b-12d3-a456-426614174000 4
where 4 is User Index
- E.g
-
Save index for search user by UUID:
- E.g
SET usersUUIDListIndex:123e4567-e89b-12d3-a456-426614174000 4
- E.g
-
On error, we should remove index for search by Username:
- E.g
DEL usersUsernameListIndex:123e4567-e89b-12d3-a456-426614174000
- E.g
-
Set user online status, should expire after 60sec:
- E.g
SETEX userStatus:123e4567-e89b-12d3-a456-426614174000 2021-04-06T12:53:10.436Z 60
where we pass the start date in the ISO string format.
- E.g
How the data is accessed:
-
Read user by UUID from redis KV, on exist will return user index for users list in redis:
- E.g
GET usersUUIDListIndex:123e4567-e89b-12d3-a456-426614174000
- E.g
-
Read user by username from redis KV, on exists will return user index for users list in redis:
- E.g
GET usersUsernameListIndex:123e4567-e89b-12d3-a456-426614174000
- E.g
Key for store users in LList: users
- Read user from the start list by user index in list, will return json stringify on success:
- E.g
lindex users 5
where 5 is index.
- E.g
User sign up
New user registration, will append signIn
on successful.
Send a message to websocket:
{
type: "signUp",
signUp: {
username: "Username",
password: "Password"
}
}
Receive a message from websocket:
{
type: "authorized",
authorized: {
userUUID: "123e4567-e89b-12d3-a456-426614174000",
accessKey: "generated session access key"
}
}
Each of authorized messages should contain authorized properties userUUID
and sessionUUID
, see details below.
Send system message to all connected users:
{
type: "sys",
sys: {
type: "signIn",
signIn: {
uuid: "123e4567-e89b-12d3-a456-426614174000",
username: "Username"
}
}
}
Redis Commands
Check the Sign In
section for redis commands, it's the same
Logout user
This is atomic command without message body.
Send a message to websocket:
{
type: "signOut",
userUUID: "123e4567-e89b-12d3-a456-426614174000"
}
Receive a message from websocket:
{
type: "signOut",
signOut: {
uuid: "123e4567-e89b-12d3-a456-426614174000"
}
}
Redis Commands
How the data is stored:
On user exist and signed in, remove sign in data.
-
Delete user access key:
- E.g
DEL access_key:123e4567-e89b-12d3-a456-426614174000
- E.g
-
Set user offline:
- E.g
DEL userStatus:123e4567-e89b-12d3-a456-426614174000
- E.g
How the data is accessed:
-
Check if user exist. Read user index by UUID from redis list:
- E.g
GET usersUUIDListIndex:123e4567-e89b-12d3-a456-426614174000
- E.g
-
Read user from redis list by an index:
- E.g
LINDEX users 4
where 4 is User Index
- E.g
-
Read user online status (it called in
UserGet
method):- E.g
GET userStatus:123e4567-e89b-12d3-a456-426614174000
- E.g
Get users
This is atomic operation, it not expected users
block in request.
Send a message to websocket:
{
userUUID: "123e4567-e89b-12d3-a456-426614174000",
type: "users"
}
Receive a message from websocket:
{
type: "users",
users: {
total: 0,
received: 0,
users: [
{
UUID: "123e4567-e89b-12d3-a456-426614174000",
Username: "User name",
OnLine: true
}
]
}
}
Redis Commands
- Read number of users:
- E.g
LLEN users
- E.g
- Read all users:
- E.g
LRANGE users 0 10
where 10 is number of users.
- E.g
Code Example: Prepare User Data in Redis HashSet
func (r *Redis) UserCreate(username, password string) (*User, error) {
log.Println("UserCreate", fmt.Sprintf("[%s|%s]", username, password))
if user, err := r.getUserFromListByUsername(username); err == nil {
return user, nil
}
user := &User{
UUID: uuid.NewString(),
Username: username,
Password: password,
}
if err := r.addUser(user); err != nil {
return nil, err
}
return user, nil
}
Rooms
After signIn/signUp client should send channelJoin
for receive messages from specified channel.
With empty channelJoin.recipientUUID
user will join to general channel.
For private channel set channelJoin.recipientUUID
with valid userUUID
.
Before channel join we should leave other channels if joined, user should have one joined channel.
Send a message to websocket:
{
userUUID: "123e4567-e89b-12d3-a456-426614174000",
sessionUUID: "123e4567-e89b-12d3-a456-426614174000",
type: "channelJoin",
channelJoin: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000",
}
}
Receive a message from websocket
{
type: "channelJoin",
channelJoin: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000",
messages: [ // array of messages in channel in desc order
{
UUID: "123e4567-e89b-12d3-a456-426614174000", // message UUID
SenderUUID: "123e4567-e89b-12d3-a456-426614174000",
Sender: {
UUID: "123e4567-e89b-12d3-a456-426614174000", //user UUID
Username: "User name"
},
RecipientUUID: "123e4567-e89b-12d3-a456-426614174000",
Recipient: {
UUID: "123e4567-e89b-12d3-a456-426614174000", //user UUID
Username: "User name"
},
Message: "Text message",
CreatedAt: "Message send date"
}
],
users: [ // array of joined users
{
UUID: "123e4567-e89b-12d3-a456-426614174000", //user UUID
Username: "Username",
OnLine: true
}
]
}
}
Send system message to all connected users:
{
type: "sys",
SUUID: "123e4567-e89b-12d3-a456-426614174000",
userUUID: "123e4567-e89b-12d3-a456-426614174000",
user: {
UUID: "123e4567-e89b-12d3-a456-426614174000",
Username: "User name",
OnLine: true
},
sys: {
type: "channelJoin",
channelJoin: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000"
}
}
}
Join to channel (room)
After signIn/signUp client should send channelJoin
for receive messages from specified channel.
With empty channelJoin.recipientUUID
user will join to general channel.
For private channel set channelJoin.recipientUUID
with valid userUUID
.
Before channel join we should leave other channels if joined, user should have one joined channel.
Send a message to websocket:
{
userUUID: "123e4567-e89b-12d3-a456-426614174000",
sessionUUID: "123e4567-e89b-12d3-a456-426614174000",
type: "channelJoin",
channelJoin: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000",
}
}
Receive a message from websocket
{
type: "channelJoin",
channelJoin: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000",
messages: [ // array of messages in channel in desc order
{
UUID: "123e4567-e89b-12d3-a456-426614174000", // message UUID
SenderUUID: "123e4567-e89b-12d3-a456-426614174000",
Sender: {
UUID: "123e4567-e89b-12d3-a456-426614174000", //user UUID
Username: "User name"
},
RecipientUUID: "123e4567-e89b-12d3-a456-426614174000",
Recipient: {
UUID: "123e4567-e89b-12d3-a456-426614174000", //user UUID
Username: "User name"
},
Message: "Text message",
CreatedAt: "Message send date"
}
],
users: [ // array of joined users
{
UUID: "123e4567-e89b-12d3-a456-426614174000", //user UUID
Username: "Username",
OnLine: true
}
]
}
}
Send system message to all connected users:
{
type: "sys",
SUUID: "123e4567-e89b-12d3-a456-426614174000",
userUUID: "123e4567-e89b-12d3-a456-426614174000",
user: {
UUID: "123e4567-e89b-12d3-a456-426614174000",
Username: "User name",
OnLine: true
},
sys: {
type: "channelJoin",
channelJoin: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000"
}
}
}
Redis Commands
Leave channel if joined before channel join, see redis flow in Leave channel
section of this README.
How the data is stored:
-
Save joined sender to channel
HSET channelUsers:<ChannelUUID> <SenderUUID> <Joined date as string>
:- E.g
HSET channelUsers:123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426634174000 2021-04-06T13:26:44.415Z
- E.g
-
Save joined recipient for private channel
HSET channelUsers:<ChannelUUID> <RecipientUUID> <Joined date as string>
:- E.g
HSET channelUsers:123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426634174000 2021-04-06T13:26:44.415Z
- E.g
-
Subcribe to channel
SUBSCRIBE <ChannelUUID>
:- E.g
SUBSCRIBE 123e4567-e89b-12d3-a456-426614174000
- E.g
How the data is accessed:
-
Read user index by UUID:
- E.g
GET usersUUIDListIndex:123e4567-e89b-12d3-a456-426614174000
- E.g
-
Read user list by user index:
- E.g
LINDEX users 5
where 5 is index
- E.g
-
Read channel UUID
GET channelSenderRecipient:<SenderUUID>:<RecipientUUID>
:- E.g
GET channelSenderRecipient:123e4567-e89b-12d3-a456-426614174000:123e4567-e89b-12d3-a456-426614174022
- E.g
-
Count message in a channel
LLEN channelMessages:<ChannelUUID>
:- E.g
LLEN channelMessages:5
- E.g
-
Read last 10 messages from a channel, if number of messages in a channel less than 10:
- E.g
LRANGE channelMessages:123e4567-e89b-12d3-a456-426614174000 0, 10
- E.g
-
Read last 10 messages from a channel, if number of messages in a channel more than 10
<Offset>
is<Number of messages>-1
:LRANGE channelMessages:<ChannelUUID> <Offset>, -1
- E.g
LRANGE channelMessages:123e4567-e89b-12d3-a456-426614174000 10, -1
- E.g
-
Read channel users:
- E.g
HGETALL channelUsers:123e4567-e89b-12d3-a456-426614174000
- E.g
Leave channel
Send a message to websocket:
{
SUUID: "123e4567-e89b-12d3-a456-426614174000",
type: "channelLeave",
userUUID: "123e4567-e89b-12d3-a456-426614174000",
channelLeave: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000"
}
}
Receive a message from websocket, all connected users will receive it:
{
SUUID: "123e4567-e89b-12d3-a456-426614174000",
type: "channelLeave",
userUUID: "123e4567-e89b-12d3-a456-426614174000",
channelLeave: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000"
}
}
Redis Commands
How the data is stored:
If channel UUID not found for sender or recipient, we should crate it for both.
Generate channelUUID
.
-
Set channel UUID for sender
SET channelSenderRecipient:<SenderUUID>:<RecipientUUID> <ChannelUUID>
:- E.g
SET channelSenderRecipient:123e4567-e89b-12d3-a456-426614174000:123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000
- E.g
-
Set channel UUID for recipient:
SET channelSenderRecipient:<RecipientUUID>:<SenderUUID> <ChannelUUID>
- E.g
SET channelSenderRecipient:123e4567-e89b-12d3-a456-426614174000:123e4567-e89b-12d3-a456-426614174000 123e4567-e89b-12d3-a456-426614174000
- E.g
How the data is accessed:
Get channel UUID, will return public
on empty recipientUUID
.
Key for private channels, first UUID is a sender(userUUID), second is recipient(userUUID):
channelSenderRecipient:123e4567-e89b-12d3-a456-426614174000:123e4567-e89b-12d3-a456-426614174000
Key for public channels:
channelSenderRecipient:123e4567-e89b-12d3-a456-426614174000:public
- Read channel UUID:
- E.g
GET channelSenderRecipient:123e4567-e89b-12d3-a456-426614174000:public
- E.g
Code Example: Join Room
func (r *Redis) ChannelJoin(senderUUID, recipientUUID string) (*ChannelPubSub, string, error) {
channelUUID, err := r.getChannelUUID(senderUUID, recipientUUID)
if err != nil {
return nil, "", err
}
err = r.channelJoin(channelUUID, senderUUID, recipientUUID)
if err != nil {
return nil, "", err
}
pubSub := r.client.Subscribe(channelUUID)
channel := r.addChannelPubSub(channelUUID, pubSub)
return channel, channelUUID, nil
}
Messages
Send a message to websocket:
{
userUUID: "123e4567-e89b-12d3-a456-426614174000",
type: "channelMessage",
channelMessage: {
recipientUUID: "123e4567-e89b-12d3-a456-426614174000",
message: "Message text"
}
}
Receive a message from websocket, all joined users received it too:
{
type: "channelMessage",
channelMessage: {
SenderUUID: "123e4567-e89b-12d3-a456-426614174000",
Sender: {
UUID: "123e4567-e89b-12d3-a456-426614174000",
Username: "User name",
OnLine: true,
},
RecipientUUID: "123e4567-e89b-12d3-a456-426614174000",
Recipient: {
UUID: "123e4567-e89b-12d3-a456-426614174000",
Username: "User name",
OnLine: true,
},
Message: "Text message",
CreatedAt: "0000-00-00T00:00:00.000000000Z"
}
}
Redis Commands
How the data is stored:
-
Publish a message to redis PubSub:
PUBLISH <ChannelUUID> <Message json as string>
- E.g
PUBLISH 123e4567-e89b-12d3-a456-426614174000 {\"UUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"SenderUUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"RecipientUUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"Message\":\"Text message\",\"CreatedAt\":\"0000-00-00T00:00:00.000000000Z\"}
- E.g
-
Save message in the end of redis list:
RPUSH channelMessages.<ChannelUUID> <Message json as string>
- E.g
RPUSH channelMessages.123e4567-e89b-12d3-a456-426614174000 {\"UUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"SenderUUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"RecipientUUID\":\"123e4567-e89b-12d3-a456-426614174000\",\"Message\":\"Text message\",\"CreatedAt\":\"0000-00-00T00:00:00.000000000Z\"}
- E.g
Message structure:
{
UUID: "123e4567-e89b-12d3-a456-426614174000", // message UUID
SenderUUID: "123e4567-e89b-12d3-a456-426614174000", // user UUID
RecipientUUID: "123e4567-e89b-12d3-a456-426614174000", // user UUID, or empty for public channel
Message: "Text message",
CreatedAt: "0000-00-00T00:00:00.000000000Z"
}
How the data is accessed:
Read channel UUID. See Redis Commands in Channel leave
.
Code Example: Send Message
func channelSessionsSendMessage(skipUserUUID, channelUUID string, write Write, message *Message) {
channelSessionsSync.RLock()
defer channelSessionsSync.RUnlock()
for _, data := range channelSessionsJoins[channelUUID] {
if skipUserUUID != "" && skipUserUUID == data.userUUID {
continue
}
if err := write(data.conn, ws.OpText, message); err != nil {
log.Println(err)
}
}
}
Session handling
On first connect to websocket client receive ready
message:
{
type: "ready",
ready: {
sessionUUID: "123e4567-e89b-12d3-a456-426614174000"
}
}
Redis Commands
How the data is stored:
-
Key for store user session UUID:
userSession:123e4567-e89b-12d3-a456-426614174000
- E.g
SETEX userSession.123e4567-e89b-12d3-a456-426614174000 0000-00-00T00:00:00.000000000Z 3600
- E.g
-
Remove user session:
- E.g
DEL userSession.123e4567-e89b-12d3-a456-426614174000
- E.g
How the data is accessed:
-
Read user session created time:
- E.g
GET userSession.123e4567-e89b-12d3-a456-426614174000
- E.g
Code example: Managing session
func (r *Redis) getKeyUserSession(userSessionUUID string) string {
return fmt.Sprintf("%s.%s", keyUserSession, userSessionUUID)
}
func (r *Redis) AddConnection(userSessionUUID string) error {
key := r.getKeyUserSession(userSessionUUID)
return r.client.Set(key, time.Now().String(), time.Hour).Err()
}
func (r *Redis) DelConnection(userSessionUUID string) error {
key := r.getKeyUserSession(userSessionUUID)
return r.client.Del(key).Err()
}
How to run it locally?
The client utilizes Create React App template, to run it with the development instance of backend, specify the proxy parameter in package.json:
"proxy": "http://localhost:5555",
Run frontend
cd client
yarn install
yarn start
Run backend
Set the next environment variables (.env.example):
SERVER_ADDRESS=:5555
CLIENT_LOCATION=/api/public
REDIS_HOST=chat-redis
REDIS_ADDRESS=:6379
REDIS_PASSWORD=
go run