Exercise 04: Network Server Design

エクササイズの目的

このエクササイズでは、高性能ネットワークサーバーの設計と実装を実践します。3つの段階的なプロジェクトを通じて、本格的なサーバーアプリケーションを構築します。

プロジェクト構成

exercise-04-server/
├── src/
│   ├── tcp_echo.zig          # TCP Echo Server (mandatory)
│   ├── connection_pool.zig   # Connection管理 (mandatory)
│   ├── http_server.zig       # HTTP Server (mandatory)
│   ├── load_balancer.zig     # Load Balancer (bonus)
│   └── websocket.zig         # WebSocket Server (bonus)
├── tests/
│   ├── tcp_test.zig
│   ├── http_test.zig
│   └── load_test.zig
├── benchmark/
│   ├── throughput.zig
│   └── latency.zig
└── build.zig

Part 1: TCP Echo Server (30点)

要件

epollを使用した高性能なTCP Echo Serverを実装してください。

マンダトリー機能:

  • 基本的なEchoサーバー (15点)
- 複数クライアントの同時接続対応 - Non-blocking I/O - Edge-triggered epoll - データのエコーバック

  • Connection Pool (15点)
- 効率的なコネクション管理 - タイムアウト処理 - Graceful shutdown - 統計情報の収集

実装テンプレート

const std = @import("std");
const net = std.net;
const os = std.os;
const linux = os.linux;

pub const TcpEchoServer = struct {
    allocator: std.mem.Allocator,
    listener: net.Server,
    epoll_fd: i32,
    pool: ConnectionPool,
    running: bool,

    const Self = @This();

    pub fn init(
        allocator: std.mem.Allocator,
        port: u16,
        max_connections: usize,
    ) !Self {
        // TODO: 実装
        // 1. リスニングソケットを作成
        // 2. epollインスタンスを作成
        // 3. リスニングソケットをepollに登録
        // 4. Connection poolを初期化
        _ = allocator;
        _ = port;
        _ = max_connections;
        return error.NotImplemented;
    }

    pub fn run(self: *Self) !void {
        // TODO: メインイベントループを実装
        // 1. epoll_waitでイベントを待機
        // 2. リスニングソケットのイベント → acceptConnection()
        // 3. クライアントソケットのイベント → handleConnection()
        // 4. タイムアウトチェック
        _ = self;
    }

    fn acceptConnection(self: *Self) !void {
        // TODO: 新しい接続を受け入れる
        // 1. accept()を呼び出し
        // 2. Non-blockingに設定
        // 3. TCP_NODELAYを設定
        // 4. Connection poolから取得
        // 5. epollに登録
        _ = self;
    }

    fn handleConnection(self: *Self, fd: i32) !void {
        // TODO: 接続を処理
        // 1. Connection poolから検索
        // 2. データを読み込み
        // 3. データをエコーバック
        // 4. エラー処理
        _ = self;
        _ = fd;
    }

    pub fn shutdown(self: *Self) !void {
        // TODO: Graceful shutdownを実装
        // 1. 新規接続の受付を停止
        // 2. アクティブな接続の完了を待機
        // 3. タイムアウト後に強制終了
        _ = self;
    }

    pub fn deinit(self: *Self) void {
        // TODO: リソースをクリーンアップ
        _ = self;
    }
};

pub const ConnectionPool = struct {
    connections: []Connection,
    free_list: std.ArrayList(usize),
    allocator: std.mem.Allocator,

    pub const Connection = struct {
        fd: i32,
        buffer: []u8,
        read_pos: usize,
        write_pos: usize,
        last_activity: i64,
        state: State,

        pub const State = enum {
            idle,
            reading,
            writing,
        };
    };

    pub fn init(allocator: std.mem.Allocator, capacity: usize) !ConnectionPool {
        // TODO: 実装
        _ = allocator;
        _ = capacity;
        return error.NotImplemented;
    }

    pub fn acquire(self: *ConnectionPool) ?*Connection {
        // TODO: 空きコネクションを取得
        _ = self;
        return null;
    }

    pub fn release(self: *ConnectionPool, conn: *Connection) void {
        // TODO: コネクションを返却
        _ = self;
        _ = conn;
    }

    pub fn findByFd(self: *ConnectionPool, fd: i32) ?*Connection {
        // TODO: ファイルディスクリプタから検索
        _ = self;
        _ = fd;
        return null;
    }

    pub fn timeoutCheck(self: *ConnectionPool, timeout_ms: i64) void {
        // TODO: タイムアウトした接続を検出
        _ = self;
        _ = timeout_ms;
    }

    pub fn deinit(self: *ConnectionPool) void {
        // TODO: クリーンアップ
        _ = self;
    }
};

