Project 3: Unix Domain Socket Chat

Build a local chat server and client using AF_UNIX sockets with message framing and permissions.

Quick Reference

Attribute Value
Difficulty Level 2: Intermediate
Time Estimate 3-5 days
Main Programming Language C (Alternatives: Rust, Go, Python)
Alternative Programming Languages Rust, Go, Python
Coolness Level Level 2: IPC Apprentice
Business Potential 1: The “Local Utility”
Prerequisites basic sockets, file permissions, simple protocol design
Key Topics AF_UNIX sockets, message framing, client management

1. Learning Objectives

By completing this project, you will:

  1. Build a working implementation of unix domain socket chat and verify it with deterministic outputs.
  2. Explain the underlying Unix and terminal primitives involved in the project.
  3. Diagnose common failure modes with logs and targeted tests.
  4. Extend the project with performance and usability improvements.

2. All Theory Needed (Per-Concept Breakdown)

Unix Domain Socket IPC with Framing and Permissions

  • Fundamentals Unix Domain Socket IPC with Framing and Permissions is the core contract that makes the project behave like a real terminal tool. It sits at the boundary between raw bytes and structured state, so you must treat it as both a protocol and a data model. The goal of the fundamentals is to understand what assumptions the system makes about ordering, buffering, and ownership, and how those assumptions surface as user-visible behavior. Key terms include: AF_UNIX, SOCK_STREAM, filesystem permissions, framing, unlink. In practice, the fastest way to gain intuition is to trace a single input through the pipeline and note where it can be delayed, reordered, or transformed. That exercise reveals why Unix Domain Socket IPC with Framing and Permissions needs explicit invariants and why even small mistakes can cascade into broken rendering or stuck input.

  • Deep Dive into the concept A deep understanding of Unix Domain Socket IPC with Framing and Permissions requires thinking in terms of state transitions and invariants. You are not just implementing functions; you are enforcing a contract between producers and consumers of bytes, and that contract persists across time. Most failures in this area are caused by violating ordering guarantees, dropping state updates, or misunderstanding how the operating system delivers events. This concept is built from the following pillars: AF_UNIX, SOCK_STREAM, filesystem permissions, framing, unlink. A reliable implementation follows a deterministic flow: Create socket(AF_UNIX, SOCK_STREAM). -> Unlink existing socket path and bind(). -> chmod socket path to 0600. -> accept() clients and add to poll set. -> Frame messages with length prefix.. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. When you design the project, treat each key term as a source of constraints. For example, if a term implies buffering, decide the buffer size and how overflow is handled. If a term implies state, decide how that state is initialized, updated, and reset. Finally, validate your assumptions with deterministic fixtures so you can reproduce bugs. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness. From a systems perspective, the tricky part is coordinating concurrency without introducing races. Even in a single-threaded loop, multiple events can arrive in the same tick, so you need deterministic ordering. This is why many implementations keep a strict sequence: read, update state, compute diff, render. Another subtlety is error handling and recovery. A robust design treats errors as part of the normal control flow: EOF is expected, partial reads are expected, and transient failures must be retried or gracefully handled. The deep dive should also cover how to observe the system, because without logs and trace points, you cannot reason about correctness.

  • How this fit on projects This concept is the backbone of the project because it defines how data and control flow move through the system.

  • Definitions & key terms

    • AF_UNIX -> local socket address family using filesystem paths
    • SOCK_STREAM -> byte-stream socket type with ordered, reliable delivery
    • filesystem permissions -> mode bits that control who can connect to a Unix socket path
    • framing -> protocol technique that defines message boundaries on a byte stream
    • unlink -> filesystem operation to remove a pre-existing socket path before bind
  • Mental model diagram (ASCII)

[Input] -> [Unix Domain Socket IPC with Framing and Permissions] -> [State] -> [Output]
  • How it works (step-by-step, with invariants and failure modes)

    1. Create socket(AF_UNIX, SOCK_STREAM).
    2. Unlink existing socket path and bind().
    3. chmod socket path to 0600.
    4. accept() clients and add to poll set.
    5. Frame messages with length prefix.
  • Minimal concrete example

    struct sockaddr_un addr = {0};
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/chat.sock");
bind(fd, (struct sockaddr*)&addr, sizeof(addr));
  • Common misconceptions

    • “Unix sockets are always secure” -> permissions decide who can connect.
    • “Stream sockets preserve messages” -> they do not; you must frame.
  • Check-your-understanding questions

    • Why does tmux use Unix sockets instead of TCP?
    • What happens if a client disconnects mid-frame?
    • How do permissions protect against other users?
  • Check-your-understanding answers

    • Unix sockets are local and fast, with filesystem-based access control.
    • You must discard partial frames and close the client.
    • Socket path permissions restrict access to the owner.
  • Real-world applications

    • tmux server sockets
    • local daemon control sockets
  • Where you’ll apply it

  • References

    • The Linux Programming Interface - Ch. 57
    • UNIX Network Programming - Ch. 4
  • Key insights Unix Domain Socket IPC with Framing and Permissions works best when you treat it as a stateful contract with explicit invariants.

  • Summary You now have a concrete mental model for Unix Domain Socket IPC with Framing and Permissions and can explain how it affects correctness and usability.

  • Homework/Exercises to practice the concept

    • Write a tiny echo server with AF_UNIX.
    • Add length-prefixed framing and test with partial writes.
  • Solutions to the homework/exercises

    • Use poll() with a client list.
    • Implement a small buffer per client to reassemble frames.

3. Project Specification

3.1 What You Will Build

A server daemon that accepts multiple local clients over a Unix socket and a CLI client that can send and receive framed chat messages.

3.2 Functional Requirements

  1. Requirement 1: Create a Unix domain socket and listen on a filesystem path.
  2. Requirement 2: Accept multiple clients and broadcast messages.
  3. Requirement 3: Frame messages with length prefixes to avoid boundary ambiguity.
  4. Requirement 4: Handle client disconnects and cleanup resources.
  5. Requirement 5: Enforce socket permissions (0600) on startup.

3.3 Non-Functional Requirements

  • Performance: Avoid blocking I/O; batch writes when possible.
  • Reliability: Handle partial reads/writes and cleanly recover from disconnects.
  • Usability: Provide clear CLI errors, deterministic output, and helpful logs.

3.4 Example Usage / Output

    $ ./uds_chatd --socket /tmp/chat.sock
[server] listening on /tmp/chat.sock

$ ./uds_chat --socket /tmp/chat.sock
chat> hello
[server] user1: hello
[exit code: 0]

$ ./uds_chatd --socket /tmp/chat.sock
[error] socket path already exists
[exit code: 1]

3.5 Data Formats / Schemas / Protocols

    Message frame:
- uint32 length (network byte order)
- UTF-8 payload bytes

3.6 Edge Cases

  • Socket path already exists.
  • Client disconnects mid-frame.
  • Message larger than max frame size.

3.7 Real World Outcome

This section defines a deterministic, repeatable outcome. Use fixed inputs and set TZ=UTC where time appears.

3.7.1 How to Run (Copy/Paste)

make
./uds_chatd --socket /tmp/chat.sock

3.7.2 Golden Path Demo (Deterministic)

The “success” demo below is a fixed scenario with a known outcome. It should always match.

3.7.3 If CLI: provide an exact terminal transcript

    $ ./uds_chatd --socket /tmp/chat.sock
[server] listening on /tmp/chat.sock

$ ./uds_chat --socket /tmp/chat.sock
chat> hello
[server] user1: hello
[exit code: 0]

Failure Demo (Deterministic)

    $ ./uds_chatd --socket /tmp/chat.sock
[error] socket path already exists
[exit code: 1]

3.7.8 If TUI

At least one ASCII layout for the UI:

    +------------------------------+
    | Unix Domain Socket Chat           |
    | [content area]               |
    | [status / hints]             |
    +------------------------------+

4. Solution Architecture

4.1 High-Level Design

    +-----------+     +-----------+     +-----------+
    |  Client   | <-> |  Server   | <-> |  PTYs     |
    +-----------+     +-----------+     +-----------+

4.2 Key Components

| Component | Responsibility | Key Decisions | |-----------|----------------|---------------| | Server | Accepts clients and broadcasts messages. | Use poll to multiplex clients. | | Client | Connects, sends input, prints broadcasts. | Use separate thread or poll for stdin/socket. | | Protocol | Defines length-prefixed frames. | Keep framing simple for reliability. |

4.4 Data Structures (No Full Code)

    struct Frame {
    uint32_t len;
    uint8_t  payload[4096];
};

4.4 Algorithm Overview

Key Algorithm: Length-prefixed framing

  1. Read 4-byte length header.
  2. Validate length <= max.
  3. Read exactly length bytes.
  4. Broadcast payload to all clients.

Complexity Analysis:

  • Time O(n) per message; Space O(max_frame).

5. Implementation Guide

5.1 Development Environment Setup

    cc --version
make --version

5.2 Project Structure

    uds-chat/
|-- src/
|   |-- server.c
|   |-- client.c
|   `-- protocol.c
|-- include/
|   `-- protocol.h
`-- Makefile

5.3 The Core Question You’re Answering

“How can two local processes communicate reliably without TCP?”

5.4 Concepts You Must Understand First

  1. AF_UNIX sockets
    • Why it matters and how it impacts correctness.
  2. message framing
    • Why it matters and how it impacts correctness.
  3. client management
    • Why it matters and how it impacts correctness.

5.5 Questions to Guide Your Design

  • Will you use stream or datagram sockets?
  • How will you handle framing and max message size?
  • How do you clean up the socket file on crash?

    5.6 Thinking Exercise

Design a header that includes version, length, and message type.

5.7 The Interview Questions They’ll Ask

  • Why prefer Unix sockets for tmux IPC?
  • How do you avoid path length issues with AF_UNIX?

    5.8 Hints in Layers

  • Use SOCK_STREAM for ordered delivery.

5.9 Books That Will Help

| Topic | Book | Chapter | |——-|——|———| | Unix sockets | The Linux Programming Interface | Ch. 57 | | Framing | UNIX Network Programming | Ch. 4 |

5.10 Implementation Phases

Phase 1: Foundation (3-5 days)

Goals:

  • Establish the core data structures and loop.
  • Prove basic I/O or rendering works.

Tasks:

  1. Implement the core structs and minimal main loop.
  2. Add logging for key events and errors.

Checkpoint: You can run the tool and see deterministic output.

Phase 2: Core Functionality (3-5 days)

Goals:

  • Implement the main requirements and pass basic tests.
  • Integrate with OS primitives.

Tasks:

  1. Implement remaining functional requirements.
  2. Add error handling and deterministic test fixtures.

Checkpoint: All functional requirements are met for the golden path.

Phase 3: Polish & Edge Cases (3-5 days)

Goals:

  • Handle edge cases and improve UX.
  • Optimize rendering or I/O.

Tasks:

  1. Add edge-case handling and exit codes.
  2. Improve logs and documentation.

Checkpoint: Failure demos behave exactly as specified.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
I/O model blocking vs non-blocking non-blocking avoids stalls in multiplexed loops
Logging text vs binary text for v1 easier to inspect and debug

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Validate components parser, buffer, protocol
Integration Tests Validate interactions end-to-end CLI flow
Edge Case Tests Handle boundary conditions resize, invalid input

6.2 Critical Test Cases

  1. Two clients receive broadcast message.
  2. Server rejects messages above max size.
  3. Socket permissions are 0600.

    6.3 Test Data

text Send "hello" from client A; expect client B to receive it verbatim.


7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |———|———|———-| | bind() fails | Socket path already exists | unlink() before bind. | | Clients hang | No framing leads to stuck reads | Add length prefix. | | Permissions too open | Socket accessible to other users | chmod to 0600. |

7.2 Debugging Strategies

  • Use lsof to inspect socket file ownership.
  • Print frame boundaries to logs.

    7.3 Performance Traps

  • Broadcasting synchronously to slow clients can block the server.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add usernames and join/leave messages.
  • Add /quit command.

    8.2 Intermediate Extensions

  • Add message types (system vs user).
  • Add server-side history.

    8.3 Advanced Extensions

  • Add authentication token handshake.
  • Add optional encryption for payloads.

9. Real-World Connections

9.1 Industry Applications

  • Local control sockets for daemons
  • IPC for desktop apps
  • tmux server
  • systemd notify sockets

    9.3 Interview Relevance

  • Event loops, terminal I/O, and state machines are common interview topics.

10. Resources

10.1 Essential Reading

  • The Linux Programming Interface by Michael Kerrisk - Ch. 57
  • UNIX Network Programming by Stevens et al. - Ch. 4

    10.2 Video Resources

  • Unix domain sockets explained (lecture).

    10.3 Tools & Documentation

  • socat (test sockets): socat (test sockets)
  • lsof (inspect open sockets): lsof (inspect open sockets)
  • Project 2: ANSI Escape Sequence Renderer - Builds prerequisites
  • Project 4: Signal-Aware Process Supervisor - Extends these ideas

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain the core concept without notes
  • I can explain how input becomes output in this tool
  • I can explain the main failure modes

11.2 Implementation

  • All functional requirements are met
  • All test cases pass
  • Code is clean and well-documented
  • Edge cases are handled

11.3 Growth

  • I can identify one thing I’d do differently next time
  • I’ve documented lessons learned
  • I can explain this project in a job interview

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Tool runs and passes the golden-path demo
  • Deterministic output matches expected snapshot
  • Failure demo returns the correct exit code

Full Completion:

  • All minimum criteria plus:
  • Edge cases handled and tested
  • Documentation covers usage and troubleshooting

Excellence (Going Above & Beyond):

  • Add at least one advanced extension
  • Provide a performance profile and improvement notes