Skip to content

Man1ac-1773/lanmsg

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 

Repository files navigation

lanmsg — A Terminal LAN Messenger

A multithreaded, terminal-native LAN chat application written in C.
Clients connect over TCP and communicate through a central server. The interface is rendered entirely using ANSI/VT100 escape sequences — no external UI libraries, no ncurses.

Table of Contents

Overview

lanmsg is a prototype LAN messenger built as a systems programming exercise. It demonstrates:

  • TCP socket programming with a custom binary protocol
  • Multithreaded server using POSIX threads and mutex-protected shared state
  • A concurrent client that reads from the network while accepting keyboard input simultaneously
  • A live terminal UI built with raw ANSI escape codes, including scroll region management and terminal resize handling

The codebase is intentionally minimal: three files, no dependencies beyond the C standard library and POSIX.

Architecture

┌──────────────────────────────────────────────────────────┐
│                         SERVER                           │
│                                                          │
│   main() accept loop                                     │
│        │                                                 │
│        ├──► clients[MAX_CLIENTS]  (shared, mutex-guarded)│
│        │                                                 │
│        └──► handle_client() thread (one per connection)  │
│                  │                                       │
│                  ├── MSG_CHAT  → send_to_all()           │
│                  ├── MSG_PRIV  → send_private()          │
│                  ├── MSG_LIST  → enumerate clients[]     │
│                  └── MSG_EXIT  → mark inactive, broadcast│
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│                         CLIENT                           │
│                                                          │
│   main thread          recieve_messages() thread         │
│   (stdin → send)       (recv → terminal output)          │
│        │                        │                        │
│        └────── server_fd ───────┘                        │
└──────────────────────────────────────────────────────────┘

Server: Single-process, multi-threaded. One thread per connected client, all sharing a clients[] array guarded by a single pthread_mutex_t. The main thread does nothing after spawning — it loops on accept().

Client: Two threads share one socket. The main thread owns stdin and sends packets; the receiver thread blocks on recv() and writes incoming messages to the terminal. Cursor state is preserved across both threads using ANSI save/restore sequences (\e7 / \e8), which acts as a crude form of output synchronization.

Wire Protocol

All communication uses a single fixed-size packet type defined in common.h:

typedef struct packet {
    enum msg_type type;
    char username[NAME_SIZE];  // 32 bytes — sender name
    char target[NAME_SIZE];    // 32 bytes — recipient (private messages only)
    size_t payload_len;
    char message[SIZE];        // 256 bytes — message body
} chat_packet;

Total packet size is constant regardless of message length. This simplifies framing entirely — recv(..., MSG_WAITALL) on the client side always reads exactly sizeof(chat_packet) bytes.

Message Types

Type Direction Description
MSG_CONNECT Client → Server Initial handshake; carries the username
MSG_CHAT Bidirectional Broadcast message to all connected clients
MSG_PRIV Client → Server Private message; target field names recipient
MSG_LIST Client → Server Request list of active users
MSG_EXIT Client → Server Graceful disconnect signal
MSG_SERVER_ACKNW Server → Client Acknowledgement of successful handshake
MSG_SERVER_ERR Server → Client Error response (e.g. unknown private target)
MSG_SERVER_REJ Server → Client Rejection of connection due to server at max capacity.

Connection Lifecycle

Client                          Server
  │                               │
  │──── MSG_CONNECT (username) ──►│   add_client(), log name
  │◄─── MSG_SERVER_ACKNW ─────────│
  │                               │
  │  [chat loop]                  │
  │                               │
  │──── MSG_EXIT ─────────────────►│  broadcast exit, mark inactive
  │                               │

When the server is full, a client is rejected by the server after they enter a name. This is because the listener is not active before the name is sent over to the server (intentional architecture), and response of rejection is only received once listener becomes active. It prints the rejection message and immediately exits the program.

Terminal UI Design

The client uses raw ANSI/VT100 escape sequences to build a split terminal layout without ncurses.

A screenshot of the chat from a client perspective. A screenshot of the chat from a client perspective

