Course Navigation
Module 1: Game Server Fundamentals
Module 2: State Synchronization
Module 3: Advanced Networking
Module 4: Scaling & Performance
Module 5: Game Systems
Module 6: Production & Deployment
Capstone Project
Network Protocols for Games
Learning Objectives
- • Understand why HTTP isn't suitable for real-time games
- • Master UDP vs TCP trade-offs for different game types
- • Learn message serialization with Protocol Buffers
- • Build UDP server with connection management
- • Handle packet loss and out-of-order delivery
Lesson 2.1: Why Not HTTP?
❌ Why HTTP Fails for Games
Developers new to games often ask: "Why not just use HTTP/REST like web apps?" Let's see why this approach fails for real-time games.
The HTTP Problem
Scenario: Player moves in game at 60 updates/sec
HTTP/REST Approach
T=0ms   Client: POST /game/move {"x": 10, "y": 20}
        └─> Network (50ms)
T=50ms  Server receives, processes
T=60ms  Server responds 200 OK with full game state
        └─> Network (50ms)
T=110ms Client receives → Finally sees updateResult: 110ms latency before player sees anything. Unplayable!
Why HTTP Fails for Games
- • Request/Response Model: Must wait for response
- • High Overhead: Headers, compression, parsing
- • No Streaming: Can't continuously send state
- • Polling: If you poll for state updates, you're constantly sending requests = huge waste
Lesson 2.2: TCP vs UDP Trade-offs
TCP (Transmission Control Protocol)
Characteristics:
- Connection-oriented (handshake required)
- Guaranteed delivery (retransmits lost packets)
- In-order delivery (packets arrive in sequence)
- Automatic congestion control
- Slower (overhead for guarantees)
Overhead per packet: 20 bytes minimum
Latency: Higher (retransmits, waits for ACK)
Best for: Turn-based games, MMO chat, file transfers
NOT good for: Fast-paced shootersReal-World Examples
- • Turn-based strategy (Chess.com)
- • Card games (Hearthstone)
- • Text chat in any game
- • Persistent messaging (trade requests, emails)
UDP (User Datagram Protocol)
Characteristics:
- Connectionless (no handshake)
- No delivery guarantee (sent, might be lost)
- No ordering guarantee (packets arrive out of order)
- No congestion control (app must handle)
- Faster (minimal overhead)
Overhead per packet: 8 bytes
Latency: Lower (send immediately)
Best for: Real-time shooters, fighting games
Tradeoff: App must handle loss/orderingReal-World Examples
- • FPS shooters (Valorant, CS:GO)
- • Battle royale (Fortnite)
- • Fighting games (Street Fighter)
- • Action games (any real-time)
Visual Comparison
TCP Timeline
T=0ms   Client sends packet
        └─> Network (50ms)
T=50ms  Server receives
        └─> Server responds ACK (50ms)
T=100ms Client receives ACK
        └─> Can send next packet
Total: 100ms to send one update (too slow!)UDP Timeline
T=0ms   Client sends packet
        └─> Network (50ms)
T=50ms  Server receives
        (No waiting for ACK, immediately ready for next)
Total: 50ms latency (much better!)💡 Hybrid Approaches
Most modern games use both protocols for different purposes:
TCP Used For:
- • Authentication
- • Lobby chat
- • Ranked queue
- • Inventory updates
UDP Used For:
- • In-game position
- • Shooting
- • Abilities
- • Animations
Lesson 2.3: Message Serialization
Format Comparison
| Format | Size | Speed | Ease | Network | 
|---|---|---|---|---|
| JSON | 80B | Slow | Easy | Bandwidth hog | 
| MessagePack | 30B | Fast | Medium | Good | 
| Protobuf | 16B | Fast | Medium | Excellent | 
| Binary | 14B | Fastest | Hard | Best | 
Recommendation for Games
- • Prototype/development: JSON (easy debugging)
- • Production: Protocol Buffers (good balance)
- • Ultra-competitive (esports): Binary (every byte counts)
Protocol Buffers Example
syntax = "proto3";
package game;
// Player input from client
message PlayerInput {
  int32 player_id = 1;
  float move_x = 2;           // -1 to 1
  float move_y = 3;           // -1 to 1
  uint32 tick = 4;            // Which tick this input is for
  bool shoot = 5;
  bool jump = 6;
  uint32 target_player_id = 7;
}
// World update from server to client
message WorldUpdate {
  uint32 tick = 1;
  int64 server_time_ms = 2;
  repeated ProjectileState projectiles = 4;
  repeated Entity entities = 5;
}
message PlayerState {
  int32 player_id = 1;
  Vector3 position = 2;
  Vector3 velocity = 3;
  float rotation = 4;
  int32 health = 5;
  string animation = 6;
  repeated StatusEffect effects = 7;
}
message Vector3 {
  float x = 1;
  float y = 2;
  float z = 3;
}Usage in Go
import "game.pb"  // Generated from protobuf
// Encode message
input := &game.PlayerInput{
    PlayerId: 5,
    MoveX:    0.5,
    MoveY:    0.0,
    Tick:     120,
    Shoot:    true,
}
// Convert to bytes
data, err := proto.Marshal(input)
if err != nil {
    log.Fatal(err)
}
// Send over network
conn.WriteToUDP(data, remoteAddr)
// On receiving end
var receivedInput game.PlayerInput
err = proto.Unmarshal(packetData, &receivedInput)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Player %d moved (%.2f, %.2f)\n", 
    receivedInput.PlayerId, 
    receivedInput.MoveX, 
    receivedInput.MoveY)Lab 2: Build UDP Server with Broadcast
Objective
Build a UDP server that accepts connections from multiple clients, tracks connected clients, broadcasts messages to all connected clients, and handles timeouts.
Requirements
- • Accept UDP connections
- • Track multiple clients by IP:Port
- • Broadcast received messages to all clients
- • Detect timeouts and remove disconnected clients
- • Print stats every 5 seconds
Starter Code
package main
import (
    "fmt"
    "log"
    "net"
    "time"
)
type GameServer struct {
    conn              *net.UDPConn
    clients           map[int]*ClientConnection
    clientsByAddr     map[string]int
    nextPlayerID      int
    disconnectTimeout time.Duration
    ticker            *time.Ticker
}
type ClientConnection struct {
    PlayerID     int
    RemoteAddr   *net.UDPAddr
    LastPacketAt time.Time
    IsConnected  bool
}
// Broadcast message from one player to all others
type BroadcastMessage struct {
    SenderID  int
    Message   string
    Timestamp int64
}
func NewGameServer(addr string, port int) (*GameServer, error) {
    // TODO: Create UDP listener on addr:port
    // Return GameServer or error if binding fails
    
    return &GameServer{
        clients:           make(map[int]*ClientConnection),
        clientsByAddr:     make(map[string]int),
        nextPlayerID:      1,
        disconnectTimeout: 30 * time.Second,
    }, nil
}
func (g *GameServer) Start() {
    // TODO: Start listening loop
    // 1. Read UDP packets in loop
    // 2. For each packet: call HandlePacket()
}
func (g *GameServer) HandlePacket(data []byte, remoteAddr *net.UDPAddr) {
    // TODO:
    // 1. Check if sender is known (by IP:Port)
    // 2. If new: assign PlayerID, register client
    // 3. If known: update LastPacketAt
    // 4. Parse message and broadcast to all
}
func (g *GameServer) Broadcast(message BroadcastMessage) {
    // TODO:
    // 1. Convert message to bytes
    // 2. Send to all connected clients
    // 3. Log broadcast
}
func (g *GameServer) CheckTimeouts() {
    // TODO:
    // 1. Check each client's LastPacketAt
    // 2. If older than disconnectTimeout:
    //    - Remove from clients
    //    - Remove from clientsByAddr
    //    - Notify others that they left
}
func (g *GameServer) PrintStats() {
    fmt.Printf("[%s] Connected: %d players\n", 
        time.Now().Format("15:04:05"), 
        len(g.clients))
    
    for playerID, client := range g.clients {
        fmt.Printf("  Player %d: %s (last: %.1fs ago)\n",
            playerID,
            client.RemoteAddr.String(),
            time.Since(client.LastPacketAt).Seconds())
    }
}
func main() {
    server, err := NewGameServer("127.0.0.1", 9000)
    if err != nil {
        log.Fatal(err)
    }
    
    // Start server loop
    go server.Start()
    
    // Periodically check timeouts
    go func() {
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()
        
        for range ticker.C {
            server.CheckTimeouts()
            server.PrintStats()
        }
    }()
    
    // Run for 60 seconds
    time.Sleep(60 * time.Second)
}Client Simulator (for testing)
// Run this in separate process to test server
func TestClient(id int, serverAddr string) {
    conn, err := net.Dial("udp", serverAddr)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    
    // Send initial message
    msg := fmt.Sprintf("Hello, I'm client %d", id)
    conn.Write([]byte(msg))
    
    // Send periodic messages
    ticker := time.NewTicker(10 * time.Second)
    for i := 0; i < 5; i++ {
        <-ticker.C
        msg := fmt.Sprintf("Message %d from client %d", i, id)
        conn.Write([]byte(msg))
    }
}Expected Output
[09:32:15] Connected: 2 players
  Player 1: 127.0.0.1:54321 (last: 0.2s ago)
  Player 2: 127.0.0.1:54322 (last: 0.1s ago)
[09:32:20] Connected: 2 players
  Player 1: 127.0.0.1:54321 (last: 5.2s ago)
  Player 2: 127.0.0.1:54322 (last: 5.1s ago)
[09:32:25] Connected: 1 players
  Player 2: 127.0.0.1:54322 (last: 0.1s ago)
[Player 1 TIMED OUT]✅ Success Criteria
- • Server accepts UDP connections on specified port
- • Tracks multiple clients by IP:Port combination
- • Broadcasts received messages to all connected clients
- • Detects timeouts and removes disconnected clients
- • Prints connection stats every 5 seconds
Ready for Week 3?
Next week we'll dive into state synchronization, client-side prediction, and keeping all players in sync while handling network issues.
Continue to Week 3: State Synchronization →