テストケース

const std = @import("std");
const testing = std.testing;
const net = std.net;

test "TCP Echo Server - Single client" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Start server in separate thread
    var server = try TcpEchoServer.init(allocator, 8888, 100);
    defer server.deinit();

    const server_thread = try std.Thread.spawn(.{}, runServer, .{&server});
    defer server_thread.join();

    std.time.sleep(100 * std.time.ns_per_ms); // Wait for server to start

    // Connect client
    const address = try net.Address.parseIp("127.0.0.1", 8888);
    const stream = try net.tcpConnectToAddress(address);
    defer stream.close();

    // Send data
    const sent = "Hello, Echo Server!";
    _ = try stream.write(sent);

    // Receive echo
    var buffer: [1024]u8 = undefined;
    const received = try stream.read(&buffer);

    try testing.expectEqualStrings(sent, buffer[0..received]);

    server.running = false;
}

test "TCP Echo Server - Multiple clients" {
    // TODO: 複数クライアントのテスト
}

test "Connection Pool - Acquire and release" {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var pool = try ConnectionPool.init(allocator, 10);
    defer pool.deinit();

    // Acquire all connections
    var conns: [10]*ConnectionPool.Connection = undefined;
    for (&conns) |*conn| {
        conn.* = pool.acquire() orelse return error.TestFailed;
    }

    // Pool should be empty
    try testing.expect(pool.acquire() == null);

    // Release one connection
    pool.release(conns[0]);

    // Should be able to acquire again
    _ = pool.acquire() orelse return error.TestFailed;
}

fn runServer(server: *TcpEchoServer) void {
    server.run() catch {};
}

Part 2: HTTP Server (30点)

要件

基本的なHTTPサーバーを実装してください。

マンダトリー機能:

  • HTTPパーサーとルーター (15点)
- HTTP/1.1 リクエストのパース - ルーティング機能 - ヘッダー処理 - レスポンス生成

  • 静的ファイル配信 (15点)
- ファイルの非同期読み込み - Content-Typeの自動判定 - Range requestsのサポート - キャッシュヘッダー

実装例

pub const HttpServer = struct {
    tcp_server: TcpEchoServer,
    router: Router,
    static_dir: []const u8,
    stats: Statistics,

    pub const Router = struct {
        routes: std.StringHashMap(Handler),

        pub const Handler = *const fn (
            *HttpRequest,
        ) anyerror!HttpResponse;

        pub fn init(allocator: std.mem.Allocator) Router {
            return .{
                .routes = std.StringHashMap(Handler).init(allocator),
            };
        }

        pub fn get(
            self: *Router,
            path: []const u8,
            handler: Handler,
        ) !void {
            // TODO: ルートを登録
            _ = self;
            _ = path;
            _ = handler;
        }

        pub fn route(self: *Router, request: *HttpRequest) ?Handler {
            // TODO: パスに対応するハンドラーを検索
            _ = self;
            _ = request;
            return null;
        }
    };

    pub fn init(
        allocator: std.mem.Allocator,
        port: u16,
        static_dir: []const u8,
    ) !HttpServer {
        // TODO: 実装
        _ = allocator;
        _ = port;
        _ = static_dir;
        return error.NotImplemented;
    }

    fn handleRequest(
        self: *HttpServer,
        conn: *Connection,
        request: HttpRequest,
    ) !void {
        // TODO: リクエストを処理
        // 1. ルーターでハンドラーを検索
        // 2. ハンドラーを実行
        // 3. レスポンスを送信
        // 4. 統計を更新
        _ = self;
        _ = conn;
        _ = request;
    }

    fn serveStatic(
        self: *HttpServer,
        request: *HttpRequest,
    ) !HttpResponse {
        // TODO: 静的ファイルを配信
        // 1. パスを解決 (パストラバーサル対策)
        // 2. ファイルを読み込み
        // 3. Content-Typeを判定
        // 4. レスポンスを生成
        _ = self;
        _ = request;
        return error.NotImplemented;
    }
};

pub const HttpRequest = struct {
    method: Method,
    path: []const u8,
    version: []const u8,
    headers: std.StringHashMap([]const u8),
    body: []const u8,

    pub const Method = enum {
        GET,
        POST,
        PUT,
        DELETE,
        HEAD,
    };

    pub fn parse(
        allocator: std.mem.Allocator,
        buffer: []const u8,
    ) !HttpRequest {
        // TODO: HTTPリクエストをパース
        _ = allocator;
        _ = buffer;
        return error.NotImplemented;
    }

    pub fn deinit(self: *HttpRequest) void {
        self.headers.deinit();
    }
};

pub const HttpResponse = struct {
    status: u16,
    status_text: []const u8,
    headers: std.StringHashMap([]const u8),
    body: []const u8,

    pub fn ok(allocator: std.mem.Allocator, body: []const u8) !HttpResponse {
        // TODO: 200 OKレスポンスを生成
        _ = allocator;
        _ = body;
        return error.NotImplemented;
    }

    pub fn notFound(allocator: std.mem.Allocator) !HttpResponse {
        // TODO: 404 Not Foundレスポンスを生成
        _ = allocator;
        return error.NotImplemented;
    }

    pub fn format(
        self: HttpResponse,
        allocator: std.mem.Allocator,
    ) ![]u8 {
        // TODO: HTTP形式にフォーマット
        _ = self;
        _ = allocator;
        return error.NotImplemented;
    }
};

必須エンドポイント

GET /              → index.html
GET /static/*      → 静的ファイル
GET /api/stats     → サーバー統計 (JSON)
GET /api/health    → ヘルスチェック

テストケース

test "HTTP Parser - GET request" {
    const request_text =
        "GET /index.html HTTP/1.1\r\n" ++
        "Host: localhost:8080\r\n" ++
        "User-Agent: Test\r\n" ++
        "\r\n";

    var request = try HttpRequest.parse(testing.allocator, request_text);
    defer request.deinit();

    try testing.expect(request.method == .GET);
    try testing.expectEqualStrings("/index.html", request.path);
    try testing.expectEqualStrings("HTTP/1.1", request.version);
}

test "HTTP Server - Serve static file" {
    // TODO: 静的ファイル配信のテスト
}

test "HTTP Server - Route matching" {
    // TODO: ルーティングのテスト
}

Part 3: Bonus Projects (40点)

Bonus 1: Multi-threaded HTTP Server with Load Balancer (20点)

マルチスレッドHTTPサーバーとロードバランサーを実装してください。

要件:

  • ワーカースレッドプール
  • ロードバランシング戦略
- Round-robin - Least connections - Least response time
  • スレッド間統計の集約
  • グレースフルシャットダウン

pub const MultiThreadedServer = struct {
    allocator: std.mem.Allocator,
    workers: []Worker,
    load_balancer: LoadBalancer,
    listener: net.Server,

    pub const Worker = struct {
        id: usize,
        thread: std.Thread,
        server: HttpServer,
        running: std.atomic.Value(bool),

        pub fn run(self: *Worker) void {
            // TODO: ワーカーのメインループ
        }
    };

    pub fn init(
        allocator: std.mem.Allocator,
        port: u16,
        num_workers: usize,
    ) !MultiThreadedServer {
        // TODO: 実装
    }

    pub fn run(self: *MultiThreadedServer) !void {
        // TODO: メインループ
        // 1. 接続を受け入れ
        // 2. ロードバランサーでワーカーを選択
        // 3. ワーカーに接続を委譲
    }
};

性能目標:

  • 10,000同時接続
  • 50,000 req/s以上のスループット
  • p99レイテンシ < 50ms

Bonus 2: WebSocket Server (20点)

WebSocketプロトコルをサポートするサーバーを実装してください。

要件:

  • WebSocketハンドシェイク
  • フレームのパース・生成
  • テキスト・バイナリメッセージ
  • Ping/Pong
  • Closeハンドシェイク

pub const WebSocketServer = struct {
    http_server: HttpServer,

    pub const Frame = struct {
        fin: bool,
        opcode: Opcode,
        masked: bool,
        payload: []const u8,

        pub const Opcode = enum(u4) {
            continuation = 0x0,
            text = 0x1,
            binary = 0x2,
            close = 0x8,
            ping = 0x9,
            pong = 0xA,
        };

        pub fn parse(buffer: []const u8) !Frame {
            // TODO: WebSocketフレームをパース
            _ = buffer;
            return error.NotImplemented;
        }

        pub fn encode(
            allocator: std.mem.Allocator,
            opcode: Opcode,
            payload: []const u8,
        ) ![]u8 {
            // TODO: WebSocketフレームをエンコード
            _ = allocator;
            _ = opcode;
            _ = payload;
            return error.NotImplemented;
        }
    };

    pub fn handleUpgrade(
        self: *WebSocketServer,
        request: *HttpRequest,
    ) !HttpResponse {
        // TODO: WebSocketアップグレード処理
        // 1. Sec-WebSocket-Keyを取得
        // 2. Acceptキーを計算
        // 3. 101 Switching Protocolsレスポンス
        _ = self;
        _ = request;
        return error.NotImplemented;
    }

    pub fn handleWebSocket(
        self: *WebSocketServer,
        conn: *Connection,
    ) !void {
        // TODO: WebSocket接続を処理
        // 1. フレームを受信
        // 2. フレームをパース
        // 3. メッセージを処理
        // 4. レスポンスを送信
        _ = self;
        _ = conn;
    }
};

実装するエコーサーバー例:

クライアント送信: {"type": "echo", "data": "Hello"}
サーバー応答: {"type": "echo", "data": "Hello"}

ベンチマークツール

スループット測定

pub fn benchmarkThroughput(
    server_address: []const u8,
    num_requests: usize,
    concurrency: usize,
) !void {
    std.log.info("Throughput Benchmark", .{});
    std.log.info("  Requests: {}", .{num_requests});
    std.log.info("  Concurrency: {}", .{concurrency});

    const start = std.time.milliTimestamp();

    // TODO: 実装
    // 1. 並列でリクエストを送信
    // 2. 完了を待機
    // 3. スループットを計算

    const end = std.time.milliTimestamp();
    const duration_ms = end - start;
    const throughput = @as(f64, @floatFromInt(num_requests * 1000)) /
        @as(f64, @floatFromInt(duration_ms));

    std.log.info("\nResults:", .{});
    std.log.info("  Duration: {}ms", .{duration_ms});
    std.log.info("  Throughput: {d:.2} req/s", .{throughput});
}

レイテンシ測定

pub fn benchmarkLatency(
    server_address: []const u8,
    num_samples: usize,
) !void {
    var latencies = try std.ArrayList(i64).initCapacity(
        std.heap.page_allocator,
        num_samples,
    );
    defer latencies.deinit();

    // TODO: 実装
    // 1. リクエストを送信
    // 2. レイテンシを記録
    // 3. 統計を計算 (min, max, avg, p50, p95, p99)

    std.sort.heap(i64, latencies.items, {}, std.sort.asc(i64));

    const min = latencies.items[0];
    const max = latencies.items[latencies.items.len - 1];
    const p50 = latencies.items[latencies.items.len * 50 / 100];
    const p95 = latencies.items[latencies.items.len * 95 / 100];
    const p99 = latencies.items[latencies.items.len * 99 / 100];

    std.log.info("Latency Benchmark:", .{});
    std.log.info("  Min: {}ms", .{min});
    std.log.info("  P50: {}ms", .{p50});
    std.log.info("  P95: {}ms", .{p95});
    std.log.info("  P99: {}ms", .{p99});
    std.log.info("  Max: {}ms", .{max});
}

評価基準

マンダトリー要件 (80点)

項目 配点 評価基準
TCP Echo Server 30点 - 正常動作
- Connection pool
- Graceful shutdown
HTTP Server 30点 - HTTP/1.1対応
- ルーティング
- 静的ファイル配信
テスト 20点 - ユニットテスト
- 統合テスト
- エラーハンドリング

必須:

  • すべてのテストが通過
  • メモリリークなし
  • 適切なエラーハンドリング
  • ドキュメント完備
  • ボーナス要件 (20点)

    項目 配点 評価基準
    Multi-threaded Server 10点 - マルチスレッド対応
    - ロードバランシング
    - 性能目標達成
    WebSocket Server 10点 - フルプロトコル実装
    - 安定動作
    - エコーサーバー実装

    提出方法

    ディレクトリ構造

    exercise-04-server/
    ├── README.md
    ├── build.zig
    ├── src/
    │   ├── tcp_echo.zig
    │   ├── connection_pool.zig
    │   ├── http_server.zig
    │   ├── load_balancer.zig       # (bonus)
    │   └── websocket.zig           # (bonus)
    ├── tests/
    │   └── *.zig
    ├── benchmark/
    │   └── *.zig
    ├── static/                      # 静的ファイル用
    │   └── index.html
    └── PERFORMANCE.md              # ベンチマーク結果
    

    README.md テンプレート

    # Exercise 04: Network Server Implementation
    
    ## Author
    - Name: [あなたの名前]
    - Date: [提出日]
    
    ## Build
    \`\`\`bash
    zig build
    \`\`\`
    
    ## Run
    \`\`\`bash
    # TCP Echo Server
    zig build run-tcp -- --port 8080
    
    # HTTP Server
    zig build run-http -- --port 8080 --static ./static
    
    # Multi-threaded Server (bonus)
    zig build run-mt -- --port 8080 --workers 4
    \`\`\`
    
    ## Test
    \`\`\`bash
    zig build test
    \`\`\`
    
    ## Benchmark
    \`\`\`bash
    zig build benchmark
    \`\`\`
    
    ## Implementation Details
    [実装の詳細、工夫した点など]
    
    ## Performance Results
    [ベンチマーク結果へのリンク: PERFORMANCE.md]
    
    ## Bonus Features
    - [ ] Multi-threaded Server
    - [ ] WebSocket Server
    

    PERFORMANCE.md フォーマット

    # Performance Benchmark Results
    
    ## Environment
    - CPU: [CPU情報]
    - RAM: [メモリ容量]
    - OS: [OS情報]
    - Zig: [Zigバージョン]
    
    ## TCP Echo Server
    
    ### Throughput
    | Concurrency | Requests | Throughput | Duration |
    |-------------|----------|------------|----------|
    | 10          | 10,000   | XXX req/s  | XXX ms   |
    | 100         | 10,000   | XXX req/s  | XXX ms   |
    | 1000        | 10,000   | XXX req/s  | XXX ms   |
    
    ### Latency
    | Metric | Value |
    |--------|-------|
    | Min    | XX ms |
    | P50    | XX ms |
    | P95    | XX ms |
    | P99    | XX ms |
    | Max    | XX ms |
    
    ## HTTP Server
    
    [同様のフォーマットで記載]
    
    ## Analysis
    [結果の分析、ボトルネックの考察など]
    

    ヒント

    デバッグテクニック

    // 接続状態のログ
    fn logConnection(conn: *Connection, msg: []const u8) void {
        std.log.debug("[fd={}] {s}: state={s}, read_pos={}, write_pos={}",
            .{ conn.fd, msg, @tagName(conn.state), conn.read_pos, conn.write_pos });
    }
    
    // epollイベントのログ
    fn logEpollEvent(event: linux.epoll_event) void {
        std.log.debug("epoll event: fd={}, events={b:08}", .{
            event.data.fd,
            event.events,
        });
    }
    
    // パフォーマンス測定
    const Timer = struct {
        start: i64,
    
        pub fn init() Timer {
            return .{ .start = std.time.microTimestamp() };
        }
    
        pub fn elapsed(self: Timer) i64 {
            return std.time.microTimestamp() - self.start;
        }
    
        pub fn log(self: Timer, msg: []const u8) void {
            std.log.info("{s}: {}μs", .{ msg, self.elapsed() });
        }
    };
    

    よくあるエラー

  • EWOULDBLOCK/EAGAIN
- Non-blocking I/Oで正常な状態 - 適切にハンドルすること

  • EPIPE (Broken pipe)
- クライアントが接続を切断 - エラーとして扱わず、接続をクリーンアップ

  • Too many open files
- ulimitを確認・調整 - Connection poolのサイズを適切に設定

参考資料

期限

提出期限: [指定された日付]

頑張ってください!