概要

HTTP はリクエスト行、ヘッダー、空行、ボディという単純なテキスト構造のプロトコルです。HttpClient や HttpURLConnection はこの構造を抽象化してくれますが、一度は生のソケットで HTTP リクエストを手作りしてみることで、「Host ヘッダーはなぜ必須か」「空行の CRLF がなぜ重要か」「Connection: close は何を意味するか」といった疑問が腹落ちします。この記事では、TCP ソケットを使って HTTP GET リクエストを手動で組み立て、サーバーからのレスポンスをステータス行・ヘッダー・ボディに分解して読み取ります。フレームワークの裏で何が起きているかを一度理解しておくと、通信トラブルの原因特定が格段に速くなります。

使いどころ

HTTP 通信のトラブルシューティングで、プロトコルレベルの動作を確認する

新人研修や勉強会で、HTTP プロトコルの構造を手を動かして理解する

プロキシやロードバランサーの動作検証で、生の HTTP リクエストを送信して応答を確認する

コード例

TCP ソケットで HTTP GET リクエストを手動送信する
import java.io.*;

public class HttpSocketSample {

    record HttpResponse(int statusCode, String statusMessage,
                        Map<String, String> headers, String body) {
        String header(String name) {
            return headers.getOrDefault(name.toLowerCase(), "");
        }
    }

    static HttpResponse sendGet(String host, int port, String path)
            throws IOException {
        try (var socket = new Socket(host, port)) {
            var request = "GET " + path + " HTTP/1.1\r\n"
                    + "Host: " + host + "\r\n"
                    + "User-Agent: JavaSocketClient/1.0\r\n"
                    + "Connection: close\r\n"
                    + "\r\n";

            var writer = new PrintWriter(
                new OutputStreamWriter(socket.getOutputStream()), true);
            writer.print(request);
            writer.flush();

            var reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));

            var statusLine = reader.readLine();
            int statusCode = 0;
            String statusMessage = "";
            if (statusLine != null && statusLine.startsWith("HTTP/")) {
                var parts = statusLine.split(" ", 3);
                if (parts.length >= 2) {
                    statusCode = Integer.parseInt(parts[1]);
                    statusMessage = parts.length > 2 ? parts[2] : "";
                }
            }

            var headers = new LinkedHashMap<String, String>();
            String line;
            while ((line = reader.readLine()) != null && !line.isEmpty()) {
                int colon = line.indexOf(':');
                if (colon > 0) {
                    headers.put(
                        line.substring(0, colon).trim().toLowerCase(),
                        line.substring(colon + 1).trim());
                }
            }

            var body = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                body.append(line).append("\n");
            }

            return new HttpResponse(statusCode, statusMessage,
                headers, body.toString());
        }
    }

    public static void main(String[] args) {
        try {
            var response = sendGet("example.com", 80, "/");
            System.out.println("ステータス: " + response.statusCode()
                + " " + response.statusMessage());
            System.out.println("Content-Type: "
                + response.header("content-type"));
            int len = Math.min(response.body().length(), 200);
            System.out.println("ボディ(先頭 " + len + " 文字): "
                + response.body().substring(0, len));
        } catch (IOException e) {
            System.out.println("接続エラー: " + e.getMessage());
        }

        System.out.println("\n注意: 実務では HttpClient を使用してください");
    }
}

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

Version Coverage

record でレスポンスを不変オブジェクトとして表現し、テキストブロックでリクエスト例を読みやすく記述できる。

Java 17
// Java 17: record でレスポンスを表現
record HttpResponse(int statusCode,
    String statusMessage,
    Map<String, String> headers, String body) {
    String header(String name) {
        return headers.getOrDefault(
            name.toLowerCase(), "");
    }
}

String example = """
    GET /path HTTP/1.1
    Host: example.com
    Connection: close
    (空行)
    """;

Library Comparison

標準 API(Socket + 手動リクエスト構築)HTTP プロトコルの学習や、プロキシ・ロードバランサーの動作検証。TLS、チャンク転送、リダイレクト追跡などは全て自前で実装する必要がある。実用には向かない。
標準 HttpClient(Java 11+)実務での HTTP 通信。プロトコルの詳細を意識せずに使える。プロトコルの仕組みは隠蔽される。
curl / telnet コマンドHTTP リクエストの手動テスト。Java コードを書かずに確認できる。Java コードへの組み込みはできない。

注意点

この記事のコードは HTTP プロトコルの仕組み理解が目的。実務では HttpClient または HttpURLConnection を使うこと

HTTP/1.1 では Host ヘッダーが必須。省略するとサーバーが 400 Bad Request を返す場合がある

Connection: close を指定しないと、サーバーがキープアライブで接続を保持し、読み取りがブロックする場合がある

HTTPS(TLS)には対応していない。HTTPS サーバーへの接続には SSLSocket が必要

FAQ

HTTPS サーバーにもソケットで接続できますか。

SSLSocketFactory で SSLSocket を作成すれば TLS 接続が可能です。ただし証明書検証の設定が必要になるため、実務では HttpClient を使うのが安全です。

HTTP/2 にもソケットで対応できますか。

HTTP/2 はバイナリプロトコルのため、テキストベースの手作りリクエストでは対応できません。HttpClient は HTTP/2 をサポートしています。

レスポンスボディが途中で切れる場合はどうすればよいですか。

Transfer-Encoding: chunked の場合、チャンクサイズの解析が必要です。Connection: close であればソケットが閉じるまで読めば全データを取得できます。

関連書籍

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

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