Overview
The client management module orchestrates the complete lifecycle of each connected client, from initial connection through thread spawning, state synchronization, and graceful disconnection cleanup. It serves as the integration hub between the main server loop and all per-client subsystems, managing the sophisticated per-client threading architecture that enables real-time video chat at 60fps per client.
Implementation: src/server/client.c, src/server/client.h
Key Responsibilities:
- Client connection establishment and initialization
- Per-client thread creation and management (4 threads per client)
- Client state management with thread-safe access patterns
- Hash table management for O(1) client ID lookups
- Client disconnection handling and resource cleanup
- Integration point between main.c and other server modules
Client Lifecycle
Connection Establishment
When a new client connects:
{
int slot = find_available_client_slot();
uint32_t client_id = generate_client_id(client_port);
atomic_store(&client->
active,
true);
crypto_handshake_result_t handshake_result =
perform_crypto_handshake_server(socket, &client->crypto_ctx);
if (handshake_result != HANDSHAKE_SUCCESS) {
return -1;
}
create_client_receive_thread(client);
create_client_send_thread(client);
return client_id;
}
int create_client_render_threads(server_context_t *server_ctx, client_info_t *client)
Create and initialize per-client rendering threads.
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
int add_client(server_context_t *server_ctx, socket_t socket, const char *client_ip, int port)
int remove_client(server_context_t *server_ctx, uint32_t client_id)
Per-client state structure for server-side client management.
mutex_t client_state_mutex
client_info_t * clients_by_id
uthash head pointer for O(1) client_id -> client_info_t* lookups
client_info_t clients[MAX_CLIENTS]
Array of client_info_t structures (backing storage)
Thread Creation Order:
- Send Thread: Created first (cleanest exit, no blocking I/O typically)
- Receive Thread: Created second (handles incoming packets, may block on network)
- Video Render Thread: Created third (60fps frame generation)
- Audio Render Thread: Created fourth (172fps audio mixing)
Disconnection Handling
Client disconnection is detected and handled gracefully:
{
atomic_store(&client->
active,
false);
return 0;
}
void crypto_handshake_cleanup(crypto_handshake_context_t *ctx)
Cleanup crypto handshake context with secure memory wiping.
void packet_queue_destroy(packet_queue_t *queue)
Destroy a packet queue and free all resources.
void image_destroy(image_t *p)
Destroy an image allocated with image_new()
atomic_bool video_render_thread_running
asciichat_thread_t audio_render_thread
asciichat_thread_t send_thread
video_frame_buffer_t * incoming_video_buffer
atomic_bool audio_render_thread_running
asciichat_thread_t receive_thread
asciichat_thread_t video_render_thread
int client_count
Current number of active clients.
Cleanup Order:
- Mark inactive (threads exit loops)
- Join send thread (quickest, usually not blocked)
- Join receive thread (may be blocked on recv(), will timeout)
- Join render threads (computational work, clean exit)
- Cleanup resources (crypto, queues, buffers)
- Remove from hash table
- Clear slot for reuse
Per-Client Threading Architecture
Each connected client spawns exactly 4 dedicated threads:
Receive Thread
Purpose: Handle incoming packets from client Module: protocol.c functions called by receive thread Operations:
- Receives encrypted packets from socket
- Validates packet headers and CRC32 checksums
- Decrypts packets using per-client crypto context
- Dispatches to protocol handler functions based on packet type
- Stores media data (video frames, audio samples) in client buffers
- Updates client state (capabilities, dimensions, stream status)
Thread Safety:
- Uses per-client mutex for state updates
- Snapshot pattern for minimal lock holding time
- Thread-safe media buffers (double-buffer system)
Send Thread
Purpose: Manage outgoing packet delivery to client Module: client.c (send thread implementation) Operations:
- Reads packets from client's video and audio packet queues
- Encrypts packets using per-client crypto context
- Sends encrypted packets to client socket
- Handles chunked transmission for large packets
- Detects send failures and marks connection lost
Thread Safety:
- Packet queues are internally thread-safe
- Socket access protected by connection state checks
- Connection loss detected atomically
Video Render Thread
Purpose: Generate personalized ASCII frames at 60fps Module: render.c (video render thread implementation) Operations:
- Collects video frames from all active clients (via stream.c)
- Creates composite frame with appropriate grid layout
- Converts composite to ASCII using client's terminal capabilities
- Queues ASCII frame in client's video packet queue
- Rate limits to exactly 60fps with precise timing
Performance:
- 16.67ms intervals (60fps)
- Per-client frame generation (each client gets personalized view)
- Terminal capability awareness (color depth, palette, UTF-8 support)
- Linear scaling (no shared bottlenecks)
Audio Render Thread
Purpose: Mix audio streams at 172fps (excluding client's own audio) Module: render.c (audio render thread implementation) Operations:
- Reads audio samples from all clients via audio mixer
- Excludes client's own audio to prevent echo
- Mixes remaining audio streams with ducking and compression
- Queues mixed audio in client's audio packet queue
- Rate limits to exactly 172fps (5.8ms intervals)
Performance:
- 5.8ms intervals (172fps)
- Low-latency audio delivery
- Professional audio processing (ducking, compression, noise gate)
- Linear scaling (per-client dedicated threads)
Data Structures
Client Manager
Global singleton managing all clients:
typedef struct {
int client_count;
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Global client manager structure for server-side client coordination.
Synchronization:
- Reader-writer lock allows concurrent reads
- Exclusive write access for client add/remove
- Hash table provides O(1) client lookups
- Array provides O(1) slot access by index
Client Information
Per-client state structure:
typedef struct {
_Atomic bool active;
_Atomic bool video_render_thread_running;
_Atomic bool audio_render_thread_running;
crypto_handshake_ctx_t crypto_ctx;
_Atomic bool crypto_initialized;
unsigned short width;
unsigned short height;
struct client_info client_info_t
Per-client state structure for server-side client management.
Thread-safe packet queue for producer-consumer communication.
Lock-free ring buffer structure.
Complete terminal capabilities structure.
State Protection:
client_state_mutex: Protects most client state fields
video_buffer_mutex: Protects video frame buffer
- Atomic variables: Thread control flags (no mutex needed)
- Atomic snapshot pattern: Copy state under mutex, process without locks
Synchronization Patterns
Lock Ordering Protocol
CRITICAL RULE: Always acquire locks in this order:
g_client_manager_rwlock (global reader-writer lock)
client->client_state_mutex (per-client mutex)
- Specialized mutexes (
video_buffer_mutex, etc.)
This ordering prevents deadlocks across all server code.
Example:
Snapshot Pattern
All client state access uses the snapshot pattern to minimize lock contention:
unsigned short width_snapshot = client->
width;
unsigned short height_snapshot = client->
height;
if (should_continue) {
generate_frame(client_id_snapshot, width_snapshot, height_snapshot);
}
Benefits:
- Minimal lock holding time (reduces contention)
- No blocking operations while holding locks
- Prevents deadlocks from complex call chains
- Enables parallel processing across clients
Hash Table Integration
The client manager uses a hash table for O(1) client ID lookups:
Performance:
- O(1) average-case lookup time
- Handles hash collisions internally
- Thread-safe operations (protected by RWLock)
- Scales to 9+ clients efficiently
Client ID Generation:
- Based on client's local port number
- Ensures uniqueness across connections
- Provides stable identifier for session
Integration with Other Modules
Integration with main.c
Called By:
Provides To:
- Global client manager (
g_client_manager) for thread access
- Client lookup functions for other modules
- Thread lifecycle coordination
Integration with protocol.c
Called By:
Provides To:
- Client state access for protocol handlers
- Media buffer storage (video frames, audio samples)
- State update functions
Integration with render.c
Called By:
Provides To:
- Client state snapshots for render threads
- Media buffers for frame generation
- Terminal capabilities for ASCII conversion
Integration with stream.c
Called By:
- Render threads call stream generation functions
generate_personalized_ascii_frame(): Creates client-specific frames
Provides To:
- Client capabilities for frame generation
- Video frame access for mixing
- Frame queue for delivery
Error Handling
Connection Errors:
- Handshake failures: Client rejected, socket closed
- Thread creation failures: Client rejected, resources cleaned up
- Resource allocation failures: Graceful degradation, client rejected
Runtime Errors:
- Receive thread errors: Connection marked inactive, cleanup triggered
- Send thread errors: Connection marked inactive, cleanup triggered
- Render thread errors: Thread exits, connection continues (graceful degradation)
Cleanup Errors:
- Thread join timeouts: Logged but don't block shutdown
- Resource cleanup failures: Logged but don't crash server
- Hash table errors: Handled gracefully with fallback
Performance Characteristics
Linear Scaling:
- Each client gets dedicated CPU resources
- No shared bottlenecks between clients
- Performance scales linearly up to 9+ clients
- Real-time guarantees maintained per client
Memory Usage:
- Per-client allocations: ~50KB per client (buffers, queues)
- Hash table overhead: ~1KB per client
- Frame buffers: Variable based on terminal size
- Audio buffers: Fixed size ring buffers
Thread Overhead:
- 4 threads per client (receive, send, video render, audio render)
- Minimal context switching overhead
- CPU affinity possible for performance tuning
Best Practices
DO:
- Always use proper lock ordering (RWLock → per-client mutex)
- Use snapshot pattern for client state access
- Check
client->active before operations
- Join threads before resource cleanup
- Use hash table for client lookups (not array iteration)
DON'T:
- Don't acquire per-client mutex before global RWLock
- Don't hold locks during blocking operations
- Don't access client state without mutex protection
- Don't skip thread joins during cleanup
- Don't modify client array without write lock
- See also
- src/server/client.c
-
src/server/client.h
-
Server Overview
-
Server Main Entry Point
-
Protocol Handler
-
Render Threads
-
Concurrency Documentation