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点)
- Connection Pool (15点)
実装テンプレート
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点)
- 静的ファイル配信 (15点)
実装例
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サーバーとロードバランサーを実装してください。
要件:
- ワーカースレッドプール
- ロードバランシング戦略
- スレッド間統計の集約
- グレースフルシャットダウン
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() });
}
};
よくあるエラー
- EPIPE (Broken pipe)
- Too many open files
参考資料
期限
提出期限: [指定された日付]
頑張ってください!