Java NIOを活用したノンブロッキング通信と簡易HTTPサーバーの実装

NIOチャネルの非同期制御とデータ転送

JavaのNIO(New I/O)アーキテクチャでは、データの格納領域であるバッファと、通信経路を抽象化するチャネルの連携が処理の核となります。クライアント側実装では、チャネルインスタンスを生成し、ターゲットサーバーのエンドポイントへ接続を確立した後、バッファを経由してバイト列を転送します。

基本的な実装フローは以下の通りです:

  • チャネルインスタンスの生成
  • 接続先アドレスおよびポートの指定
  • バッファへのデータ書き込みとチャネル送信
  • リソースの適切な解放
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class NioClientEndpoint {
    public static void main(String[] args) throws IOException {
        // チャネルの初期化と自動クローズ
        try (SocketChannel clientSock = SocketChannel.open()) {
            // 接続先設定
            clientSock.connect(new InetSocketAddress("127.0.0.1", 10000));

            // ペイロードのバッファリングと送信
            byte[] payload = "接続テストメッセージ".getBytes(StandardCharsets.UTF_8);
            ByteBuffer txBuffer = ByteBuffer.wrap(payload);
            clientSock.write(txBuffer);
        }
    }
}

サーバー側チャネルの多重化制御

サーバー実装では、ServerSocketChannelが接続受付を担当し、実際のデータ転送を行うクライアント用チャネルを内部で動的に生成します。チャネルのブロックモードを無効化することで、単一スレッドでの多重接続対応が可能になります。接続監視ループ内では、未接続状態において処理がブロックされず即座に制御が戻る特性を利用します。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;

public class NioBlockingServer {
    public static void main(String[] args) throws IOException {
        // リスニングチャネルの生成
        try (ServerSocketChannel listenCh = ServerSocketChannel.open()) {
            listenCh.bind(new InetSocketAddress(10000));
            listenCh.configureBlocking(false);

            // ノンブロッキング監視ループ
            while (true) {
                SocketChannel workerCh = listenCh.accept();
                if (workerCh == null) continue;

                try {
                    ByteBuffer rxBuffer = ByteBuffer.allocate(1024);
                    int bytesRead = workerCh.read(rxBuffer);
                    
                    if (bytesRead > 0) {
                        rxBuffer.flip();
                        String received = new String(rxBuffer.array(), 0, bytesRead, StandardCharsets.UTF_8);
                        System.out.println("受信データ: " + received);

                        // レスポンス返送
                        ByteBuffer txBuffer = ByteBuffer.wrap("ACK受信完了".getBytes(StandardCharsets.UTF_8));
                        workerCh.write(txBuffer);
                    }
                } finally {
                    workerCh.close();
                }
            }
        }
    }
}

セレクターによるI/Oイベント駆動モデル

大規模な同時接続を効率的に処理するには、Selectorコンポーネントを用いたイベント駆動型モデルが不可欠です。セレクターは登録された複数のチャネルの状態を監視し、接続要求(OP_ACCEPT)やデータ読み取り準備(OP_READ)が発生した際にのみ処理をトリガーします。これにより、アイドル状態でのCPUスリープを防ぎ、リソースを最適配分します。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

public class NioSelectorServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverCh = ServerSocketChannel.open();
        serverCh.bind(new InetSocketAddress(10000));
        serverCh.configureBlocking(false);

        Selector eventDispatcher = Selector.open();
        serverCh.register(eventDispatcher, SelectionKey.OP_ACCEPT);

        while (true) {
            eventDispatcher.select();
            Set<SelectionKey> readyKeys = eventDispatcher.selectedKeys();
            Iterator<SelectionKey> iter = readyKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();

                if (key.isAcceptable()) {
                    ServerSocketChannel src = (ServerSocketChannel) key.channel();
                    SocketChannel clientCh = src.accept();
                    if (clientCh != null) {
                        clientCh.configureBlocking(false);
                        clientCh.register(eventDispatcher, SelectionKey.OP_READ);
                    }
                } else if (key.isReadable()) {
                    SocketChannel clientCh = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = clientCh.read(buffer);
                    if (len > 0) {
                        buffer.flip();
                        System.out.println(new String(buffer.array(), 0, len, StandardCharsets.UTF_8));
                        clientCh.write(ByteBuffer.wrap("ResponseOK".getBytes(StandardCharsets.UTF_8)));
                    }
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
        }
    }
}

HTTPプロトコルの構造と解析要件

HTTPはTCP/IP基盤上で動作するアプリケーション層プロトコルであり、クライアントからのリクエストとサーバーからのレスポンスで構成されます。リクエストメッセージは「リクエスト行」「リクエストヘッダー」「空行」「リクエストボディ」の4要素で定義され、レスポンスも同様に「ステータス行」「ヘッダー」「空行」「ボディ」から成ります。HTTP/1.1以降は持続的接続が標準化され、ハンドシェイクオーバーヘッドの削減に貢献しています。

NIOを活用した簡易HTTPサーバーの実装

ブラウザからのHTTPリクエストをNIOチャネルで受け付け、パースして静的コンテンツやステータスコードを返すサーバーアーキテクチャを構築します。受信データをリクエストクラスにマッピングし、URIに基づいて適切なMIMEタイプとHTTPステータスを設定してレスポンスクラス経由で送信します。

リクエストパーサーの実装:

import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class HttpRequestParser {
    private String method;
    private String uriPath;
    private String httpVersion;
    private final Map<String, String> headers = new HashMap<>();

    public void extract(SelectionKey key) throws IOException {
        SocketChannel ch = (SocketChannel) key.channel();
        StringBuilder rawRequest = new StringBuilder();
        ByteBuffer buffer = ByteBuffer.allocate(4096);
        int readCount;

        while ((readCount = ch.read(buffer)) > 0) {
            buffer.flip();
            rawRequest.append(new String(buffer.array(), 0, readCount, StandardCharsets.UTF_8));
            buffer.clear();
        }

        parseProtocol(rawRequest.toString());
    }

    private void parseProtocol(String rawData) {
        if (rawData.isEmpty()) return;
        String[] lines = rawData.split("\\r\\n");
        String[] requestLine = lines[0].split(" ");
        
        this.method = requestLine.length > 0 ? requestLine[0] : null;
        this.uriPath = requestLine.length > 1 ? requestLine[1] : null;
        this.httpVersion = requestLine.length > 2 ? requestLine[2] : null;

        for (int i = 1; i < lines.length; i++) {
            if (lines[i].contains(": ")) {
                String[] pair = lines[i].split(": ", 2);
                headers.put(pair[0], pair[1]);
            }
        }
    }
    // アクセサメソッドは環境に応じて実装
}

レスポンスビルダーの実装:

import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Paths;

public class HttpResponseBuilder {
    private String version = "HTTP/1.1";
    private String statusCode = "200";
    private String statusPhrase = "OK";
    private final Map<String, String> responseHeaders = new HashMap<>();

    public void dispatch(SelectionKey key, HttpRequestParser request) throws IOException {
        SocketChannel ch = (SocketChannel) key.channel();
        String target = request.getUriPath() == null ? "/" : request.getUriPath();
        String resourcePath = "webroot" + target;

        if (!Files.exists(Paths.get(resourcePath))) {
            statusCode = "404";
            statusPhrase = "Not Found";
            responseHeaders.put("Content-Type", "text/html; charset=UTF-8");
        } else {
            responseHeaders.put("Content-Type", getMimeType(target));
        }

        StringBuilder headerBuilder = new StringBuilder();
        headerBuilder.append(version).append(" ").append(statusCode).append(" ").append(statusPhrase).append("\r\n");
        for (Map.Entry<String, String> entry : responseHeaders.entrySet()) {
            headerBuilder.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
        }
        headerBuilder.append("\r\n");

        ch.write(ByteBuffer.wrap(headerBuilder.toString().getBytes(StandardCharsets.UTF_8)));

        byte[] content;
        if ("404".equals(statusCode)) {
            content = "Resource Missing".getBytes(StandardCharsets.UTF_8);
        } else {
            content = Files.readAllBytes(Paths.get(resourcePath));
        }
        ch.write(ByteBuffer.wrap(content));
        ch.close();
    }

    private String getMimeType(String uri) {
        if (uri.endsWith(".html") || uri.equals("/")) return "text/html; charset=UTF-8";
        if (uri.endsWith(".jpg") || uri.endsWith(".jpeg")) return "image/jpeg";
        if (uri.endsWith(".png")) return "image/png";
        if (uri.endsWith(".ico")) return "image/x-icon";
        return "application/octet-stream";
    }
}

メインループとの統合により、セレクターが接続イベントを検知した際にパーサーを起動し、解析完了後にレスポンスビルダーへ処理を委譲することで、ノンブロッキングかつスケーラブルなHTTPサーバー基盤が完成します。イベントループ内でのキー削除処理とバッファの適切なフリップ操作は、メモリリークと無限ループを防止する上で必須の制御フローとなります。

タグ: java.nio IO-Multiplexing selector-api http-parser socket-channel

7月21日 21:23 投稿