Case Study 1: Building a Chat Application
Overview
In this case study, we build a TCP-based chat system with a server that relays messages between connected clients. This demonstrates persistent connections, a text-based protocol, and the challenges of managing multiple simultaneous clients — a preview of the threading concepts covered in Chapter 36.
Problem Statement
Build a chat system where:
- A server listens on a configurable port
- Multiple clients can connect and join a chat room
- When a client sends a message, the server broadcasts it to all other connected clients
- Clients can set a nickname with
/nick <name> - The server announces when clients join and leave
- The server displays a log of all messages
Protocol Design
Client → Server:
/nick Rosa Set nickname to "Rosa"
Hello everyone! Send a message
/quit Disconnect
Server → Client:
[Server] Welcome! Use /nick <name> to set your nickname.
[Server] Rosa has joined the chat.
[Rosa] Hello everyone!
[Server] Tomas has left the chat.
All messages are single lines terminated by \r\n. The server prefixes each broadcast with the sender's nickname in square brackets.
The Server
The server maintains a list of connected clients, each with a nickname and a socket stream:
type
TChatClient = record
Stream: TSocketStream;
Nickname: string;
Active: Boolean;
end;
var
Clients: array[0..31] of TChatClient;
ClientCount: Integer;
procedure BroadcastMessage(const Msg: string; ExcludeIndex: Integer = -1);
var
I: Integer;
Line: string;
begin
Line := Msg + #13#10;
for I := 0 to ClientCount - 1 do
if Clients[I].Active and (I <> ExcludeIndex) then
begin
try
Clients[I].Stream.Write(Line[1], Length(Line));
except
{ Client disconnected — mark as inactive }
Clients[I].Active := False;
end;
end;
end;
procedure HandleClient(Index: Integer);
var
Buffer: array[0..1023] of Byte;
BytesRead: Integer;
Msg, Trimmed: string;
begin
{ Send welcome message }
BroadcastMessage('[Server] ' + Clients[Index].Nickname + ' has joined the chat.');
while Clients[Index].Active do
begin
BytesRead := Clients[Index].Stream.Read(Buffer, SizeOf(Buffer));
if BytesRead <= 0 then Break;
SetString(Msg, PChar(@Buffer[0]), BytesRead);
Trimmed := Trim(Msg);
if Trimmed = '' then Continue;
{ Handle commands }
if Copy(Trimmed, 1, 6) = '/nick ' then
begin
Clients[Index].Nickname := Copy(Trimmed, 7, Length(Trimmed));
BroadcastMessage('[Server] Nickname changed to ' + Clients[Index].Nickname);
end
else if Trimmed = '/quit' then
Break
else
begin
{ Broadcast the message }
WriteLn(Clients[Index].Nickname, ': ', Trimmed);
BroadcastMessage('[' + Clients[Index].Nickname + '] ' + Trimmed, Index);
end;
end;
{ Client disconnected }
Clients[Index].Active := False;
BroadcastMessage('[Server] ' + Clients[Index].Nickname + ' has left the chat.');
end;
The Client
The client is simpler — it reads user input and sends it to the server, while displaying messages received from the server:
procedure RunChatClient(const Host: string; Port: Word);
var
Socket: TInetSocket;
UserInput: string;
Buffer: array[0..4095] of Byte;
BytesRead: Integer;
begin
Socket := TInetSocket.Create(Host, Port);
try
WriteLn('Connected to chat server.');
WriteLn('Commands: /nick <name>, /quit');
WriteLn;
{ Note: A real client would use threads or select()
to read from the socket and stdin simultaneously.
This simplified version alternates between sending
and receiving. }
repeat
{ Check for incoming messages }
{ In practice, this needs non-blocking I/O or a thread }
Write('You: ');
ReadLn(UserInput);
if UserInput = '' then Continue;
UserInput := UserInput + #13#10;
Socket.Write(UserInput[1], Length(UserInput));
until Trim(UserInput) = '/quit'#13#10;
finally
Socket.Free;
end;
end;
Key Challenge: Simultaneous Read and Write
The fundamental challenge of a chat client is that it needs to do two things at once:
- Read user input from the keyboard (blocking)
- Read messages from the server socket (also blocking)
A single thread cannot block on both simultaneously. Solutions:
- Two threads: One reads the keyboard, one reads the socket (Chapter 36)
- Non-blocking I/O: Set the socket to non-blocking mode and poll it between keyboard reads
- Select/poll: Use
fpSelectto wait on multiple file descriptors simultaneously
The server has the same problem — it needs to listen for new connections while handling existing clients. The standard solution is one thread per client (Chapter 36).
Lessons Learned
-
Protocol design comes first. Define the message format before writing code. A well-designed protocol makes the implementation straightforward.
-
Blocking I/O limits concurrency. A single thread can block on only one operation. Real chat applications require threading or asynchronous I/O.
-
Clients disconnect unexpectedly. Network connections can drop at any time. The server must handle write failures gracefully (mark clients as inactive rather than crashing).
-
Broadcast is O(n). Sending a message to all clients requires iterating the client list. For large-scale chat, this becomes a bottleneck that requires more sophisticated architectures (message queues, pub/sub).
-
Text protocols are debuggable. Because our protocol is plain text, we can test the server using
telnetornetcatwithout writing a client at all. This is invaluable during development.