概要
HTTP はリクエスト行、ヘッダー、空行、ボディという単純なテキスト構造のプロトコルです。HttpClient や HttpURLConnection はこの構造を抽象化してくれますが、一度は生のソケットで HTTP リクエストを手作りしてみることで、「Host ヘッダーはなぜ必須か」「空行の CRLF がなぜ重要か」「Connection: close は何を意味するか」といった疑問が腹落ちします。この記事では、TCP ソケットを使って HTTP GET リクエストを手動で組み立て、サーバーからのレスポンスをステータス行・ヘッダー・ボディに分解して読み取ります。フレームワークの裏で何が起きているかを一度理解しておくと、通信トラブルの原因特定が格段に速くなります。
使いどころ
HTTP 通信のトラブルシューティングで、プロトコルレベルの動作を確認する
新人研修や勉強会で、HTTP プロトコルの構造を手を動かして理解する
プロキシやロードバランサーの動作検証で、生の HTTP リクエストを送信して応答を確認する
コード例
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 を使用してください");
}
}Version Coverage
record でレスポンスを不変オブジェクトとして表現し、テキストブロックでリクエスト例を読みやすく記述できる。
// 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
注意点
この記事のコードは HTTP プロトコルの仕組み理解が目的。実務では HttpClient または HttpURLConnection を使うこと
HTTP/1.1 では Host ヘッダーが必須。省略するとサーバーが 400 Bad Request を返す場合がある
Connection: close を指定しないと、サーバーがキープアライブで接続を保持し、読み取りがブロックする場合がある
HTTPS(TLS)には対応していない。HTTPS サーバーへの接続には SSLSocket が必要
FAQ
SSLSocketFactory で SSLSocket を作成すれば TLS 接続が可能です。ただし証明書検証の設定が必要になるため、実務では HttpClient を使うのが安全です。
HTTP/2 はバイナリプロトコルのため、テキストベースの手作りリクエストでは対応できません。HttpClient は HTTP/2 をサポートしています。
Transfer-Encoding: chunked の場合、チャンクサイズの解析が必要です。Connection: close であればソケットが閉じるまで読めば全データを取得できます。