A web sockets chat server implemented in Akka, Akka Streams, and Akka Persistence.
Not meant to be production-ready. Never tested for the real load. Not systematically supported.
- A single channel, multiple users.
- Multiple sessions of a single user at the same time.
- Authentication (right now, only pre-defined users).
- Global serialization of chat events.
- Left/join notifications; request for users online.
- Request for "previous" messages in the chat.
- Double send prevention.
- Akka Persistence for the chat log.
- A very simple React frontend.
- User - a person who uses the chat and their account.
- Client - a client application that connects to the server.
- Session - an established connection between a client and the server.
- Chat log - the ordered sequence of events that happened in the chat.
The communication between the server and client happens via web sockets in JSON format (described below).
A session is an established connection between a client and the server.
A user can open multiple sessions with the chat through different clients.
The user joins the chat when the first session is opened and leaves when the last is closed.
The chat log consists of events. There are the following types of events:
- A user joined of left.
- A user sent a message.
The chat supports the global order of events. It assigns sequence numbers and timestamps to events, serving as the only source of truth about their sequence. This prevents de-synchronization between the chat log on clients if they're implemented correctly.
The client sends authRequest
message to the server with the login and password.
The server responses with authResponse
, which may be positive or negative if authentication failed.
In case of the negative response, the server closes the connection afterwards.
authRequest
{
"msgType": "authRequest",
"username": "alice",
"password": "alice"
}
authResponse
{
"msgType": "authResponse",
"success": true
}
Implementation note: see AuthenticationStage
.
When the client is authenticated, the chat actor is notified about it.
It assigns a global sequence number and a timestamp to the event of joining, adds it to the log and persists it.
The fact of joining is dispatched to other online users by sending userJoinedOrLeft
to their sessions.
userJoinedOrLeft
{
"msgType": "userJoinedOrLeft",
"seqN": 123,
"username": "alice",
"joined": true,
"timestamp": "2018-02-14T21:46:37.897+02:00[Europe/Helsinki]"
}
Once the client has joined the chat, it normally requests the list of other clients with "online" indication by sending getUsersInChat
.
The server replies with usersInChat
.
getUsersInChat
{
"msgType": "getUsersInChat"
}
usersInChat
{
"msgType": "usersInChat",
"users": [
{"username": "bob", "online": true},
{"username": "charlie", "online": false}
]
}
The client will be notified about subsequent joins and leaves.
The client also requests the existing chat log elements by sending getChatLogElements
.
The server replies with chatLogElements
.
The server always returns up to a specified (configurable) number of elements (50 by default).
This is also used for the scrolling chat log backwards with the loading of previous elements.
getChatLogElements
{
"msgType": "getChatLogElements"
}
It's possible to requests elements before an event with a particular sequence number.
To do this, an integer field before
must be added.
chatLogElements
{
"msgType": "chatLogElements",
"elements": [
{
"elementType": "userJoinedOrLeft",
"seqN": 123,
"username": "alice",
"joined": true,
"timestamp": "2018-02-14T21:46:37.897+02:00[Europe/Helsinki]"
},
{
"elementType": "message",
"seqN": 124,
"username": "alice",
"timestamp": "2018-02-14T21:46:37.897+02:00[Europe/Helsinki]",
"text": "Hi there!"
}
]
}
To send a message to the chat, the client:
- Generates a unique client-side ID, normally, UUID.
- Sends
clientToServerMessage
to the server. - Waits for
messageAck
with the client-side ID and the assigned sequence number ("true" ID) and the assigned timestamp. - In the presentation, adds the timestamp to the message, replaces the client-side ID with the sequence number and moves the message according to the sequence number.
The server acts the following way:
- When receives
clientToServerMessage
, assigns a sequence number and a timestamp to it, adds it to the chat log and persists it. - Notifies other clients and other sessions of the sender user about the message by sending
serverToClientMessage
to them. - Acknowledges the message to the sender by replying with
messageAck
.
clientToServerMessage:
{
"msgType": "clientToServerMessage",
"clientSideId": "313888dd-5dc4-49e7-a54d-72c8d8c5eb26",
"text": "Hi."
}
serverToClientMessage:
{
"msgType": "serverToClientMessage",
"seqN": 123,
"username": "alice",
"timestamp": "2018-02-14T21:46:37.897+02:00[Europe/Helsinki]",
"text": "Hi."
}
messageAck:
{
"msgType": "messageAck",
"clientSideId": "313888dd-5dc4-49e7-a54d-72c8d8c5eb26",
"seqN": 123,
"timestamp": "2018-02-14T21:46:37.897+02:00[Europe/Helsinki]"
}
When all the user's sessions are closed, they leaves the chat.
The chat notifies other users the same way as with joining—by sending userJoinedOrLeft
—
but with "joined": false
.
{
"msgType": "userJoinedOrLeft",
"seqN": 123,
"username": "alice",
"joined": false,
"timestamp": "2018-02-14T21:46:37.897+02:00[Europe/Helsinki]"
}
The UI is an extremely simple React application. To run it:
- set the connection string in ui/config.js (the default is for
localhost
); - open ui/chat.html in the browser.
Copyright 2018 Ivan Yurchenko
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.