java-recipes

ホーム ネットワーク › N-07: HTTP(TCP ソケット低レベル)

N-07: HTTP リクエスト(TCP ソケット低レベル実装)

TCP ソケットで HTTP リクエストを手作りし、HTTP プロトコルの仕組みを理解するサンプルです。 リクエスト行・ヘッダー・空行・ボディという構造、レスポンスのステータス行解析など HTTP/1.1 の基礎を学べます。

⚠️ このページは「仕組みを理解する」ための低レベル実装です

実務で HTTP 通信するときは N-01(HttpClient、Java 11〜) または N-02(HttpURLConnection、Java 8〜) を使ってください。 ソケットで手作りすると HTTPS(TLS)対応・リダイレクト追跡・チャンク転送エンコーディングなど 多くの考慮事項があり、実用的な実装には膨大なコードが必要です。

HTTP/1.1 の構造

HTTP はテキストベースのプロトコルです。リクエストとレスポンスはどちらも 「開始行」「ヘッダー」「空行」「ボディ」という共通の構造を持っています。

部分リクエストレスポンス
開始行GET /path HTTP/1.1HTTP/1.1 200 OK
ヘッダーHost: example.com User-Agent: ...Content-Type: text/html Content-Length: 1234
空行(CRLF のみの行)(CRLF のみの行)
ボディPOST の場合に送信データ(GET は省略)<html>...</html>

代表的なステータスコード

コード意味
200 OKリクエスト成功
301 Moved Permanently恒久的なリダイレクト
400 Bad Requestリクエストの形式が不正
401 Unauthorized認証が必要
403 Forbiddenアクセス拒否
404 Not Foundリソースが存在しない
500 Internal Server Errorサーバー内部エラー

サンプルコード

TCP ソケットで HTTP GET リクエストを手作りし、レスポンスを解析するサンプルです。 Java 8 版ではクラスを使ったレスポンス表現、Java 17 版では record による不変なレスポンスクラスと ヘッダーの Key-Value 解析、Java 21 版では sealed interface と pattern matching switch を使った ステータスコードの型安全な分類処理を実装しています。example.com への実際のリクエストを試みますが、 ネットワーク環境によっては接続できない場合があります。

Sample.java
import java.io.*;
import java.net.Socket;

public class HttpSocketSample {

    // HTTP レスポンスを表すクラス
    static class HttpResponse {
        final int statusCode;
        final String statusMessage;
        final String body;

        HttpResponse(int statusCode, String statusMessage, String body) {
            this.statusCode = statusCode;
            this.statusMessage = statusMessage;
            this.body = body;
        }

        @Override
        public String toString() {
            int len = Math.min(body.length(), 200);
            return "HTTP " + statusCode + " " + statusMessage
                    + "\nBody(先頭 " + len + " 文字): " + body.substring(0, len) + "...";
        }
    }

    // TCP ソケットで HTTP GET リクエストを送信
    static HttpResponse sendGet(String host, int port, String path) throws IOException {
        try (Socket socket = new Socket(host, port)) {
            // HTTP/1.1 リクエストを手作りする
            String 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";  // ヘッダー終端(空行 CRLF)

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

            System.out.println("=== 送信したリクエスト ===");
            // \r\n を見やすく置換して表示
            System.out.println(request.replace("\r\n", "[CRLF]\n"));

            // レスポンスを受信する
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));

            // ステータス行を読む(例: "HTTP/1.1 200 OK")
            String statusLine = reader.readLine();
            System.out.println("=== 受信したレスポンス ===");
            System.out.println(statusLine);

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

            // ヘッダー行を読む(空行まで)
            String line;
            while ((line = reader.readLine()) != null && !line.isEmpty()) {
                System.out.println(line);
            }

            // ボディを読む
            StringBuilder body = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                body.append(line).append("\n");
            }

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

    public static void main(String[] args) {
        System.out.println("=== HTTP の仕組みデモ(TCP ソケットで HTTP/1.1 リクエストを手作り) ===");
        System.out.println();

        // HTTP リクエストの構造説明
        System.out.println("--- HTTP GET リクエストの構造 ---");
        System.out.println("  GET /path HTTP/1.1       ← リクエスト行(メソッド・パス・バージョン)");
        System.out.println("  Host: example.com        ← 必須ヘッダー(HTTP/1.1 から必須)");
        System.out.println("  User-Agent: MyClient/1.0 ← 任意ヘッダー");
        System.out.println("  Connection: close        ← 接続を閉じる指示");
        System.out.println("  (空行 CRLF)            ← ヘッダーとボディの区切り");
        System.out.println();
        System.out.println("--- HTTP レスポンスの構造 ---");
        System.out.println("  HTTP/1.1 200 OK          ← ステータス行");
        System.out.println("  Content-Type: text/html  ← レスポンスヘッダー");
        System.out.println("  Content-Length: 1234");
        System.out.println("  (空行 CRLF)            ← ヘッダーとボディの区切り");
        System.out.println("  <html>...</html>         ← レスポンスボディ");
        System.out.println();

        // 実際のリクエスト(ネットワーク接続が必要)
        try {
            HttpResponse response = sendGet("example.com", 80, "/");
            System.out.println("\n=== レスポンス情報 ===");
            System.out.println("ステータス: " + response.statusCode + " " + response.statusMessage);
            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("このサンプルはネットワーク接続が必要です");
        }

        System.out.println();
        System.out.println("注意: 実務では N-01(HttpClient)または N-02(HttpURLConnection)を使用してください");
    }
}

よくあるミス・注意点

⚠️ HTTP/1.1 の Host ヘッダーは必須

HTTP/1.1 では Host ヘッダーが必須です。 1 台のサーバーで複数のドメインをホストする「バーチャルホスト」に対応するために必要です。 Host ヘッダーを省略すると、サーバーによっては 400 Bad Request が返ります。

// NG: Host ヘッダーなし(HTTP/1.1 ではエラーになる)
"GET / HTTP/1.1\r\n\r\n"

// OK: Host ヘッダーを必ず含める
"GET / HTTP/1.1\r\n"
+ "Host: example.com\r\n"
+ "\r\n"

📌 HTTPS(443 ポート)はソケット直結では通信できない

HTTPS は TLS(Transport Layer Security)で暗号化された HTTP です。 通常の Socket では平文通信しかできないため、 HTTPS に接続するには SSLSocketFactory を使ったSSLSocket が必要です。 実務では HttpClient や HttpURLConnection が TLS を自動処理してくれます。

// NG: 通常の Socket で HTTPS に接続しようとするとエラー
Socket socket = new Socket("example.com", 443); // TLS ハンドシェイクができない

// HTTPS の場合は SSLSocket を使う
SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) factory.createSocket("example.com", 443);
socket.startHandshake();

📌 チャンク転送エンコーディング(Transfer-Encoding: chunked)の扱い

HTTP/1.1 ではレスポンスボディが Transfer-Encoding: chunked で 送られてくる場合があります。この場合、ボディは「16 進数のサイズ + CRLF + データ + CRLF」が繰り返され、 サイズが 0 の行で終わります。単純に readLine() で読むと チャンクサイズの行が混入してしまいます。HttpClient はこれを自動処理します。

📌 Java バージョンごとの違い

Java 8 ではクラスを使ったレスポンス表現になります。 Java 17 では record で不変なレスポンスを簡潔に定義でき、 テキストブロックでリクエスト構造の説明も読みやすく書けます。 Java 21 では sealed interface と pattern matching switch でステータスコードの分類を 型安全かつ網羅的に処理できます。

テストする観点

  • ステータス行の解析が正しいか("HTTP/1.1 200 OK" → statusCode=200, statusMessage="OK")
  • ヘッダーの Key-Value 解析が正しいか(コロン区切り・前後のスペースのトリム)
  • ヘッダーとボディの区切り(空行)を正しく検出できるか
  • ステータスコードの分類(2xx/3xx/4xx/5xx)が正しいか(境界値: 200・299・300・399・400・499・500・599)
  • 存在しないホストへの接続で UnknownHostException が発生するか
  • 接続タイムアウト(socket.setSoTimeout())を設定した場合に SocketTimeoutException が発生するか

GitHub でソースコードを見る →