Before Rubén Pérez (@anarthal) started writing code for the BoostServerTech Chat project, he had to figure out how everything would communicate, including the browser to the server, the server to Redis, MySQL, and an in-memory broadcast system. As the author of Boost.MySQL and co-maintainer of Boost.Redis, he built this chat server as a case study in leveraging Boost libraries. The initial design work was focused on drawing boundaries between systems and deciding the message formats between them.
App Features
Before diving into what the API should contain, we should first ask: "What do we want to support?" There are many potential features, so focus is necessary. Ruben chose to start simple: users can create accounts and log in with usernames and passwords, and they can participate in group chats called rooms, which are currently static.
Two Protocols, One Server
Account creation and login are one-shot operations. The client sends a request and waits for a response, which is fine for HTTP. Chat messages are different; when someone types something in a room, every connected client needs to see it immediately. WebSockets provide a persistent connection where the server can push data. Thus, Rubén used this approach. The rule is simple: one-shot operations go over HTTP, while real-time interactions go over WebSocket.
The HTTP surface ended up small, with just two endpoints:
POST /api/create-accountfor self-registrationPOST /api/loginfor authentication
The WebSocket Protocol
WebSocket is just a bidirectional pipe, but a message format is still needed. Rubén went with a simple envelope: every message is a JSON object with a type field and a payload field. The type indicates what it is, while the payload carries the data. When a client opens a WebSocket connection, the server sends back a hello event containing everything the UI needs to render: the authenticated user, the room list, and recent message history for each room.
Example of a hello event
{ "type": "hello", "payload": { "me": { "id": 1, "username": "alice" }, "rooms": [ { "id": "beast", "name": "Boost.Beast", "messages": [ { "id": "1697312400000-0", "content": "Has anyone tried the new...", "user": { "id": 2, "username": "bob" }, "timestamp": 1697312400000 } ], "hasMoreMessages": true } ] } }
Real-time Message Broadcasting
clientMessages: sent by the client when the user hits send, carrying a room ID and an array of message objects.serverMessages: the broadcast sent by the server when anyone sends a message. The server persists it and pushesserverMessagesto every connected client in that room, including the sender.
WebSocket: clientMessages
{ "type": "clientMessages", "payload": { "roomId": "beast", "messages": [ { "content": "This is my message" } ] } }
WebSocket: serverMessages
{ "type": "serverMessages", "payload": { "roomId": "beast", "messages": [ { "id": "1697312500000-0", "content": "This is my message", "user": { "id": 1, "username": "alice" }, "timestamp": 1697312500000 } ] } }
Room History
The hello event contains only the most recent messages for each room for efficiency. Clients may request older messages using requestRoomHistory messages.
WebSocket: requestRoomHistory
{ "type": "requestRoomHistory", "payload": { "roomId": "beast", "firstMessageId": "1697312400000-0" } }
The HTTP API
The HTTP API handles authentication. The server generates a session ID stored in Redis upon successful login, and the client stores this ID in a cookie for subsequent requests.
HTTP: Create Account Request
{ "username": "alice", "email": "alice@example.com", "password": "hunter2" }
Backend Systems
Three systems manage different types of data: MySQL for user data, Redis for messages, and an in-memory pub/sub system for broadcasting.
Conclusion
Rubén chose to split things this way for better management and flexibility. The plan to offload old messages from Redis to MySQL for archival purposes only touches the message layer, leaving everything else unchanged.
Blogger's Review: This article delves into the design philosophy of the chat server, particularly in constructing the interface layer. By appropriately segmenting backend systems, Rubén demonstrates how to optimize data storage and access patterns, ensuring the system's efficiency and scalability.