概要
HTTP サーバー自作連載の第1回として、まずは最小構成のサーバーを組み立てます。ここで押さえたいのは、クライアントから TCP 接続を受け付け、HTTP リクエスト行を読み、パスに応じてレスポンスを返すまでの一連の流れです。Spring Boot や Javalin を使えばこの部分は隠れますが、最初に手で書いておくと、以降のクエリ文字列、POST、静的配信、Cookie といった話がかなり追いやすくなります。この記事では、学習用の最小サーバーとしてどこまで実装すれば十分かに絞って整理します。
使いどころ
HTTP サーバーの最小構成を手で追い、リクエストからレスポンスまでの流れを理解したい
後続のクエリ文字列や POST 処理に進む前に、土台になる処理だけを整理したい
テスト用や検証用の最小サーバーがどの程度のコード量になるかを確認したい
コード例
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) {}
}
});
}
}
}
}Version Coverage
テキストブロックと `var` により、レスポンス HTML や補助コードを Java 8 より読みやすく書ける。
// 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
注意点
この実装は学習用の最小構成であり、認証、入力検証、HTTPS など実務で必要になる要素はまだ扱わない
HTTP リクエスト行が null のままになるケースを考慮しないと、即切断したクライアントで例外になりやすい
Content-Length は文字数ではなく UTF-8 のバイト数で設定する必要がある
ブラウザは `/favicon.ico` も取りに来るため、最小サーバーでも 404 の流れを見ておくと挙動を追いやすい
Java 8 / 17 ではスレッドプール設計が必要で、Java 21 では Virtual Thread に置き換えやすい
FAQ
勧めません。この記事のコードは HTTP の流れを理解するための最小構成であり、公開運用や実アプリの基盤として使う前提ではありません。
技術的には可能です。パスからファイルを読み込み、Content-Type を付けて返せば動きますが、実際にはパス正規化やキャッシュ制御も必要になります。
Java 8 / 17 では ExecutorService のスレッドプールサイズが上限になります。Java 21 では Virtual Thread を使うことで、接続ごとの処理をより素直に書けます。
Java 単体でも SSLServerSocket を使えば実装できます。ただし証明書や更新手順まで含めると話が大きくなるため、学習の次の段階では既製の基盤と合わせて考えることが多いです。