概要
HTTP サーバー自作連載の第2回として、GET リクエストに付くクエリ文字列を扱います。最小サーバーではパスだけを見れば十分でしたが、実際の HTTP では `?` 以降の値を読み取り、デコードし、未指定時のデフォルト値を決める場面がすぐに出てきます。この記事では、クエリ文字列の分離、URL デコード、動的レスポンス生成を順に実装し、次の POST 処理につながる土台を作ります。
使いどころ
GET リクエストで値を受け取る流れを、低レイヤから確認したい
クエリ文字列の分解と URL デコードを手で追いたい
動的レスポンスの入口として、最小の検索風 UI を作ってみたい
コード例
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpQueryServerSample {
static final int PORT = 8083;
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\r\n");
writer.flush();
out.write(bodyBytes);
out.flush();
}
static Map<String, String> parseQuery(String rawQuery)
throws UnsupportedEncodingException {
var params = new HashMap<String, String>();
if (rawQuery == null || rawQuery.isEmpty()) {
return params;
}
for (var pair : rawQuery.split("&")) {
var kv = pair.split("=", 2);
var key = URLDecoder.decode(kv[0], "UTF-8");
var value = kv.length > 1
? URLDecoder.decode(kv[1], "UTF-8") : "";
params.put(key, value);
}
return params;
}
static String escapeHtml(String text) {
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">");
}
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"));
var requestLine = reader.readLine();
if (requestLine == null || requestLine.isEmpty()) {
return;
}
String line;
while ((line = reader.readLine()) != null && !line.isEmpty()) {
// ヘッダーは今回は読み飛ばす
}
var parts = requestLine.split(" ");
var method = parts[0];
var target = parts.length > 1 ? parts[1] : "/";
if (!"GET".equals(method)) {
sendResponse(out, 405, "Method Not Allowed",
"text/plain", "405 Method Not Allowed");
return;
}
var path = target;
var rawQuery = "";
var queryIndex = target.indexOf('?');
if (queryIndex >= 0) {
path = target.substring(0, queryIndex);
rawQuery = target.substring(queryIndex + 1);
}
var params = parseQuery(rawQuery);
if ("/".equals(path)) {
var html = """
<html><body>
<h1>クエリパラメータデモ</h1>
<form method='GET' action='/search'>
<input name='keyword' placeholder='検索語'>
<button>検索</button>
</form>
<p><a href='/hello?name=Java'>/hello?name=Java</a></p>
</body></html>
""";
sendResponse(out, 200, "OK", "text/html", html);
} else if ("/hello".equals(path)) {
var name = params.getOrDefault("name", "Guest");
sendResponse(out, 200, "OK", "text/plain",
"Hello, " + name + "!");
} else if ("/search".equals(path)) {
var keyword = params.getOrDefault("keyword", "");
var html = """
<html><body>
<h1>検索結果</h1>
<p>keyword = %s</p>
<p><a href='/'>戻る</a></p>
</body></html>
""".formatted(escapeHtml(keyword));
sendResponse(out, 200, "OK", "text/html", html);
} else {
sendResponse(out, 404, "Not Found",
"text/plain", "404 Not Found");
}
}
}
public static void main(String[] args) throws IOException {
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("HTTP Query Server 起動中... 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
テキストブロックと `formatted` を使うと、動的 HTML の返却を読みやすく書ける。
var params = parseQuery(rawQuery);
var keyword = params.getOrDefault("keyword", "");
var html = """
<html><body>
<h1>検索結果</h1>
<p>keyword = %s</p>
</body></html>
""".formatted(escapeHtml(keyword));Library Comparison
注意点
クエリ文字列は URL に残るため、検索条件のような軽い値と相性がよい
URLDecoder を通さないと、日本語や空白を含む値の確認が難しくなる
入力値を HTML に戻す場合は、最小サンプルでもエスケープ処理を入れておきたい
同じキーが複数回出る場合の扱いは、この段階では単純化している
FAQ
勧めません。この記事はクエリ文字列の扱いを理解するための学習用であり、実画面では入力検証やエラーハンドリングが別途必要です。
検索条件やページ番号のように URL として共有しやすい値はクエリパラメータが向いています。長い入力や URL に出したくない値は POST ボディを選びます。
実務では List として扱う場合がありますが、この記事の最小実装では最後の値だけを採用しています。必要になった段階で拡張すれば十分です。
クエリで受け取った値をそのまま HTML に埋め込むと、画面表示の崩れやスクリプト注入の入口になります。最小サンプルでも変換の考え方は見ておくと役に立ちます。