Course Navigation
← Back to Course OverviewAll Lessons
✓
Introduction and Database Fundamentals ✓
Building the Core Data Structure ✓
Concurrency and Thread Safety ✓
Append-Only Log (Write-Ahead Log) ✓
SSTables and LSM Trees ✓
Compaction and Optimization 7
TCP Server and Protocol Design 8
Client Library and Advanced Networking 9
Transactions and ACID Properties 10
Replication and High Availability 11
Monitoring, Metrics, and Observability 12
Performance Optimization and Tuning 13
Configuration and Deployment 14
Security and Production Hardening 15
Final Project and Beyond Current Lesson
7 of 15
Progress 47%
TCP Server and Protocol Design
Learning Objectives
- • Master network protocol design principles
- • Implement RESP (Redis Protocol) from scratch
- • Build a production-ready TCP server in Go
- • Handle concurrent connections efficiently
- • Parse and validate network requests
Lesson 7.1: Network Protocol Design
Text-based vs Binary Protocols
When designing a database protocol, you must choose between text and binary approaches. Each has distinct advantages and trade-offs.
Text-based Protocol (Redis Protocol - RESP)
Example: SET user:1 alice
Text format (human-readable):
*3\r\n # Array of 3 elements
$3\r\n # First element: 3 bytes
SET\r\n # Command
$6\r\n # Second element: 6 bytes
user:1\r\n # Key
$5\r\n # Third element: 5 bytes
alice\r\n # Value
Advantages:
- Human-readable for debugging
- Easy to test with telnet/nc
- Text editors can read/write
- Language-agnostic
Disadvantages:
- Larger wire size (more bytes)
- Parsing requires careful handling
- Line breaks matter Binary Protocol (Protocol Buffers, MessagePack)
Example: SET user:1 alice
Binary format (compact):
01 # Op: SET
06 # Key length: 6
75 73 65 72 3A 31 # "user:1"
05 # Value length: 5
61 6C 69 63 65 # "alice"
Advantages:
- Compact representation
- Faster parsing
- Type safety
- Smaller wire size
Disadvantages:
- Not human-readable
- Requires code generation
- Harder to debug Decision for our course: Use RESP (Redis Serialization Protocol)
- • Text-based (easier debugging)
- • Well-defined standard
- • Widely supported in clients
- • Simpler to implement correctly
RESP Protocol Specification
RESP has multiple data types for different use cases:
Simple String: +OK\r\n
Error: -ERR key not found\r\n
Integer: :42\r\n
Bulk String: $6\r\nhello!\r\n
Array: *2\r\n$3\r\nGET\r\n$4\r\nkey1\r\n
Null Bulk String: $-1\r\n Simple Strings
Format: +<string>\r\n
Examples:
+OK\r\n # Success response
+PONG\r\n # Ping response Errors
Format: -<error message>\r\n
Examples:
-ERR unknown command\r\n
-ERR key not found\r\n
-WRONGTYPE Operation against a key holding wrong kind of value\r\n Integers
Format: :<integer>\r\n
Examples:
:1000\r\n # Response to incr
:0\r\n # False/no
:1\r\n # True/yes Bulk Strings
Format: $<length>\r\n<data>\r\n
Examples:
$6\r\nfoobar\r\n # 6-byte string "foobar"
$-1\r\n # Null (key not found)
$0\r\n\r\n # Empty string Arrays
Format: *<count>\r\n<element1>...<elementN>
Examples:
*2\r\n
$4\r\nLLEN\r\n
$6\r\nmylist\r\n
*3\r\n
$3\r\nSET\r\n
$3\r\nkey\r\n
$5\r\nvalue\r\n Complete RESP Example
Client sends:
*3\r\n
$3\r\nSET\r\n
$6\r\nuser:1\r\n
$5\r\nalice\r\n
Server responds:
+OK\r\n
Client sends:
*2\r\n
$3\r\nGET\r\n
$6\r\nuser:1\r\n
Server responds:
$5\r\nalice\r\n Request-Response Patterns
Synchronous Request-Response
Client connects:
└─ TCP connection established
Client sends request:
GET key\r\n
Server processes:
└─ Reads key
Server sends response:
$5\r\nvalue\r\n
Pattern repeats for next request Pipelining
Client sends multiple requests without waiting:
*2\r\n$3\r\nGET\r\n$2\r\nk1\r\n
*2\r\n$3\r\nGET\r\n$2\r\nk2\r\n
*2\r\n$3\r\nGET\r\n$2\r\nk3\r\n
Server processes and sends responses in order:
$2\r\nv1\r\n
$2\r\nv2\r\n
$2\r\nv3\r\n
Benefits:
- Reduces round-trip latency
- Increases throughput
- Better network utilization Lesson 7.2: TCP Server in Go
net Package Fundamentals
Go's `net` package provides low-level networking primitives for building servers and clients.
import "net"
// Listen creates TCP listener
listener, err := net.Listen("tcp", ":6379")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
// Accept connections
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("accept error: %v", err)
continue
}
// Handle connection
go handleConnection(conn)
}
func handleConnection(conn net.Conn) {
defer conn.Close()
// Read from connection
buf := make([]byte, 1024)
n, err := conn.Read(buf)
// Write to connection
conn.Write([]byte("response"))
} Connection Handling Patterns
Goroutine-Per-Connection Model
type Server struct {
listener net.Listener
done chan struct{}
}
func (s *Server) Start(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
s.listener = listener
go s.acceptLoop()
return nil
}
func (s *Server) acceptLoop() {
defer s.listener.Close()
for {
select {
case <-s.done:
return
default:
}
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.done:
return
default:
log.Printf("accept error: %v", err)
}
continue
}
// Handle each connection in separate goroutine
go s.handleConnection(conn)
}
}
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
// Process client requests
for {
// Read request
// Process
// Write response
}
}
func (s *Server) Stop() {
close(s.done)
} Goroutine-Per-Connection Trade-offs
Advantages:
- • Simple to implement
- • Good for many concurrent connections
- • Go's scheduler handles goroutines efficiently
Disadvantages:
- • Memory per goroutine (~2MB)
- • Can have 100K+ goroutines
- • Context switching overhead
Typical limits: 10K concurrent connections per machine. High-performance servers use event loops (epoll, kqueue).
Connection State Management
type ClientState struct {
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
lastActive time.Time
authenticated bool
db int // Selected database number
}
func (cs *ClientState) Update() {
cs.lastActive = time.Now()
}
func (cs *ClientState) Idle() time.Duration {
return time.Since(cs.lastActive)
}
// Per-connection context
type ConnectionContext struct {
state *ClientState
txn *Transaction // If in transaction
subscription *Subscription // If subscribed
} Connection Pooling
// Server-side pooling (limit concurrent connections)
type ConnPool struct {
maxConns int
current int
mu sync.Mutex
waitQueue chan struct{}
}
func (cp *ConnPool) Acquire() error {
cp.mu.Lock()
if cp.current >= cp.maxConns {
cp.mu.Unlock()
// Wait for connection to be available
<-cp.waitQueue
cp.mu.Lock()
}
cp.current++
cp.mu.Unlock()
return nil
}
func (cp *ConnPool) Release() {
cp.mu.Lock()
cp.current--
cp.mu.Unlock()
// Signal waiting connection
select {
case cp.waitQueue <- struct{}{}:
default:
}
} Lesson 7.3: Request Parsing
Efficient Buffer Management
import "bufio"
type RESPParser struct {
reader *bufio.Reader
}
func NewRESPParser(conn net.Conn) *RESPParser {
return &RESPParser{
reader: bufio.NewReaderSize(conn, 64*1024), // 64KB buffer
}
}
// Read line (until \r\n)
func (p *RESPParser) readLine() ([]byte, error) {
line, err := p.reader.ReadBytes('\n')
if err != nil {
return nil, err
}
// Remove \r\n
if len(line) > 0 && line[len(line)-1] == '\n' {
line = line[:len(line)-1]
}
if len(line) > 0 && line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
return line, nil
}
// Peek into buffer without consuming
func (p *RESPParser) peek() (byte, error) {
b, err := p.reader.Peek(1)
if len(b) > 0 {
return b[0], nil
}
return 0, err
}
// Read N bytes exactly
func (p *RESPParser) readN(n int) ([]byte, error) {
buf := make([]byte, n)
_, err := io.ReadFull(p.reader, buf)
return buf, err
} Streaming Large Values
// For very large values, stream instead of buffering all
func (p *RESPParser) ParseBulkString(maxSize int64) (io.Reader, error) {
// Parse length
line, _ := p.readLine()
size := parseBulkSize(line)
if size > maxSize {
return nil, ErrValueTooLarge
}
// Return limited reader
return io.LimitReader(p.reader, size), nil
} Protocol State Machines
type ParserState int
const (
StateWaitingForCommand ParserState = iota
StateWaitingForArgs
StateWaitingForBulk
)
type StatefulParser struct {
state ParserState
command string
args [][]byte
reader *bufio.Reader
}
func (sp *StatefulParser) Parse() (*Command, error) {
switch sp.state {
case StateWaitingForCommand:
return sp.parseCommand()
case StateWaitingForArgs:
return sp.parseArgs()
case StateWaitingForBulk:
return sp.parseBulk()
}
return nil, ErrInvalidState
}
func (sp *StatefulParser) parseCommand() (*Command, error) {
// Read array marker
marker, _ := sp.peek()
if marker != '*' {
return nil, ErrInvalidFormat
}
// Parse array length
line, _ := sp.readLine()
count := parseArrayCount(line)
sp.state = StateWaitingForArgs
return nil, ErrNeedMoreData
} Complete Server Implementation
package server
import (
"bufio"
"context"
"fmt"
"io"
"log"
"net"
"strconv"
"sync"
"time"
)
// Server accepts client connections and processes commands
type Server struct {
addr string
listener net.Listener
done chan struct{}
wg sync.WaitGroup
db Store
maxConns int
connCount int
connMu sync.Mutex
}
// NewServer creates a new server
func NewServer(addr string, db Store) *Server {
return &Server{
addr: addr,
done: make(chan struct{}),
db: db,
maxConns: 10000,
}
}
// Start begins listening for connections
func (s *Server) Start() error {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
return err
}
s.listener = listener
log.Printf("Server listening on %s", s.addr)
s.wg.Add(1)
go s.acceptLoop()
return nil
}
// acceptLoop accepts incoming connections
func (s *Server) acceptLoop() {
defer s.wg.Done()
defer s.listener.Close()
for {
select {
case <-s.done:
return
default:
}
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.done:
return
default:
log.Printf("accept error: %v", err)
}
continue
}
// Check connection limit
s.connMu.Lock()
if s.connCount >= s.maxConns {
s.connMu.Unlock()
conn.Close()
continue
}
s.connCount++
s.connMu.Unlock()
s.wg.Add(1)
go s.handleClient(conn)
}
}
// handleClient processes commands from a client
func (s *Server) handleClient(conn net.Conn) {
defer s.wg.Done()
defer conn.Close()
defer func() {
s.connMu.Lock()
s.connCount--
s.connMu.Unlock()
}()
// Set connection timeouts
conn.SetDeadline(time.Now().Add(30 * time.Second))
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
parser := NewRESPParser(bufio.NewReader(conn))
writer := bufio.NewWriter(conn)
defer writer.Flush()
ctx := context.Background()
for {
// Parse next command
cmd, err := parser.ParseCommand()
if err == io.EOF {
return
}
if err != nil {
s.sendError(writer, fmt.Sprintf("ERR %v", err))
continue
}
// Reset timeout on each successful command
conn.SetDeadline(time.Now().Add(30 * time.Second))
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
// Execute command
s.executeCommand(ctx, cmd, writer)
}
}
// executeCommand processes a single command
func (s *Server) executeCommand(ctx context.Context, cmd *Command, w *bufio.Writer) {
switch cmd.Name {
case "GET":
s.handleGet(ctx, cmd, w)
case "SET":
s.handleSet(ctx, cmd, w)
case "DEL":
s.handleDel(ctx, cmd, w)
case "PING":
s.sendSimpleString(w, "PONG")
case "ECHO":
if len(cmd.Args) < 1 {
s.sendError(w, "ERR wrong number of arguments for 'echo' command")
return
}
s.sendBulkString(w, cmd.Args[0])
case "INFO":
s.handleInfo(w)
default:
s.sendError(w, fmt.Sprintf("ERR unknown command '%s'", cmd.Name))
}
w.Flush()
}
// handleGet executes GET command
func (s *Server) handleGet(ctx context.Context, cmd *Command, w *bufio.Writer) {
if len(cmd.Args) != 1 {
s.sendError(w, "ERR wrong number of arguments for 'get' command")
return
}
value, err := s.db.Get(ctx, cmd.Args[0])
if err != nil {
s.sendNullBulkString(w)
return
}
s.sendBulkString(w, value)
}
// handleSet executes SET command
func (s *Server) handleSet(ctx context.Context, cmd *Command, w *bufio.Writer) {
if len(cmd.Args) != 2 {
s.sendError(w, "ERR wrong number of arguments for 'set' command")
return
}
err := s.db.Put(ctx, cmd.Args[0], cmd.Args[1])
if err != nil {
s.sendError(w, fmt.Sprintf("ERR %v", err))
return
}
s.sendSimpleString(w, "OK")
}
// handleDel executes DEL command
func (s *Server) handleDel(ctx context.Context, cmd *Command, w *bufio.Writer) {
if len(cmd.Args) == 0 {
s.sendError(w, "ERR wrong number of arguments for 'del' command")
return
}
deleted := 0
for _, key := range cmd.Args {
err := s.db.Delete(ctx, key)
if err == nil {
deleted++
}
}
s.sendInteger(w, int64(deleted))
}
// handleInfo returns server info
func (s *Server) handleInfo(w *bufio.Writer) {
s.connMu.Lock()
connCount := s.connCount
s.connMu.Unlock()
info := fmt.Sprintf("# Server\r\nconnected_clients:%d\r\n", connCount)
s.sendBulkString(w, []byte(info))
}
// Response writers
func (s *Server) sendSimpleString(w *bufio.Writer, msg string) {
fmt.Fprintf(w, "+%s\r\n", msg)
}
func (s *Server) sendError(w *bufio.Writer, msg string) {
fmt.Fprintf(w, "-%s\r\n", msg)
}
func (s *Server) sendInteger(w *bufio.Writer, n int64) {
fmt.Fprintf(w, ":%d\r\n", n)
}
func (s *Server) sendBulkString(w *bufio.Writer, data []byte) {
fmt.Fprintf(w, "$%d\r\n", len(data))
w.Write(data)
fmt.Fprintf(w, "\r\n")
}
func (s *Server) sendNullBulkString(w *bufio.Writer) {
fmt.Fprintf(w, "$-1\r\n")
}
// Stop gracefully shuts down the server
func (s *Server) Stop() {
close(s.done)
s.wg.Wait()
}
// Command represents a parsed client command
type Command struct {
Name string
Args [][]byte
}
// Store interface for persistence
type Store interface {
Get(ctx context.Context, key []byte) ([]byte, error)
Put(ctx context.Context, key, value []byte) error
Delete(ctx context.Context, key []byte) error
}
// RESPParser parses Redis Serialization Protocol
type RESPParser struct {
reader *bufio.Reader
}
// NewRESPParser creates a new parser
func NewRESPParser(r *bufio.Reader) *RESPParser {
return &RESPParser{reader: r}
}
// ParseCommand parses next RESP command
func (p *RESPParser) ParseCommand() (*Command, error) {
// Read array marker
marker, err := p.reader.ReadByte()
if err != nil {
return nil, err
}
if marker != '*' {
return nil, fmt.Errorf("expected '*', got %c", marker)
}
// Read array length
line, err := p.readLine()
if err != nil {
return nil, err
}
count, err := strconv.Atoi(string(line))
if err != nil {
return nil, fmt.Errorf("invalid array length: %v", err)
}
if count == 0 {
return nil, fmt.Errorf("empty array")
}
// Read first element (command name)
cmdName, err := p.parseBulkString()
if err != nil {
return nil, err
}
// Read remaining arguments
args := make([][]byte, count-1)
for i := 0; i < count-1; i++ {
arg, err := p.parseBulkString()
if err != nil {
return nil, err
}
args[i] = arg
}
return &Command{
Name: string(cmdName),
Args: args,
}, nil
}
// parseBulkString parses a RESP bulk string
func (p *RESPParser) parseBulkString() ([]byte, error) {
marker, err := p.reader.ReadByte()
if err != nil {
return nil, err
}
if marker != '$' {
return nil, fmt.Errorf("expected '$', got %c", marker)
}
line, err := p.readLine()
if err != nil {
return nil, err
}
length, err := strconv.Atoi(string(line))
if err != nil {
return nil, fmt.Errorf("invalid length: %v", err)
}
if length == -1 {
return nil, nil // Null bulk string
}
// Read data
data := make([]byte, length)
_, err = io.ReadFull(p.reader, data)
if err != nil {
return nil, err
}
// Read trailing \r\n
_, err = p.readLine()
if err != nil {
return nil, err
}
return data, nil
}
// readLine reads until \r\n
func (p *RESPParser) readLine() ([]byte, error) {
line, err := p.reader.ReadBytes('\n')
if err != nil {
return nil, err
}
// Remove \r\n
if len(line) > 0 && line[len(line)-1] == '\n' {
line = line[:len(line)-1]
}
if len(line) > 0 && line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
return line, nil
} Lab 7.1: Implement Redis Protocol Server
Objective
Build a complete RESP-compatible TCP server with connection handling, command parsing, and response generation.
Requirements
- • TCP Server: Listen on configurable address, accept multiple concurrent connections
- • RESP Protocol: Parse all RESP data types, handle errors gracefully
- • Command Handlers: GET, SET, DEL, PING, ECHO, INFO
- • Error Handling: Invalid commands, wrong argument counts, malformed RESP
- • Testing: Test with redis-cli, concurrent client tests
- • Benchmarks: Throughput (commands/sec), connection setup overhead
Starter Code
package server
import (
"bufio"
"context"
"log"
"net"
"sync"
"time"
)
// Server accepts and handles client connections
type Server struct {
addr string
// TODO: Add fields for listener, storage, lifecycle
}
// NewServer creates a new server
// TODO: Implement
func NewServer(addr string, db Store) *Server {
return nil
}
// Start begins listening
// TODO: Implement
func (s *Server) Start() error {
return nil
}
// acceptLoop accepts incoming connections
// TODO: Implement goroutine-per-connection model
func (s *Server) acceptLoop() {
}
// handleClient processes commands from one client
// TODO: Implement command loop with parser
func (s *Server) handleClient(conn net.Conn) {
}
// executeCommand processes a single command
// TODO: Implement dispatch to handlers
func (s *Server) executeCommand(ctx context.Context, cmd *Command, w io.Writer) {
}
// Command handlers
// TODO: Implement handleGet, handleSet, handleDel
// Response writers
// TODO: Implement sendSimpleString, sendError, sendBulkString, etc
// Stop gracefully shuts down
// TODO: Implement
func (s *Server) Stop() {
}
// RESPParser parses RESP format
type RESPParser struct {
// TODO: Add fields
}
// NewRESPParser creates parser
// TODO: Implement
func NewRESPParser(r *bufio.Reader) *RESPParser {
return nil
}
// ParseCommand parses next command
// TODO: Implement complete RESP parsing
func (p *RESPParser) ParseCommand() (*Command, error) {
return nil, nil
}
// Command represents parsed command
type Command struct {
Name string
Args [][]byte
}
// Store interface
type Store interface {
Get(ctx context.Context, key []byte) ([]byte, error)
Put(ctx context.Context, key, value []byte) error
Delete(ctx context.Context, key []byte) error
} Test Template
func TestServerBasic(t *testing.T) {
// Create mock store
store := &MockStore{data: make(map[string][]byte)}
// Start server
server := NewServer("localhost:0", store)
if err := server.Start(); err != nil {
t.Fatal(err)
}
defer server.Stop()
// Connect client
conn, _ := net.Dial("tcp", server.Addr())
defer conn.Close()
// Send SET command
fmt.Fprintf(conn, "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n")
// Read response
reader := bufio.NewReader(conn)
resp, _ := reader.ReadString('\n')
if resp != "+OK\r\n" {
t.Errorf("expected +OK, got %s", resp)
}
}
func BenchmarkServerThroughput(b *testing.B) {
store := &MockStore{data: make(map[string][]byte)}
server := NewServer("localhost:0", store)
server.Start()
defer server.Stop()
conn, _ := net.Dial("tcp", server.Addr())
defer conn.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Fprintf(conn, "*2\r\n$4\r\nPING\r\n")
// Read response
bufio.NewReader(conn).ReadString('\n')
}
} Acceptance Criteria
- ✅ All tests pass
- ✅ Handles concurrent clients
- ✅ Parses RESP correctly
- ✅ redis-cli compatible
- ✅ Graceful shutdown
- ✅ 100K+ commands/sec throughput
- ✅ > 80% code coverage
Summary: Week 7 Complete
By completing Week 7, you've learned:
1. Protocol Design
- • Text-based vs binary protocols
- • RESP (Redis Protocol) specification
- • Request-response patterns
- • Pipelining support
2. TCP Server Fundamentals
- • Go's `net` package
- • Goroutine-per-connection model
- • Connection lifecycle management
- • Timeouts and error handling
3. Request Parsing
- • Efficient buffer management
- • RESP parsing state machine
- • Streaming large values
- • Protocol validation
4. Complete Server Implementation
- • Full working TCP server
- • RESP protocol parser
- • Command execution framework
- • Response writers
- • Connection pooling basics
Metrics Achieved:
- ✅ Handles 10,000+ concurrent connections
- ✅ Sub-millisecond command parsing
- ✅ Graceful shutdown with connection draining
- ✅ Connection timeouts to prevent hangs
Ready for Week 8?
Next week we'll build a production-ready client library with connection pooling, retry logic, circuit breakers, and advanced networking features.
Continue to Week 8: Client Library →