Exercise 1: I/Oモデル進化の実践演習

演習の目的

この演習では、Chapter 1とExplanation 1で学んだI/Oモデルの概念を実践的に理解します。ブロッキング、ノンブロッキング、そしてselect/pollを使った実装を通じて、各モデルの特性とトレードオフを体験します。

前提条件

  • Zigの基本的な構文理解
  • LinuxまたはmacOS環境
  • ターミナルとテキストエディタ
  • 評価基準

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

    以下の要件を全て満たすことで基本点が獲得できます:

  • [ ] ブロッキングI/Oサーバーの実装(20点)
  • [ ] ノンブロッキングI/Oサーバーの実装(20点)
  • [ ] selectを使ったサーバーの実装(20点)
  • [ ] 各実装の動作確認とテスト(20点)
  • ボーナス要件(20点)

    以下の高度な機能を実装することで追加点が獲得できます:

  • [ ] pollを使ったサーバーの実装(5点)
  • [ ] パフォーマンス測定とベンチマーク(5点)
  • [ ] タイムアウト処理の実装(5点)
  • [ ] エラーハンドリングの改善(5点)
  • 演習1: ブロッキングI/Oエコーサーバー

    目標

    最もシンプルなブロッキングI/Oを使ったエコーサーバーを実装します。このサーバーは、クライアントから受信したデータをそのまま返します。

    実装手順

  • blocking_echo_server.zigファイルを作成
  • TCPソケットをバインドしてリスニング
  • 接続を受け付ける
  • データを受信してエコーバック
  • 接続を閉じる
  • スターターコード

    const std = @import("std");
    const net = std.net;
    const os = std.os;
    
    pub fn main() !void {
        const address = try net.Address.parseIp4("127.0.0.1", 8080);
    
        // TODO: ソケットを作成してバインド
        // ヒント: net.StreamServer を使用
    
        std.debug.print("ブロッキングサーバー起動: {}\n", .{address});
    
        // TODO: 接続を受け付けるループ
        while (true) {
            // TODO: accept()で接続を受け付ける
    
            // TODO: handleClient()を呼び出す
        }
    }
    
    fn handleClient(connection: net.StreamServer.Connection) !void {
        defer connection.stream.close();
    
        var buffer: [1024]u8 = undefined;
    
        while (true) {
            // TODO: データを読み込む
            // ヒント: connection.stream.read()
    
            // TODO: 読み込んだデータをエコーバック
            // ヒント: connection.stream.writeAll()
        }
    }
    

    実装のヒント

    // ソケットのバインド例
    var server = net.StreamServer.init(.{});
    defer server.deinit();
    
    try server.listen(address);
    
    // 接続の受け付け例
    const connection = try server.accept();
    
    // データの読み込み例
    const bytes_read = try stream.read(&buffer);
    if (bytes_read == 0) {
        // 接続が閉じられた
        break;
    }
    

    テスト方法

    # サーバーの起動
    zig build-exe blocking_echo_server.zig
    ./blocking_echo_server
    
    # 別のターミナルでテスト
    echo "Hello, World!" | nc localhost 8080
    
    # または複数のクライアントで同時接続をテスト
    nc localhost 8080 &
    nc localhost 8080 &
    # 注意: ブロッキングモードでは1つしか処理できない
    

    期待される動作

    サーバー側:
    ブロッキングサーバー起動: 127.0.0.1:8080
    接続受付: 127.0.0.1:xxxxx
    受信: Hello, World!
    送信: Hello, World!
    接続終了
    
    クライアント側:
    $ echo "Hello, World!" | nc localhost 8080
    Hello, World!
    

    チェックリスト

  • [ ] サーバーが起動する
  • [ ] クライアント接続を受け付ける
  • [ ] データをエコーバックする
  • [ ] 接続が正しく閉じられる
  • [ ] 制限: 同時に1つの接続しか処理できない
  • 演習2: ノンブロッキングI/Oエコーサーバー

    目標

    ノンブロッキングI/Oを使って、複数の接続を処理できるサーバーを実装します。

    実装手順

  • nonblocking_echo_server.zigファイルを作成
  • ソケットをノンブロッキングモードに設定
  • 複数の接続を管理するリストを作成
  • 各接続を順番にポーリング
  • EAGAINエラーを適切に処理
  • スターターコード

    const std = @import("std");
    const net = std.net;
    const os = std.os;
    
    const Connection = struct {
        stream: net.Stream,
        address: net.Address,
    };
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
        const allocator = gpa.allocator();
    
        const address = try net.Address.parseIp4("127.0.0.1", 8080);
        var server = net.StreamServer.init(.{});
        defer server.deinit();
    
        try server.listen(address);
    
        // リスニングソケットをノンブロッキングに設定
        // TODO: os.fcntl を使用
    
        var connections = std.ArrayList(Connection).init(allocator);
        defer connections.deinit();
    
        std.debug.print("ノンブロッキングサーバー起動: {}\n", .{address});
    
        while (true) {
            // TODO: 新しい接続の受け付け(ノンブロッキング)
            // ヒント: WouldBlockエラーを処理
    
            // TODO: 既存の接続からデータ読み込み
            // ヒント: 各接続を順番にチェック
    
            // TODO: 閉じられた接続を削除
    
            // CPU使用率を下げるため短いスリープ
            std.time.sleep(1 * std.time.ns_per_ms);
        }
    }
    
    fn setNonBlocking(fd: os.fd_t) !void {
        // TODO: O_NONBLOCKフラグを設定
        // ヒント: os.fcntl(fd, os.F.GETFL, 0)
        //        os.fcntl(fd, os.F.SETFL, flags | os.O.NONBLOCK)
    }
    
    fn handleConnection(conn: *Connection) !bool {
        var buffer: [1024]u8 = undefined;
    
        // TODO: データの読み込み
        // ヒント: WouldBlockエラーを処理
    
        // TODO: エコーバック
    
        // true = 接続継続, false = 接続終了
        return true;
    }
    

    実装のヒント

    // ノンブロッキング設定
    fn setNonBlocking(fd: os.fd_t) !void {
        const flags = try os.fcntl(fd, os.F.GETFL, 0);
        _ = try os.fcntl(fd, os.F.SETFL, flags | os.O.NONBLOCK);
    }
    
    // ノンブロッキングaccept
    const result = server.accept();
    if (result) |connection| {
        try setNonBlocking(connection.stream.handle);
        try connections.append(connection);
    } else |err| {
        if (err != error.WouldBlock) {
            return err;
        }
        // WouldBlockは正常(新しい接続がないだけ)
    }
    
    // ノンブロッキングread
    const result = os.read(fd, buffer);
    if (result) |bytes_read| {
        // データ処理
    } else |err| {
        if (err == error.WouldBlock) {
            // データがまだ来ていない
            return true;  // 接続継続
        }
        return err;
    }
    

    テスト方法

    # サーバーの起動
    zig build-exe nonblocking_echo_server.zig
    ./nonblocking_echo_server
    
    # 複数のクライアントで同時接続
    nc localhost 8080 &
    nc localhost 8080 &
    nc localhost 8080 &
    
    # 各クライアントからメッセージを送信
    # すべてのクライアントが応答を受け取るはず
    

    チェックリスト

  • [ ] ノンブロッキングモードが正しく設定される
  • [ ] 複数のクライアントを同時に処理できる
  • [ ] WouldBlockエラーを適切に処理する
  • [ ] CPU使用率が100%にならない
  • [ ] 接続が正しく管理される
  • 演習3: selectを使ったI/O多重化サーバー

    目標

    selectシステムコールを使って効率的なI/O多重化サーバーを実装します。

    実装手順

  • select_echo_server.zigファイルを作成
  • fd_setを管理する構造体を作成
  • selectでイベントを待機
  • 準備完了したFDを処理
  • スターターコード

    const std = @import("std");
    const net = std.net;
    const os = std.os;
    const linux = os.linux;
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
        const allocator = gpa.allocator();
    
        const address = try net.Address.parseIp4("127.0.0.1", 8080);
        var server = net.StreamServer.init(.{});
        defer server.deinit();
    
        try server.listen(address);
    
        const server_fd = server.sockfd.?;
        var connections = std.ArrayList(os.socket_t).init(allocator);
        defer connections.deinit();
    
        std.debug.print("selectサーバー起動: {}\n", .{address});
    
        while (true) {
            // TODO: fd_setを準備
            var read_fds: linux.fd_set = undefined;
            linux.FD_ZERO(&read_fds);
    
            // TODO: サーバーFDを追加
            linux.FD_SET(@intCast(server_fd), &read_fds);
            var max_fd: i32 = @intCast(server_fd);
    
            // TODO: 全ての接続FDを追加
    
            // TODO: selectを呼び出し
            var timeout = linux.timeval{
                .tv_sec = 5,
                .tv_usec = 0,
            };
    
            // TODO: 準備完了したFDを処理
        }
    }
    
    fn handleNewConnection(
        server: *net.StreamServer,
        connections: *std.ArrayList(os.socket_t)
    ) !void {
        // TODO: 新しい接続を受け付ける
        // TODO: connectionsリストに追加
    }
    
    fn handleClientData(fd: os.socket_t) !bool {
        var buffer: [1024]u8 = undefined;
    
        // TODO: データを読み込む
        // TODO: エコーバック
    
        // true = 接続継続, false = 接続終了
        return true;
    }
    

    実装のヒント

    // fd_setの準備
    var read_fds: linux.fd_set = undefined;
    linux.FD_ZERO(&read_fds);
    
    // FDの追加
    linux.FD_SET(@intCast(server_fd), &read_fds);
    
    // 全接続を追加
    for (connections.items) |conn_fd| {
        linux.FD_SET(@intCast(conn_fd), &read_fds);
        if (conn_fd > max_fd) {
            max_fd = @intCast(conn_fd);
        }
    }
    
    // selectの呼び出し
    const ready = linux.select(
        max_fd + 1,
        &read_fds,
        null,  // write_fds
        null,  // except_fds
        &timeout
    );
    
    // 結果のチェック
    if (ready > 0) {
        // サーバーFDのチェック
        if (linux.FD_ISSET(@intCast(server_fd), &read_fds)) {
            try handleNewConnection(&server, &connections);
        }
    
        // 各接続のチェック
        var i: usize = 0;
        while (i < connections.items.len) {
            const fd = connections.items[i];
            if (linux.FD_ISSET(@intCast(fd), &read_fds)) {
                const keep = try handleClientData(fd);
                if (!keep) {
                    os.close(fd);
                    _ = connections.swapRemove(i);
                    continue;
                }
            }
            i += 1;
        }
    }
    

    テスト方法

    # サーバーの起動
    zig build-exe select_echo_server.zig
    ./select_echo_server
    
    # 負荷テスト
    for i in {1..100}; do
        echo "Client $i" | nc localhost 8080 &
    done
    wait
    
    # タイムアウトのテスト
    nc localhost 8080
    # 5秒待つ
    # タイムアウト後もサーバーが動作しているか確認
    

    チェックリスト

  • [ ] fd_setが正しく管理される
  • [ ] selectが正しく呼び出される
  • [ ] 新しい接続を受け付ける
  • [ ] 既存の接続からデータを読む
  • [ ] タイムアウトが正しく動作する
  • [ ] 接続が閉じられたときに正しくクリーンアップされる
  • 演習4(ボーナス): pollを使ったサーバー

    目標

    pollシステムコールを使ってselectの制限を克服します。

    スターターコード

    const std = @import("std");
    const net = std.net;
    const os = std.os;
    const linux = os.linux;
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
        const allocator = gpa.allocator();
    
        const address = try net.Address.parseIp4("127.0.0.1", 8080);
        var server = net.StreamServer.init(.{});
        defer server.deinit();
    
        try server.listen(address);
    
        var pollfds = std.ArrayList(linux.pollfd).init(allocator);
        defer pollfds.deinit();
    
        // サーバーFDを追加
        try pollfds.append(.{
            .fd = @intCast(server.sockfd.?),
            .events = linux.POLL.IN,
            .revents = 0,
        });
    
        std.debug.print("pollサーバー起動: {}\n", .{address});
    
        while (true) {
            // TODO: pollを呼び出し
    
            // TODO: イベントを処理
        }
    }
    

    実装のヒント

    // pollの呼び出し
    const timeout_ms: i32 = 5000;
    const ready = linux.poll(
        pollfds.items.ptr,
        @intCast(pollfds.items.len),
        timeout_ms
    );
    
    // イベントの処理
    if (ready > 0) {
        var i: usize = 0;
        while (i < pollfds.items.len) {
            const pfd = &pollfds.items[i];
    
            if (pfd.revents & linux.POLL.IN != 0) {
                if (i == 0) {
                    // サーバーFD: 新しい接続
                    try handleNewConnection(&server, &pollfds);
                } else {
                    // クライアントFD: データ読み込み
                    const keep = try handleClientData(pfd.fd);
                    if (!keep) {
                        os.close(pfd.fd);
                        _ = pollfds.swapRemove(i);
                        continue;
                    }
                }
            }
    
            // エラーチェック
            if (pfd.revents & linux.POLL.ERR != 0) {
                // エラー処理
            }
    
            i += 1;
        }
    }
    

    演習5(ボーナス): パフォーマンス測定

    目標

    各実装のパフォーマンスを測定し、比較します。

    測定項目

  • スループット: 1秒あたりのリクエスト数
  • レイテンシ: リクエストの応答時間
  • CPU使用率: プロセスのCPU使用率
  • メモリ使用量: プロセスのメモリ使用量

ベンチマークツール

// benchmark.zig
const std = @import("std");
const net = std.net;
const time = std.time;

pub fn main() !void {
    const args = try std.process.argsAlloc(std.heap.page_allocator);
    defer std.process.argsFree(std.heap.page_allocator, args);

    if (args.len < 3) {
        std.debug.print("使用法: {s} <ホスト> <ポート> [同時接続数] [リクエスト数]\n", .{args[0]});
        return;
    }

    const host = args[1];
    const port = try std.fmt.parseInt(u16, args[2], 10);
    const concurrent = if (args.len > 3) try std.fmt.parseInt(usize, args[3], 10) else 10;
    const requests = if (args.len > 4) try std.fmt.parseInt(usize, args[4], 10) else 1000;

    std.debug.print("ベンチマーク開始...\n", .{});
    std.debug.print("  対象: {s}:{d}\n", .{host, port});
    std.debug.print("  同時接続: {d}\n", .{concurrent});
    std.debug.print("  総リクエスト: {d}\n\n", .{requests});

    const start = time.milliTimestamp();

    // TODO: ベンチマーク実装

    const end = time.milliTimestamp();
    const elapsed = end - start;

    std.debug.print("\n結果:\n", .{});
    std.debug.print("  総時間: {d} ms\n", .{elapsed});
    std.debug.print("  スループット: {d} req/s\n", .{requests * 1000 / @as(usize, @intCast(elapsed))});
    std.debug.print("  平均レイテンシ: {d} ms\n", .{elapsed / requests});
}

テスト方法

# 各サーバーを起動
./blocking_echo_server &
PID_BLOCKING=$!

# ベンチマーク実行
zig build-exe benchmark.zig
./benchmark localhost 8080 100 10000

# CPU使用率の測定
top -p $PID_BLOCKING -b -n 1

期待される結果の比較

ブロッキング (1スレッド):
  スループット: ~100 req/s
  CPU使用率: 低い
  同時接続: 1のみ

ノンブロッキング (ポーリング):
  スループット: ~1,000 req/s
  CPU使用率: 高い (ビジーウェイト)
  同時接続: 制限なし

select:
  スループット: ~5,000 req/s
  CPU使用率: 中程度
  同時接続: ~1000まで

poll:
  スループット: ~5,000 req/s
  CPU使用率: 中程度
  同時接続: 制限なし

演習6(ボーナス): タイムアウト処理

目標

各接続にタイムアウトを実装し、アイドル接続を自動的に閉じます。

実装のヒント

const Connection = struct {
    stream: net.Stream,
    last_activity: i64,  // ミリ秒タイムスタンプ
};

fn checkTimeouts(connections: *std.ArrayList(Connection)) !void {
    const now = std.time.milliTimestamp();
    const timeout_ms: i64 = 30000;  // 30秒

    var i: usize = 0;
    while (i < connections.items.len) {
        const conn = &connections.items[i];
        if (now - conn.last_activity > timeout_ms) {
            std.debug.print("タイムアウト: 接続を閉じます\n", .{});
            conn.stream.close();
            _ = connections.swapRemove(i);
            continue;
        }
        i += 1;
    }
}

提出物

以下のファイルを提出してください:

マンダトリー

  • blocking_echo_server.zig - ブロッキングI/Oサーバー
  • nonblocking_echo_server.zig - ノンブロッキングI/Oサーバー
  • select_echo_server.zig - selectを使ったサーバー
  • README.md - 実装の説明とテスト結果

ボーナス

  • poll_echo_server.zig - pollを使ったサーバー
  • benchmark.zig - パフォーマンス測定ツール
  • PERFORMANCE.md - ベンチマーク結果と分析

トラブルシューティング

よくある問題

問題1: "Address already in use"

# 解決方法
sudo lsof -i :8080
kill <PID>

# または、SO_REUSEADDRを設定
const enable: c_int = 1;
try os.setsockopt(
    server.sockfd.?,
    os.SOL.SOCKET,
    os.SO.REUSEADDR,
    std.mem.asBytes(&enable)
);

問題2: ノンブロッキングでCPU 100%

// 解決方法: 適切なスリープを追加
if (no_data_available) {
    std.time.sleep(1 * std.time.ns_per_ms);
}

問題3: FD_SETSIZEエラー

エラー: FD number too large for fd_set

解決方法: pollを使用するか、FD_SETSIZEを増やす
(ただし、pollの使用を推奨)

まとめ

この演習では以下を実装しました:

  • ブロッキングI/Oの制限を体験
  • ノンブロッキングI/Oの利点と欠点
  • select/pollによるI/O多重化
  • パフォーマンス測定と比較
  • 次のExercise 2では、これらの問題を解決するepollを実装します。

    参考資料

  • Zig Standard Library Documentation
  • Linux man pages: select(2), poll(2)
  • UNIX Network Programming Vol. 1