A simple experiment / prototype exploring how to scale webscokets across multiple servers, using Redis for inter-server communication.
You can build the project with stack:
stack build
For development, you can enable fast builds with file-watching, documentation-building, & test-running:
stack test --haddock --fast --file-watch --pedantic
To build & open the documentation, run:
stack haddock --open websockets-prototype
# Start a server on port 9000
$ PORT=9000 stack run
# Start another server on port 9001
$ PORT=9001 stack run
# Check out the subscriber counts for Redis channels
$ redis-cli
127.0.0.1:6379> PUBSUB NUMSUB ping1 ping2
1) "ping1"
2) (integer) 2
3) "ping2"
4) (integer) 2
127.0.0.1:6379> exit
# Hit the servers, observe both servers printing even though the request was
# only to one.
curl localhost:9000/ping1
curl localhost:9000/ping2
curl localhost:9001/ping1
curl localhost:9001/ping2
You'll need a WebSockets client. Postman has one built in & is what we use.
# Start a server on port 9000
$ PORT=9000 stack run
# Start another server on port 9001
$ PORT=9001 stack run
Open a websocket connection to each server in 2 clients via the /websockets
route(ws://localhost:9000/websockets
& ws://localhost:9001/websockets
). You
should see each server print out the new connection & the list of connected
clients.
In a client, send this message:
{
"channel": "chat",
"message": {
"type": "SubmitMessage",
"contents": "Hello World"
}
}
The receiving server should print out the parsed message & all clients should receive this reply back, no matter what server they are connected to:
{
"channel": "chat",
"message": {
"contents": {
"content": "Hello World",
"postedAt": "2022-09-09T18:12:45.90481586Z"
},
"type": "NewMessageReceived"
}
}
Hit either of the ping API routes with curl:
curl http://localhost:9001/ping2
You should see every websockets client receive this message:
{
"channel": "ping",
"message": {
"contents": 2,
"type": "Ping"
}
}
Again, start two servers & connect a client to each. In one client, subscribe to a new chat room:
{
"channel": "chat-room",
"message": {
"type": "JoinRoom",
"contents": {
"room": "#haskell"
}
}
}
You will receive back a members list(accurate list not implemented):
{
"channel": "chat-room",
"message": {
"contents": {
"room": "#haskell",
"users": []
},
"type": "MemberList"
}
}
As well as an acknowledgement of joining:
{
"channel": "chat-room",
"message": {
"contents": {
"room": "#haskell",
"user": "b5eda4bd-a8f2-4d6f-81c5-3da4c4333679"
},
"type": "JoinedRoom"
}
}
Send a message to the chat room:
{
"channel": "chat-room",
"message": {
"type": "SendMessage",
"contents": {
"room": "#haskell",
"message": "Monad monoid category whatever"
}
}
}
You will receive the message back, the other client will not get this since it is not subscribed:
{
"channel": "chat-room",
"message": {
"contents": {
"message": "Monad monoid category whatever",
"room": "#haskell",
"user": "b5eda4bd-a8f2-4d6f-81c5-3da4c4333679"
},
"type": "NewMessage"
}
}
Now, join the room in the other client:
{
"channel": "chat-room",
"message": {
"type": "JoinRoom",
"contents": {
"room": "#haskell"
}
}
}
The first client will be notified of the new user joining:
{
"channel": "chat-room",
"message": {
"contents": {
"room": "#haskell",
"user": "7407f221-f839-4ebb-9184-000879870af1"
},
"type": "JoinedRoom"
}
}
Now whenever a client sends a message, both clients will receive it back.
A client can leave a room to stop receiving those messages:
{
"channel": "chat-room",
"message": {
"type": "LeaveRoom",
"contents": {
"room": "#haskell"
}
}
}
All subscribed clients will receive notice of their departure:
{
"channel": "chat-room",
"message": {
"contents": {
"room": "#haskell",
"user": "b5eda4bd-a8f2-4d6f-81c5-3da4c4333679"
},
"type": "LeftRoom"
}
}
BSD-3