Exercise 1: I/Oモデル進化の実践演習
演習の目的
この演習では、Chapter 1とExplanation 1で学んだI/Oモデルの概念を実践的に理解します。ブロッキング、ノンブロッキング、そしてselect/pollを使った実装を通じて、各モデルの特性とトレードオフを体験します。
前提条件
- Zigの基本的な構文理解
- LinuxまたはmacOS環境
- ターミナルとテキストエディタ
- [ ] ブロッキングI/Oサーバーの実装(20点)
- [ ] ノンブロッキングI/Oサーバーの実装(20点)
- [ ] selectを使ったサーバーの実装(20点)
- [ ] 各実装の動作確認とテスト(20点)
- [ ] pollを使ったサーバーの実装(5点)
- [ ] パフォーマンス測定とベンチマーク(5点)
- [ ] タイムアウト処理の実装(5点)
- [ ] エラーハンドリングの改善(5点)
blocking_echo_server.zigファイルを作成- TCPソケットをバインドしてリスニング
- 接続を受け付ける
- データを受信してエコーバック
- 接続を閉じる
評価基準
マンダトリー要件(80点)
以下の要件を全て満たすことで基本点が獲得できます:
ボーナス要件(20点)
以下の高度な機能を実装することで追加点が獲得できます:
演習1: ブロッキングI/Oエコーサーバー
目標
最もシンプルなブロッキングI/Oを使ったエコーサーバーを実装します。このサーバーは、クライアントから受信したデータをそのまま返します。
実装手順
スターターコード
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!
チェックリスト
演習2: ノンブロッキングI/Oエコーサーバー
目標
ノンブロッキングI/Oを使って、複数の接続を処理できるサーバーを実装します。
実装手順
nonblocking_echo_server.zigファイルを作成スターターコード
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 &
# 各クライアントからメッセージを送信
# すべてのクライアントが応答を受け取るはず
チェックリスト
演習3: selectを使ったI/O多重化サーバー
目標
selectシステムコールを使って効率的なI/O多重化サーバーを実装します。
実装手順
select_echo_server.zigファイルを作成スターターコード
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秒待つ
# タイムアウト後もサーバーが動作しているか確認
チェックリスト
演習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(ボーナス): パフォーマンス測定
目標
各実装のパフォーマンスを測定し、比較します。
測定項目
ベンチマークツール
// 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多重化
- パフォーマンス測定と比較
- Zig Standard Library Documentation
- Linux man pages: select(2), poll(2)
- UNIX Network Programming Vol. 1
次のExercise 2では、これらの問題を解決するepollを実装します。