Scroll region (\e[4;%dr): Instructs the terminal to only scroll rows 4 through terminal_h - 2. The header and input line are outside this region and are never overwritten by scrolling.

Cursor save/restore (\e7 / \e8): Before printing any incoming message, the receiver thread saves the cursor position, moves to the bottom of the scroll region, prints, then restores. This allows the input prompt to remain visually stable while messages arrive.

SIGWINCH: The client installs a handler for SIGWINCH (terminal resize). On resize, it re-queries terminal dimensions via ioctl(STDOUT_FILENO, TIOCGWINSZ, &w), redraws the full UI, and repositions the cursor. fgets is interrupted by the signal; errno == EINTR is checked to resume cleanly.

A screenshot showing server running Server Logs

Building

Requirements: GCC or Clang, POSIX-compliant system (Linux, macOS). Run these commands from project directory

# Compile server
gcc src/server.c -o server -lpthread

# Compile client
gcc src/client.c -o client -lpthread

Usage

Start the server:

./server           # listens on 0.0.0.0:8080 (default)
./server 9090      # listens on a custom port

Connect a client:

./client                        # connects to 127.0.0.1:8080
./client 192.168.1.10           # custom IP, default port
./client 192.168.1.10 9090      # custom IP and port

Ports allowed for connection on either client or server are in the range (1024-49151).

On connect, the client prompts for a username (1–31 characters). The client does not listen for incoming messages yet. After entry, the split-pane UI is drawn and the session begins.

Commands

Command Description
/msg <username> <message> Send a private message to a user
/list List all currently connected users
/quit Disconnect gracefully
(any other input) Broadcast message to all clients

Known Bugs & Limitations

Buffer overflow in /list:
The server builds the user list by concatenating names into the 256-byte message field using strcat with no bounds check. With MAX_CLIENTS=10 and NAME_SIZE=32, worst-case output is approximately 330 bytes — exceeding the buffer. Fix: track written length and truncate, or use snprintf with remaining capacity.

No name uniqueness check: Server does not enforce a name uniqueness policy, multiple clients with same name can exist simultaneously, a severe limitation in security.

MSG_EXIT broadcasts to the sender:
send_to_all(payload, -1) uses -1 as the exclude-fd, which never matches any real file descriptor. The departing client therefore receives their own exit notification before the connection closes. Does not happen in practice, since listener thread does not receive a reply from server before the program exits.

No mutual exclusion between output threads:
The main thread and receiver thread both write to stdout. The cursor save/restore mechanism provides visual coherence but is not atomic — a context switch between SAVE_CUR and the subsequent MOVE_TO print can corrupt the display under high message load.

Future Improvements

Message Timestamps

Every received message should carry a timestamp. This requires either adding a time_t field to chat_packet (set by the sender) or having the server stamp packets on receipt. Server-side stamping is preferable as it gives a consistent ordering. Display as [HH:MM] prefix in the scroll region.

Server-side Name Enforcement

Currently, the username is set by the client and trusted entirely. There is no server-side enforcement of uniqueness. Two clients can connect with the same name, making private messages ambiguous and the user list misleading. The server should reject MSG_CONNECT with a duplicate name and return a new error type (e.g. MSG_NAME_TAKEN).

Message History / Scrollback

The scroll region discards messages once they scroll past the top of the terminal. A proper scrollback buffer (ring buffer of N recent packets, replayed on connect or on a /history command) would significantly improve usability.

End-to-End Encryption

All traffic is currently plaintext. For a LAN context this is acceptable, but adding TLS via OpenSSL or a simple symmetric key exchange (e.g. X25519 + ChaCha20-Poly1305) would make the tool viable on untrusted networks.

Graceful Server Shutdown

The server has no shutdown path. SIGINT abruptly closes the listening socket without notifying connected clients. A SIGINT handler should send a MSG_CHAT broadcast from "SERVER" announcing shutdown, then close all client file descriptors before exiting.

Configurable MAX_CLIENTS

The client cap is a compile-time constant. Making it a runtime argument (or dynamically growing the clients array) would remove an arbitrary restriction.

Output Thread Synchronization

The race condition between the main thread's echo output and the receiver thread's incoming message display in client.c should be resolved with a proper output mutex rather than relying on ANSI save/restore as a substitute for synchronization.

Reconnection Support

If the server restarts, clients have no mechanism to reconnect — exit(0) is called immediately. A reconnection loop with exponential backoff would make the client more resilient.

Windows / non-VT100 Terminal Support

ANSI escape sequences are not supported on the Windows console without explicit VT processing enabled. Abstracting terminal output behind a thin compatibility layer (or detecting and falling back gracefully) would widen platform support.

File Structure

.
├── common.h    — shared protocol definitions (packet struct, enums, constants)
├── server.c    — multi-threaded TCP server
└── client.c    — terminal client with split-pane ANSI UI

About

A LAN chat application written in C, with no external dependencies.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages