Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TCP Key-Value Store Server

Production-style TCP key-value store in C implementing and benchmarking thread-per-connection and event-driven (select) architectures under concurrent workloads.

Tech Stack

  • Language: C
  • Networking: POSIX sockets (TCP/IP)
  • Concurrency: pthreads, select()
  • Synchronization: mutex locks
  • Systems: low-level systems programming, concurrent server design
  • Tools: GCC, Makefile, GDB, Valgrind

Key Highlights

  • Implemented both multi-threaded (pthreads) and event-driven (select) server architectures
  • Built a thread-safe in-memory key-value store with mutex synchronization
  • Designed a modular application-layer protocol supporting PING, ECHO, GET, SET, and DEL
  • Developed robust socket I/O with full-buffer guarantees for reliable communication
  • Benchmarked both models under 10-100 concurrent clients
  • Analyzed real-world trade-offs between thread-per-connection and event-loop designs

Why This Project

Modern backend systems must balance simplicity, scalability, and performance. This project implements and benchmarks two fundamental server architectures, thread-per-connection and event-driven I/O, to understand their real-world trade-offs in concurrency, resource usage, and system complexity.

It mirrors design decisions used in production systems such as web servers and distributed services.

Features

  • TCP client and server in C using POSIX sockets
  • Multi-threaded server using POSIX Threads (pthreads)
  • Single-threaded event-driven server using select()
  • Thread-safe shared in-memory key-value store
  • Modular protocol and socket utility layers
  • Runtime-configurable ports
  • Robust malformed input handling
  • Explicit rejection of oversized requests
  • Graceful shutdown on Ctrl+C with store cleanup
  • Robust socket I/O with full-buffer guarantees
  • Lightweight benchmark script for concurrency comparison

Supported Commands

  • PING
  • ECHO <message>
  • SET <key> <value>
  • GET <key>
  • DEL <key>

Project Structure

TCP-Server/
├── include/
│   ├── protocol.h
│   ├── store.h
│   └── utils.h
├── src/
│   ├── client.c
│   ├── protocol.c
│   ├── server_select.c
│   ├── server_threaded.c
│   ├── store.c
│   └── utils.c
├── scripts/
│   └── benchmark.sh
├── results/
│   ├── notes.md
│   ├── select_benchmark.txt
│   └── threaded_benchmark.txt
├── tests/
│   └── protocol_tests.c
├── Makefile
└── README.md

Build

Build the client, both server binaries, and the test runner:

make

Test

Run the regression tests:

make test

The test target covers protocol parsing, store operations, port validation, and request-reading edge cases such as oversized input.

Run

Start the multi-threaded server:

./server_threaded 8080

Start the event-driven select() server:

./server_select 8081

Use Ctrl+C to stop either server cleanly and release in-memory store resources.

Client Usage

Send requests using the client:

./client "PING" 8080
./client "ECHO hello" 8080
./client "SET name alice" 8080
./client "GET name" 8080
./client "DEL name" 8080

Test against the select() server by changing the port:

./client "PING" 8081
./client "SET city seattle" 8081
./client "GET city" 8081

Example Responses

./client "PING" 8080
Server response: PONG

./client "ECHO hello" 8080
Server response: hello

./client "SET name alice" 8080
Server response: OK

./client "GET name" 8080
Server response: VALUE alice

./client "DEL name" 8080
Server response: OK

./client "GET missing_key" 8080
Server response: NOT_FOUND

Request Flow

  1. Client sends a command over TCP (e.g., SET name alice)
  2. Server receives the request and passes it to the protocol layer (protocol.c)
  3. Protocol layer parses, validates, and dispatches the command
  4. Store layer performs thread-safe SET, GET, or DEL operations
  5. Response is formatted and written back to the client
  6. Connection is closed after one request

Architecture Diagram

flowchart TD
    A["Client"] --> B["TCP Socket"]
    B --> C["Server (threaded or select)"]
    C --> D["Protocol Layer"]
    D --> E["Store Layer (mutex-protected)"]
    E --> F["Response"]
    F --> A
Loading

Architecture Overview

Both server implementations share the same protocol and storage layers, enabling direct comparison of concurrency models without modifying application logic.

server_threaded

The threaded server creates one worker thread per client connection. This model is simple to implement and reason about, since each connection is handled independently, but it introduces thread creation, scheduling, and memory overhead as concurrency increases.

server_select

The event-driven server uses a single-threaded event loop with select() to monitor the listening socket and active client sockets. This avoids thread-per-connection overhead and demonstrates socket multiplexing, but increases implementation complexity and is limited by select() scalability constraints.

protocol.c

The protocol layer parses incoming requests, validates command formats, dispatches supported operations, and constructs responses.

store.c

The store module implements a shared in-memory key-value store. A mutex protects access to shared state, ensuring thread safety in the multi-threaded server.

utils.c

The utility layer provides reusable socket I/O helpers, including safe receive handling and a send_all function to guarantee complete transmission of responses.

Concurrency Models

Model Description Pros Cons
Threaded (pthreads) One thread per client connection Simple, intuitive, easy to extend Thread overhead, context switching, higher memory usage
Event-driven (select) Single-threaded socket multiplexing Lower per-connection overhead, efficient for many idle clients More complex control flow, limited scalability with select()

Benchmarking

A lightweight shell-based benchmark script is included to compare the multi-threaded server and the event-driven select() server under concurrent client workloads.

How to run

Start one server at a time:

./server_threaded 8080

or

./server_select 8081

Then run:

./scripts/benchmark.sh 8080 threaded
./scripts/benchmark.sh 8081 select

Workload

The benchmark launches multiple client processes concurrently and measures total elapsed time for representative commands such as:

  • PING
  • SET bench value
  • GET bench

Typical test sizes include:

  • 10 clients
  • 50 clients
  • 100 clients

Notes

  • Each client sends a single request per connection.
  • Ports must be valid integers in the range 1-65535.
  • Results may vary based on CPU performance, OS scheduling, and background system activity.
  • The benchmark script requires bash and python3.
  • This benchmark is intended as an engineering-focused performance comparison rather than a rigorous performance evaluation.

Summary

  • Most benchmark runs completed in a few hundredths of a second under 10-100 concurrent clients, with variability depending on workload and system state.
  • The relative winner can vary by run and workload, so the checked-in result files should be treated as current local snapshots rather than fixed conclusions about the two architectures.
  • Repeated runs still showed the expected architectural trade-off: select() avoids thread overhead, while the threaded design remains simpler to implement and competitive in lower-concurrency cases.
  • Demonstrated trade-offs between thread overhead and event-driven efficiency under concurrent workloads.
  • Treat these as machine-specific local measurements; repeat runs to smooth out scheduler noise and background activity and watch Failed clients alongside elapsed time.

Performance Highlights

  • Handled 10-100 concurrent clients with stable performance and zero failed requests in the checked-in benchmark runs
  • Completed benchmark workloads in roughly 20-65 ms per batch on this machine
  • Observed workload-dependent trade-offs between threaded and event-driven designs across repeated local runs

Design Trade-offs

  • The threaded server is simpler to implement and extend, but introduces thread creation and scheduling overhead as concurrency increases.
  • The select() server avoids per-connection thread overhead and demonstrates event-driven I/O multiplexing, but increases implementation complexity and is limited by select() scalability.
  • The linked-list key-value store favors simplicity over lookup performance; a hash table would improve efficiency at larger scale.
  • The current design uses one request per connection; supporting persistent connections would improve throughput.
  • These trade-offs mirror real production backend choices, such as thread-per-request versus event-loop architectures used in web servers.

When to Use Each Model

  • Use thread-per-connection when simplicity and maintainability are priorities, or when concurrency levels are moderate and blocking operations are common
  • Use event-driven (select) when handling many concurrent idle connections, or when minimizing thread and memory overhead is critical

Error Handling Improvements

The project includes robustness improvements such as:

  • Malformed command validation
  • Port validation for client and server startup
  • Oversized request detection with explicit error responses
  • Connection cleanup on client disconnect
  • Graceful shutdown with store cleanup on exit
  • Reliable full-buffer transmission via a send_all helper
  • Thread-safe shared state using mutex synchronization
  • Runtime-configurable ports for flexible local testing

Future Improvements

  • Add connection timeout handling
  • Support multiple requests per connection
  • Replace linked-list store with a hash table
  • Add structured logging
  • Extend benchmark coverage with more detailed metrics
  • Explore epoll for scalable event-driven I/O

Engineering Relevance

This project demonstrates core backend and systems engineering skills required in production environments, including concurrency models, network programming, and performance trade-off analysis. It also provides hands-on experience with design decisions used in real-world servers, such as thread-per-request versus event-driven architectures, making it directly relevant to backend and infrastructure roles.

Key takeaways include:

  • Low-level TCP networking
  • Concurrency with both threads and event-driven I/O
  • Synchronization of shared mutable state
  • Protocol design and command parsing
  • Modular system architecture
  • Performance trade-off analysis across concurrency models
  • Explores real-world trade-offs foundational to backend systems design and scalability
  • Used GDB and Valgrind during debugging to validate runtime behavior and memory safety
  • Threaded model aligns with traditional thread-per-request servers (e.g., Apache-style)
  • Event-driven model aligns with high-performance event-loop servers (e.g., Nginx, Node.js)
  • Demonstrates trade-offs in scalability, latency, and resource efficiency

Limitations

  • Uses select(), which does not scale to very high connection counts because of descriptor-set limits
  • Linked-list store has O(n) lookup complexity
  • Single-request-per-connection behavior limits throughput

Key Engineering Decisions

  • Chose a linked-list store for simplicity and clarity over lookup performance
  • Used mutex locking to preserve correctness under concurrent writes
  • Implemented one-request-per-connection to simplify protocol handling
  • Shared protocol and store layers across both server models for fair architectural comparison

About

Production-style TCP key-value store in C using POSIX sockets, supporting multi-threaded (pthreads) and event-driven (select) concurrency models with a thread-safe in-memory store.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages