Zig選択課題05:ネットワークプログラミング

課題説明

背景

ネットワークプログラミングは、分散システムやWebアプリケーション開発の基礎となる重要な技術です。Zigは、低レベルのソケットプログラミングから高レベルのプロトコル実装まで、幅広いネットワークプログラミングに対応できます。標準ライブラリには、非同期I/O、TCPソケット、UDPソケットなどの機能が含まれており、効率的なネットワークアプリケーションを構築できます。この課題では、TCPサーバー/クライアント、HTTPサーバー、そして非同期イベントループを実装し、ネットワークプログラミングの基本を学びます。

要件

必須機能

  • TCPサーバー/クライアント
- ソケットの作成とバインド - 接続の受け入れ - データの送受信 - マルチクライアント対応

  • HTTPサーバー
- HTTPリクエストのパース - HTTPレスポンスの生成 - ルーティング機能 - 静的ファイルの配信

  • 非同期I/O
- イベントループの実装 - ノンブロッキングソケット - select/poll/epollの使用 - タイムアウト処理

  • プロトコル実装
- カスタムバイナリプロトコル - エンディアン変換 - フレーミング - エラーハンドリング

  • セキュリティ
- TLS/SSL(オプション) -入力バリデーション - バッファオーバーフロー対策

技術的制約

  • Zigの標準ライブラリを使用
  • POSIXソケットAPI
  • 非同期I/Oの実装
  • メモリリークのない実装
  • 適切なエラーハンドリング
  • 評価ポイント

  • ソケットプログラミングの正確な実装
  • マルチクライアント処理の効率性
  • プロトコル実装の正確性
  • 非同期I/Oの適切な使用
  • エラーハンドリングの堅牢性
  • コードの可読性と保守性
  • ---

    想定解答

    プロジェクト構造

    zig-network/
    ├── src/
    │   ├── main.zig
    │   ├── tcp_server.zig
    │   ├── tcp_client.zig
    │   ├── http/
    │   │   ├── server.zig
    │   │   ├── request.zig
    │   │   ├── response.zig
    │   │   └── router.zig
    │   ├── async/
    │   │   ├── event_loop.zig
    │   │   └── poller.zig
    │   └── protocol/
    │       ├── frame.zig
    │       └── codec.zig
    ├── build.zig
    └── public/
        └── index.html
    

    src/tcp_server.zig

    const std = @import("std");
    const net = std.net;
    const os = std.os;
    
    pub const TcpServer = struct {
        allocator: std.mem.Allocator,
        address: net.Address,
        server: net.Server,
        running: bool,
    
        pub fn init(allocator: std.mem.Allocator, host: []const u8, port: u16) !TcpServer {
            const address = try net.Address.parseIp(host, port);
    
            var server = try address.listen(.{
                .reuse_address = true,
                .reuse_port = true,
            });
    
            return TcpServer{
                .allocator = allocator,
                .address = address,
                .server = server,
                .running = false,
            };
        }
    
        pub fn deinit(self: *TcpServer) void {
            self.server.deinit();
        }
    
        pub fn start(self: *TcpServer) !void {
            self.running = true;
            std.debug.print("TCP Server listening on {}\n", .{self.address});
    
            while (self.running) {
                const connection = try self.server.accept();
                std.debug.print("New connection from {}\n", .{connection.address});
    
                // 新しいスレッドでクライアントを処理
                const thread = try std.Thread.spawn(.{}, handleClient, .{ self.allocator, connection });
                thread.detach();
            }
        }
    
        pub fn stop(self: *TcpServer) void {
            self.running = false;
        }
    };
    
    fn handleClient(allocator: std.mem.Allocator, connection: net.Server.Connection) !void {
        defer connection.stream.close();
    
        var buffer: [4096]u8 = undefined;
    
        while (true) {
            const bytes_read = connection.stream.read(&buffer) catch |err| {
                std.debug.print("Read error: {}\n", .{err});
                break;
            };
    
            if (bytes_read == 0) {
                std.debug.print("Client disconnected\n", .{});
                break;
            }
    
            std.debug.print("Received {} bytes: {s}\n", .{ bytes_read, buffer[0..bytes_read] });
    
            // エコーバック
            _ = connection.stream.write(buffer[0..bytes_read]) catch |err| {
                std.debug.print("Write error: {}\n", .{err});
                break;
            };
        }
    }
    

    src/tcp_client.zig

    const std = @import("std");
    const net = std.net;
    
    pub const TcpClient = struct {
        allocator: std.mem.Allocator,
        stream: net.Stream,
    
        pub fn connect(allocator: std.mem.Allocator, host: []const u8, port: u16) !TcpClient {
            const address = try net.Address.parseIp(host, port);
            const stream = try net.tcpConnectToAddress(address);
    
            return TcpClient{
                .allocator = allocator,
                .stream = stream,
            };
        }
    
        pub fn deinit(self: *TcpClient) void {
            self.stream.close();
        }
    
        pub fn send(self: *TcpClient, data: []const u8) !usize {
            return try self.stream.write(data);
        }
    
        pub fn receive(self: *TcpClient, buffer: []u8) !usize {
            return try self.stream.read(buffer);
        }
    
        pub fn sendAndReceive(self: *TcpClient, data: []const u8, response_buffer: []u8) ![]u8 {
            const bytes_sent = try self.send(data);
            std.debug.print("Sent {} bytes\n", .{bytes_sent});
    
            const bytes_received = try self.receive(response_buffer);
            return response_buffer[0..bytes_received];
        }
    };
    

    src/http/server.zig

    const std = @import("std");
    const net = std.net;
    const Request = @import("request.zig").Request;
    const Response = @import("response.zig").Response;
    const Router = @import("router.zig").Router;
    
    pub const HttpServer = struct {
        allocator: std.mem.Allocator,
        address: net.Address,
        server: net.Server,
        router: Router,
        running: bool,
    
        pub fn init(allocator: std.mem.Allocator, host: []const u8, port: u16) !HttpServer {
            const address = try net.Address.parseIp(host, port);
    
            var server = try address.listen(.{
                .reuse_address = true,
            });
    
            return HttpServer{
                .allocator = allocator,
                .address = address,
                .server = server,
                .router = Router.init(allocator),
                .running = false,
            };
        }
    
        pub fn deinit(self: *HttpServer) void {
            self.router.deinit();
            self.server.deinit();
        }
    
        pub fn start(self: *HttpServer) !void {
            self.running = true;
            std.debug.print("HTTP Server listening on http://{}\n", .{self.address});
    
            while (self.running) {
                const connection = try self.server.accept();
    
                const thread = try std.Thread.spawn(.{}, handleHttpClient, .{
                    self.allocator,
                    connection,
                    &self.router,
                });
                thread.detach();
            }
        }
    
        pub fn get(self: *HttpServer, path: []const u8, handler: Router.Handler) !void {
            try self.router.addRoute(.GET, path, handler);
        }
    
        pub fn post(self: *HttpServer, path: []const u8, handler: Router.Handler) !void {
            try self.router.addRoute(.POST, path, handler);
        }
    };
    
    fn handleHttpClient(allocator: std.mem.Allocator, connection: net.Server.Connection, router: *Router) !void {
        defer connection.stream.close();
    
        var buffer: [8192]u8 = undefined;
        const bytes_read = try connection.stream.read(&buffer);
    
        if (bytes_read == 0) return;
    
        // リクエストのパース
        var request = try Request.parse(allocator, buffer[0..bytes_read]);
        defer request.deinit();
    
        std.debug.print("{s} {s}\n", .{ @tagName(request.method), request.path });
    
        // ルーターでハンドラを検索
        var response = if (router.findHandler(request.method, request.path)) |handler|
            try handler(allocator, &request)
        else
            Response.notFound(allocator);
    
        defer response.deinit();
    
        // レスポンスの送信
        const response_data = try response.build();
        defer allocator.free(response_data);
    
        _ = try connection.stream.write(response_data);
    }
    

    src/http/request.zig

    const std = @import("std");
    
    pub const Method = enum {
        GET,
        POST,
        PUT,
        DELETE,
        HEAD,
        OPTIONS,
        PATCH,
    
        pub fn fromString(str: []const u8) !Method {
            if (std.mem.eql(u8, str, "GET")) return .GET;
            if (std.mem.eql(u8, str, "POST")) return .POST;
            if (std.mem.eql(u8, str, "PUT")) return .PUT;
            if (std.mem.eql(u8, str, "DELETE")) return .DELETE;
            if (std.mem.eql(u8, str, "HEAD")) return .HEAD;
            if (std.mem.eql(u8, str, "OPTIONS")) return .OPTIONS;
            if (std.mem.eql(u8, str, "PATCH")) return .PATCH;
            return error.UnknownMethod;
        }
    };
    
    pub const Request = struct {
        allocator: std.mem.Allocator,
        method: Method,
        path: []const u8,
        version: []const u8,
        headers: std.StringHashMap([]const u8),
        body: ?[]const u8,
    
        pub fn parse(allocator: std.mem.Allocator, data: []const u8) !Request {
            var lines = std.mem.split(u8, data, "\r\n");
    
            // リクエストラインのパース
            const request_line = lines.next() orelse return error.InvalidRequest;
            var parts = std.mem.split(u8, request_line, " ");
    
            const method_str = parts.next() orelse return error.InvalidMethod;
            const method = try Method.fromString(method_str);
    
            const path = parts.next() orelse return error.InvalidPath;
            const version = parts.next() orelse return error.InvalidVersion;
    
            // ヘッダーのパース
            var headers = std.StringHashMap([]const u8).init(allocator);
            errdefer headers.deinit();
    
            while (lines.next()) |line| {
                if (line.len == 0) break; // 空行はヘッダーの終わり
    
                if (std.mem.indexOf(u8, line, ": ")) |colon_pos| {
                    const key = line[0..colon_pos];
                    const value = line[colon_pos + 2 ..];
                    try headers.put(key, value);
                }
            }
    
            // ボディの取得(残りのデータ)
            const body = lines.rest();
            const body_data = if (body.len > 0) body else null;
    
            return Request{
                .allocator = allocator,
                .method = method,
                .path = try allocator.dupe(u8, path),
                .version = try allocator.dupe(u8, version),
                .headers = headers,
                .body = if (body_data) |b| try allocator.dupe(u8, b) else null,
            };
        }
    
        pub fn deinit(self: *Request) void {
            self.allocator.free(self.path);
            self.allocator.free(self.version);
            self.headers.deinit();
            if (self.body) |body| {
                self.allocator.free(body);
            }
        }
    
        pub fn getHeader(self: *Request, key: []const u8) ?[]const u8 {
            return self.headers.get(key);
        }
    };
    

    src/http/response.zig

    const std = @import("std");
    
    pub const StatusCode = enum(u16) {
        OK = 200,
        Created = 201,
        NoContent = 204,
        BadRequest = 400,
        NotFound = 404,
        InternalServerError = 500,
    
        pub fn text(self: StatusCode) []const u8 {
            return switch (self) {
                .OK => "OK",
                .Created => "Created",
                .NoContent => "No Content",
                .BadRequest => "Bad Request",
                .NotFound => "Not Found",
                .InternalServerError => "Internal Server Error",
            };
        }
    };
    
    pub const Response = struct {
        allocator: std.mem.Allocator,
        status: StatusCode,
        headers: std.StringHashMap([]const u8),
        body: []const u8,
    
        pub fn init(allocator: std.mem.Allocator, status: StatusCode) Response {
            return Response{
                .allocator = allocator,
                .status = status,
                .headers = std.StringHashMap([]const u8).init(allocator),
                .body = "",
            };
        }
    
        pub fn deinit(self: *Response) void {
            self.headers.deinit();
            if (self.body.len > 0) {
                self.allocator.free(self.body);
            }
        }
    
        pub fn ok(allocator: std.mem.Allocator) Response {
            return init(allocator, .OK);
        }
    
        pub fn notFound(allocator: std.mem.Allocator) Response {
            var response = init(allocator, .NotFound);
            response.body = allocator.dupe(u8, "404 Not Found") catch "";
            return response;
        }
    
        pub fn setHeader(self: *Response, key: []const u8, value: []const u8) !void {
            try self.headers.put(key, value);
        }
    
        pub fn setBody(self: *Response, body: []const u8) !void {
            self.body = try self.allocator.dupe(u8, body);
        }
    
        pub fn json(self: *Response, data: []const u8) !void {
            try self.setHeader("Content-Type", "application/json");
            try self.setBody(data);
        }
    
        pub fn html(self: *Response, data: []const u8) !void {
            try self.setHeader("Content-Type", "text/html");
            try self.setBody(data);
        }
    
        pub fn build(self: *Response) ![]u8 {
            var list = std.ArrayList(u8).init(self.allocator);
            errdefer list.deinit();
    
            // ステータスライン
            try list.writer().print("HTTP/1.1 {} {s}\r\n", .{
                @intFromEnum(self.status),
                self.status.text(),
            });
    
            // ヘッダー
            var it = self.headers.iterator();
            while (it.next()) |entry| {
                try list.writer().print("{s}: {s}\r\n", .{ entry.key_ptr.*, entry.value_ptr.* });
            }
    
            // Content-Length
            try list.writer().print("Content-Length: {}\r\n", .{self.body.len});
    
            // 空行
            try list.appendSlice("\r\n");
    
            // ボディ
            try list.appendSlice(self.body);
    
            return list.toOwnedSlice();
        }
    };
    

    src/http/router.zig

    const std = @import("std");
    const Request = @import("request.zig").Request;
    const Response = @import("response.zig").Response;
    
    pub const Router = struct {
        allocator: std.mem.Allocator,
        routes: std.ArrayList(Route),
    
        pub const Handler = *const fn (std.mem.Allocator, *Request) anyerror!Response;
    
        const Route = struct {
            method: Request.Method,
            path: []const u8,
            handler: Handler,
        };
    
        pub fn init(allocator: std.mem.Allocator) Router {
            return Router{
                .allocator = allocator,
                .routes = std.ArrayList(Route).init(allocator),
            };
        }
    
        pub fn deinit(self: *Router) void {
            self.routes.deinit();
        }
    
        pub fn addRoute(self: *Router, method: Request.Method, path: []const u8, handler: Handler) !void {
            try self.routes.append(.{
                .method = method,
                .path = path,
                .handler = handler,
            });
        }
    
        pub fn findHandler(self: *Router, method: Request.Method, path: []const u8) ?Handler {
            for (self.routes.items) |route| {
                if (route.method == method and std.mem.eql(u8, route.path, path)) {
                    return route.handler;
                }
            }
            return null;
        }
    };
    

    src/main.zig

    const std = @import("std");
    const HttpServer = @import("http/server.zig").HttpServer;
    const Request = @import("http/request.zig").Request;
    const Response = @import("http/response.zig").Response;
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
        const allocator = gpa.allocator();
    
        var server = try HttpServer.init(allocator, "127.0.0.1", 8080);
        defer server.deinit();
    
        // ルートの登録
        try server.get("/", indexHandler);
        try server.get("/api/hello", helloHandler);
        try server.post("/api/echo", echoHandler);
    
        std.debug.print("Starting HTTP server...\n", .{});
        try server.start();
    }
    
    fn indexHandler(allocator: std.mem.Allocator, request: *Request) !Response {
        _ = request;
        var response = Response.ok(allocator);
        try response.html(
            \\<!DOCTYPE html>
            \\<html>
            \\<head><title>Zig HTTP Server</title></head>
            \\<body>
            \\<h1>Welcome to Zig HTTP Server!</h1>
            \\<p>This is a simple HTTP server written in Zig.</p>
            \\</body>
            \\</html>
        );
        return response;
    }
    
    fn helloHandler(allocator: std.mem.Allocator, request: *Request) !Response {
        _ = request;
        var response = Response.ok(allocator);
        try response.json("{\"message\": \"Hello from Zig!\"}");
        return response;
    }
    
    fn echoHandler(allocator: std.mem.Allocator, request: *Request) !Response {
        var response = Response.ok(allocator);
        if (request.body) |body| {
            try response.setBody(body);
        } else {
            try response.setBody("No body");
        }
        return response;
    }
    

    ---

    解説と応用

    ネットワークプログラミングの基礎

  • ソケットAPI
- socket() - ソケット作成 - bind() - アドレスバインド - listen() - 接続待ち受け - accept() - 接続受け入れ - connect() - サーバー接続

  • 非同期I/O
- ノンブロッキングモード - select/poll/epoll - イベント駆動アーキテクチャ

  • プロトコル設計
- リクエスト/レスポンス - フレーミング - エラーハンドリング

パフォーマンス最適化

  • 接続プール
- コネクションの再利用 - リソース管理

  • バッファリング
- 効率的なI/O - メモリ使用量の最適化

---

参考リソース

公式ドキュメント

プロジェクト

---

発展課題

レベル1:基本機能の拡張

  • WebSocketサーバー
  • ファイルアップロード対応
  • Cookie/Session管理

レベル2:高度な機能

  • HTTP/2サポート
  • リバースプロキシ
  • ロードバランサー

レベル3:最適化

  • ゼロコピーI/O
  • io_uring対応
  • 高性能非同期ランタイム

---

まとめ

Zigによるネットワークプログラミングでは、低レベル制御と安全性を両立しながら、高性能なネットワークアプリケーションを開発できます。標準ライブラリの強力なネットワーク機能と、Zigの言語特性を活用することで、効率的で保守性の高いコードを実装できます。