概要

HTTP サーバー自作連載の第1回として、まずは最小構成のサーバーを組み立てます。ここで押さえたいのは、クライアントから TCP 接続を受け付け、HTTP リクエスト行を読み、パスに応じてレスポンスを返すまでの一連の流れです。Spring Boot や Javalin を使えばこの部分は隠れますが、最初に手で書いておくと、以降のクエリ文字列、POST、静的配信、Cookie といった話がかなり追いやすくなります。この記事では、学習用の最小サーバーとしてどこまで実装すれば十分かに絞って整理します。

使いどころ

HTTP サーバーの最小構成を手で追い、リクエストからレスポンスまでの流れを理解したい

後続のクエリ文字列や POST 処理に進む前に、土台になる処理だけを整理したい

テスト用や検証用の最小サーバーがどの程度のコード量になるかを確認したい

コード例

最小構成の HTTP サーバーを ServerSocket で自作する
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MinimalHttpServerSample {

    static final int PORT = 8080;

    // HTTP レスポンスを組み立てて送信
    static void sendResponse(OutputStream out, int statusCode,
                              String statusMessage, String contentType,
                              String body) throws IOException {
        var writer = new PrintWriter(
            new OutputStreamWriter(out, "UTF-8"), true);
        var bodyBytes = body.getBytes("UTF-8");
        writer.print("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n");
        writer.print("Content-Type: " + contentType + "; charset=UTF-8\r\n");
        writer.print("Content-Length: " + bodyBytes.length + "\r\n");
        writer.print("Connection: close\r\n");
        writer.print("\r\n");
        writer.flush();
        out.write(bodyBytes);
        out.flush();
    }

    // リクエストを処理してレスポンスを返す
    static void handleRequest(Socket clientSocket) throws IOException {
        try (var in = clientSocket.getInputStream();
             var out = clientSocket.getOutputStream()) {

            var reader = new BufferedReader(
                new InputStreamReader(in, "UTF-8"));

            // リクエスト行を読む(例: GET /hello HTTP/1.1)
            var requestLine = reader.readLine();
            System.out.println("リクエスト: " + requestLine);

            // ヘッダーを読み飛ばす
            String line;
            while ((line = reader.readLine()) != null && !line.isEmpty()) {
                // ヘッダー行を読み飛ばす
            }

            // リクエスト行をパース
            String method = "GET";
            String path = "/";
            if (requestLine != null && !requestLine.isEmpty()) {
                var parts = requestLine.split(" ");
                if (parts.length >= 2) {
                    method = parts[0];
                    path = parts[1];
                }
            }

            if ("GET".equals(method)) {
                if ("/".equals(path)) {
                    var html = """
                            <html><body>
                            <h1>Hello, Java HTTP Server!</h1>
                            <p>Socket ベースの最小 HTTP サーバーです。</p>
                            <p><a href='/hello'>Hello ページへ</a></p>
                            </body></html>
                            """;
                    sendResponse(out, 200, "OK", "text/html", html);
                } else if ("/hello".equals(path)) {
                    sendResponse(out, 200, "OK", "text/plain",
                        "Hello, World!");
                } else {
                    sendResponse(out, 404, "Not Found", "text/html",
                        "<html><body><h1>404 Not Found</h1></body></html>");
                }
            } else {
                sendResponse(out, 405, "Method Not Allowed",
                    "text/plain", "405 Method Not Allowed");
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        System.out.println("HTTP サーバー起動中... http://localhost:" + PORT);

        try (var serverSocket = new ServerSocket(PORT)) {
            while (true) {
                var clientSocket = serverSocket.accept();
                executor.submit(() -> {
                    try {
                        handleRequest(clientSocket);
                    } catch (IOException e) {
                        System.out.println("エラー: " + e.getMessage());
                    } finally {
                        try { clientSocket.close(); }
                        catch (IOException ignored) {}
                    }
                });
            }
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

テキストブロックと `var` により、レスポンス HTML や補助コードを Java 8 より読みやすく書ける。

Java 17
// Java 17: テキストブロック + var
var html = """
    <html><body>
    <h1>Hello, Java HTTP Server!</h1>
    <p>Socket ベースの最小サーバーです。</p>
    </body></html>
    """;
var executor = Executors.newFixedThreadPool(10);
try (var serverSocket = new ServerSocket(PORT)) {
    while (true) {
        var client = serverSocket.accept();
        executor.submit(() -> {
            try { handleRequest(client); }
            catch (IOException e) { /* */ }
        });
    }
}

Library Comparison

標準 API(ServerSocket + 手動パース)HTTP サーバーの最小構成を手で追いたいとき。リクエスト行、ヘッダー、レスポンス組み立ての流れを把握しやすい。学習には向くが、運用に必要な機能はほぼ自前で補う必要がある。
com.sun.net.httpserver.HttpServerSocket の受け付け処理を少し省き、簡易サーバーの骨格だけ確認したいとき。JDK だけで試せる。HTTP メッセージをどこまで自分で組み立てているかは見えにくくなる。
Apache / nginx / Tomcat / Spring Boot などの既製基盤実際のアプリケーションや公開運用を考えるとき。サーバー運用や周辺機能も含めて整理しやすい。HTTP の低レイヤは抽象化されるため、仕組みを手で追う学習には向かない。

注意点

この実装は学習用の最小構成であり、認証、入力検証、HTTPS など実務で必要になる要素はまだ扱わない

HTTP リクエスト行が null のままになるケースを考慮しないと、即切断したクライアントで例外になりやすい

Content-Length は文字数ではなく UTF-8 のバイト数で設定する必要がある

ブラウザは `/favicon.ico` も取りに来るため、最小サーバーでも 404 の流れを見ておくと挙動を追いやすい

Java 8 / 17 ではスレッドプール設計が必要で、Java 21 では Virtual Thread に置き換えやすい

FAQ

このサンプルをそのまま公開運用してよいですか。

勧めません。この記事のコードは HTTP の流れを理解するための最小構成であり、公開運用や実アプリの基盤として使う前提ではありません。

この HTTP サーバーで静的ファイルを配信できますか。

技術的には可能です。パスからファイルを読み込み、Content-Type を付けて返せば動きますが、実際にはパス正規化やキャッシュ制御も必要になります。

同時アクセスが増えるとどうなりますか。

Java 8 / 17 では ExecutorService のスレッドプールサイズが上限になります。Java 21 では Virtual Thread を使うことで、接続ごとの処理をより素直に書けます。

HTTPS に対応させるにはどうすればよいですか。

Java 単体でも SSLServerSocket を使えば実装できます。ただし証明書や更新手順まで含めると話が大きくなるため、学習の次の段階では既製の基盤と合わせて考えることが多いです。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます