Old-School Backend / 2026 ・ No.03

いにしえのバックエンド入門

第3回 素手で HTTP を受ける ── ServerSocket、スレッド、JDK の HttpServer

公開

端末の画面。上は申請サーバーで java RawServer 8080 を動かしたログ。接続、1行目 POST /api/expenses HTTP/1.1、ヘッダー 5 行、本文 54 バイト、読み取った値、201 Created を返した(Content-Length: 62、本文 62 バイト)。下は PC で curl -i を実行し、HTTP/1.1 201 Created と Content-Length: 62、Location: /api/expenses/EXP-2026-0012 が返ってきたところ
JDK だけで書いた 86 行の申請サーバー(上)に、PC から curl で申請を1件送ったところ(下)。1行目、ヘッダー、本文と、HTTP を1行ずつ自分で読んで、201 を返しています(筆者がクラウド上の Linux で、2026年10月27日に撮影。ウインドウの上の帯は説明のために描き足したもの)

第1回では、Spring Boot に申請 API を書いて、小さな「窓口」で申請を受け取りました。でも、その下で HTTP を受けて、読んで、返していたのは、自分のコードではありません。
今回は Spring を使わず、JDK だけで HTTP を受けます。ServerSocket で1行ずつ読み、Content-Length を数え、スレッドを分け、JSON を手で読む。素手でやって、どこで手を切るかを実測します。

一

素手で受ける

新井Spring Boot なら @PostMapping を1行書けば受けられるのに、なんでわざわざ素手で?

古井その1行の下で何が起きているか知らないと、本番で「たまに 1 件だけ途中で切れる」と言われたとき、どこを見ればいいか分からない。

新井そんなこと、本当に起きるんですか。

古井今日、自分で起こしてみる。

全15回の路線図(いまここ:第3回)

1 全体図 2 Java の準備 3 素の HTTP 4 素の JDBC 5 Spring Boot 6 入力チェック 7 例外と返事 8 DB アクセス 9 トランザクション 10 状態遷移 11 認証・認可 12 ファイル 13 ログと監視 14 テスト 15 動かす いまここ 基礎 素手で 受け口 データ 業務と守り 運用
  • 1行ずつ読む ── HTTP は文字の行。空行までがヘッダー、そのあと Content-Length の分だけ本文(実測)
  • 長さはバイト ── 文字の数で書くと、返事が 56 バイトで切れた(実測と Wireshark)
  • スレッド ── 1人ずつだと 50 件に 5.26 秒、1人に1本なら 0.2 秒。黙っている客1人で全員が止まる(実測)
  • JSON を手で読む ── 引用符、\u エスケープ、入れ子で、読んだ値が化けた(実測)
  • JDK の HttpServer ── 31 行で、400 も返してくれる。でも JSON は読んでくれない(実測)

舞台は、これまでと同じ実験用のネットワークです。PC(192.168.1.10)から、申請サーバー(192.168.1.20、名前は expenses.example)の 8080 番へ申請を送ります。サーバーのプログラムは、JDK 25 だけで書いた RawServer.java(空行を除いて 86 行)と JdkServer.java(同じく 31 行)。ライブラリは1つも使っていません。

二

ServerSocket で1行ずつ読む

ネットワーク編の第10回で見たとおり、HTTP の頼みごとは、ただの文字の行です。だから、サーバーも行を読めば受け取れます。まず、受け取りの部分のコードです。

String first = readLine(in);  // 例:POST /api/expenses HTTP/1.1
if (first == null) { log("何も来ないまま切れた"); return; }
log("1行目 " + first);
Map<String, String> headers = new LinkedHashMap<>();
for (String line; (line = readLine(in)) != null && !line.isEmpty(); ) { // 空行まで
    int c = line.indexOf(':');
    headers.put(line.substring(0, c).trim().toLowerCase(), line.substring(c + 1).trim());
}
log("ヘッダー " + headers.size() + " 行");
int len = Integer.parseInt(headers.getOrDefault("content-length", "0"));
String body = new String(in.readNBytes(len), StandardCharsets.UTF_8);
log("本文 " + len + " バイト " + body);

1行目を読み、空行が来るまでヘッダーを1行ずつ読み、最後に Content-Length に書かれた数だけ本文のバイトを読みます。PC から curl で申請を1件送りました。

# PC から curl -v で申請を1件(> は送った行、< は受け取った行)
> POST /api/expenses HTTP/1.1
> Host: expenses.example:8080
> User-Agent: curl/8.5.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 54
> 
} [54 bytes data]
< HTTP/1.1 201 Created
< Content-Type: application/json
< Content-Length: 62
< Location: /api/expenses/EXP-2026-0012
< Connection: close
< 
{ [62 bytes data]
{"id":"EXP-2026-0012","item":"交通費","status":"submitted"}
# 同じとき、申請サーバー(RawServer)のログ
01:11:52.825 main                   待ち受け開始 0.0.0.0/0.0.0.0:8080 threads=single
01:11:55.189 main                   接続 /192.168.1.10:34696
01:11:55.191 main                   1行目 POST /api/expenses HTTP/1.1
01:11:55.196 main                   ヘッダー 5 行
01:11:55.204 main                   本文 54 バイト {"date":"2026-10-26","item":"交通費","amount":1280}
01:11:55.221 main                   読み取った値 {date=2026-10-26, item=交通費, amount=1280}
01:11:55.267 main                   201 Created を返した(Content-Length: 62、本文 62 バイト)

HTTP の頼みごとを、サーバーはこう読む

POST /api/expenses HTTP/1.1 ⏎ 29 B Host: expenses.example:8080 ⏎ 29 B Content-Type: application/json ⏎ 32 B … ほか 2 行 Content-Length: 54 ⏎ 20 B 空行(CRLF だけ)=ヘッダーの終わり 2 B {"date":"2026-10-26",…} 54 B 1行目を読む 何を・どこへ・どの版で 空行までヘッダーを読む readLine をくり返す ここで区切り ここから先は本文 Content-Length の数だけ読む readNBytes(54)。終わりの印はない 行は文字で読めるが、本文は「何バイトか」を先に聞いておかないと読めない 1行目からヘッダーまで 149 バイト+本文 54 バイト=線の上の 203 バイト(実測)

行は「改行(CRLF)が来るまで」読めば区切れます。ヘッダーの終わりは「何も書いていない行」です。ところが本文には、終わりの印がありません。本文に改行が入っていてもかまわないので、改行では区切れないのです。だから送る側は、本文が何バイトあるかを先にヘッダーで知らせ、受ける側はその数だけ読みます。ここを間違えると、次の章のようになります。

三

長さは文字ではなくバイト

返事を書く部分です。本文をバイトに変えて、その長さを Content-Length に書きます。比べるために、うっかり「文字の数」を書いてしまう版も用意しました(-Dbug.cl=true)。

byte[] b = json.getBytes(StandardCharsets.UTF_8);
int cl = BUG_CL ? json.length() : b.length;  // バグ版は「文字の数」を長さにしてしまう
String head = "HTTP/1.1 " + status + "\r\nContent-Type: application/json\r\nContent-Length: " + cl + "\r\n" + extra + "Connection: close\r\n\r\n";
out.write(head.getBytes(StandardCharsets.US_ASCII));
out.write(b);
out.flush();
# -Dbug.cl=true:Content-Length を「文字の数」で計算した版
01:11:58.764 main                   201 Created を返した(Content-Length: 56、本文 62 バイト)
# curl -v が受け取ったもの
< HTTP/1.1 201 Created
< Content-Type: application/json
< Content-Length: 56
< Location: /api/expenses/EXP-2026-0012
< Connection: close
< 
{ [62 bytes data]
* Excess found writing body: excess = 6, size = 56, maxdownload = 56, bytecount = 56
{"id":"EXP-2026-0012","item":"交通費","status":"submi

56 文字で 62 バイト。長さを文字で書くと、本文が切れる

返事の本文(UTF-8 のバイト) {"id":"EXP-2026-0012","item":" 30 B 交通費 9 B ","status":"submi 17 B tted"} 6 B Content-Length: 56 ならここで終わり あふれる 6 B json.length() → 56(文字の数) json.getBytes(UTF_8).length → 62(バイトの数) 長さはバイトで数える。日本語が 1 文字でも入ると、文字の数とずれる 実測:curl は「Excess found … excess = 6」と言って、56 バイトで切った 英数字だけのテストでは、このバグは見つからない(第2回:交通費は 9 バイト)

サーバーは 62 バイトを全部送りました。ところが curl は「Content-Length: 56 と聞いたのに、6 バイト多い」と言って、56 バイトで読むのをやめ、本文は "submi で切れました。第2回で見たとおり、「交通費」は 3 文字で 9 バイト。差の 6 バイトが、そのままはみ出しました。

Wireshark の画面。8番のパケット HTTP/1.1 201 Created を開き、Content-Length: 56 と File Data: 56 bytes が見える。右下のバイト表示では、本文の選ばれた範囲が status の submi で終わっている。下のタブは Frame (128 bytes) と Reassembled TCP (190 bytes)
Content-Length を文字の数で書いた返事を、Wireshark で開いたところ。線の上には本文 62 バイトが全部載っているのに、HTTP として読まれたのは 56 バイトで、submi で切れています(筆者がクラウド上の Linux で、2026年10月27日に撮影。ウインドウの上の帯は説明のために描き足したもの)

Wireshark で見ると、線の上には 62 バイトがちゃんと載っています。それでも HTTP として読まれたのは 56 バイトだけ。線の上は正しいのに、約束(ヘッダー)が間違っているので、受け取る側は約束のほうを信じます。今回はやりとりのたびに接続を閉じる(Connection: close)ので、はみ出した 6 バイトは捨てられるだけでした。もし1本の接続を使い回していたら、その 6 バイトは「次の返事の頭」として読まれ、次の返事まで壊れます。品目が英数字だけのテストでは、このバグは1回も出ません。

新井これが「たまに 1 件だけ途中で切れる」……日本語の品目のときだけ。

古井そういうことだ。いにしえの現場では、これを「全角のときだけ化ける」と呼んでいた。

四

1人ずつだと、黙っている客で止まる

次は、たくさんの客が来たときです。いちばん素直な書き方は、客を1人受け付けたら(accept)、その客が終わるまで相手をして、それから次の客を受け付ける形です。

ExecutorService pool = switch (mode) {
    case "platform" -> Executors.newCachedThreadPool();
    case "virtual" -> Executors.newVirtualThreadPerTaskExecutor();
    default -> null;
};
try (var server = new ServerSocket(Integer.parseInt(args[0]))) {
    log("待ち受け開始 " + server.getLocalSocketAddress() + " threads=" + mode);
    while (true) {
        Socket s = server.accept();  // ここで、だれかが来るまで止まって待つ
        if (pool == null) handle(s);  // 1人ずつ順番に
        else pool.submit(() -> handle(s));  // 来た人ごとに別の作業員へ
    }
}

-Dthreads=single で動かすと、pool が null なので handle(s) をその場で呼びます。そこへ、接続だけして何も言わない客を1人置いてから、curl で申請しました。

端末の画面。上の申請サーバーのログは、待ち受け開始と、接続 /192.168.1.10:50942 の2行だけ。下の PC では、nc で接続だけして何も送らない客を作り、続けて curl -m 3 で申請すると、curl: (28) Operation timed out after 3002 milliseconds with 0 bytes received となった
1人ずつ順番に相手をするサーバーに、接続だけして何も言わない客(nc)がいると、次の申請は3秒待っても1バイトも返ってきませんでした。サーバーのログには、黙っている客の「接続」しかありません(筆者がクラウド上の Linux で、2026年10月27日に撮影。ウインドウの上の帯は説明のために描き足したもの)

curl は3秒待って、1バイトも受け取れずにあきらめました。サーバーのログには、黙っている客の「接続」が1行あるだけです。サーバーは、その客の1行目を readLine でずっと待っていて、次の客の番が来ません。curl の接続そのものは OS が受け付けて(ネットワーク編で見た3回のあいさつまでは済んで)いるので、curl から見ると「つながったのに返事がない」状態です。

悪気のない客でも、電波の悪いスマホや、途中で止まったプログラムは同じことをします。わざとゆっくり送ってサーバーを止める攻撃もあり、本物のサーバーは、黙っている客を一定時間で切る仕組みを持っています。

五

スレッドと仮想スレッド

解決の定番は、来た客ごとに別の作業員(スレッド)を付けることです。-Dthreads=platform は OS のスレッドを、-Dthreads=virtual は Java 21 から使える仮想スレッドを、1人に1本ずつ付けます。1件ごとに 100 ミリ秒の「仕事をしているふり」を入れて、PC から 50 件を同時に送りました。

# PC から 50 件を同時に送る。1 件ごとに 100 ms の仕事(-Dwork=100)
    single    {201: 50}  total 5.261 s  slowest 5.194 s  fastest 0.151 s  作業員 1
    platform  {201: 50}  total 0.196 s  slowest 0.179 s  fastest 0.120 s  作業員 50
    virtual   {201: 50}  total 0.225 s  slowest 0.214 s  fastest 0.125 s  作業員 50

50 件を同時に:1人ずつ と 1人に1本(実測)

50 件を同時に送り、全部終わるまで(1件 100 ms の仕事) single 5.261 s 作業員 1 人 platform 0.196 s 作業員 50 人 virtual 0.225 s 作業員 50 人 1人ずつだと、50 人目は 49 人分待つ。5 秒は、100 ms × 50 件そのもの single:accept したら、その客が終わるまで次の客を受けない platform:来た人ごとに OS のスレッド。数百、数千と増えるとメモリを食う virtual:Java 21 からの軽いスレッド。待っている間は OS のスレッドを手放す 作業員の顔ぶれはログのスレッド名で数えた(main だけ/50 種類)

1人ずつだと 5.261 秒。100 ミリ秒 × 50 件が、そのまま行列になりました。1人に1本なら 0.2 秒ほどで、ログのスレッド名を数えると、作業員は 50 人いました。仮想スレッドで、さっきの黙っている客をもう一度置いてみます。

# 仮想スレッド版で、黙っている客がいる間に curl
{"id":"EXP-2026-0012","item":"交通費","status":"submitted"}
  -> 201 (0.026412 s)
# サーバーのログ(virtual-22 は黙っている客、virtual-29 が curl)
01:12:26.955 virtual-22             接続 /192.168.1.10:33352
01:12:27.436 virtual-29             接続 /192.168.1.10:33362
01:12:27.438 virtual-29             1行目 POST /api/expenses HTTP/1.1
01:12:27.440 virtual-29             ヘッダー 5 行
01:12:27.443 virtual-29             本文 54 バイト {"date":"2026-10-26","item":"交通費","amount":1280}
01:12:27.446 virtual-29             読み取った値 {date=2026-10-26, item=交通費, amount=1280}
01:12:27.464 virtual-29             201 Created を返した(Content-Length: 62、本文 62 バイト)

黙っている客(virtual-22)は待たせたまま、curl の申請(virtual-29)は 0.026 秒で 201 が返りました。

OS のスレッドは1本ごとにメモリを確保するので、数千、数万の客には向きません。仮想スレッドは、待っている間は OS のスレッドを手放すので、大勢の「待っている客」を安く抱えられます。ただし、黙っている客がいなくなるわけではありません。接続も仮想スレッドも、客が黙っている限り残り続けます。待つ時間に上限(タイムアウト)を付けるのは、どちらでも必要です。第1回の Tomcat は、OS のスレッドの池(最大 200 本)で客をさばいていて、Spring Boot では設定1つ(spring.threads.virtual.enabled=true)で仮想スレッドに切り替えられます。

六

JSON を手で読む落とし穴

本文の JSON は、正規表現で「それっぽく」読みました。"名前":"文字" か "名前":数 を順に拾うだけです。

Matcher mt = Pattern.compile("\"(\\w+)\"\\s*:\\s*(\"([^\"]*)\"|([-\\d.]+))").matcher(body);
while (mt.find()) m.put(mt.group(1), mt.group(3) != null ? mt.group(3) : mt.group(4));

ふつうの申請は読めました。そこで、ちょっと意地悪な、でも現場では普通にある申請を3つ送りました。

# a ふつうの申請
    送った本文: {"date":"2026-10-26","item":"交通費","amount":1280}
    読み取った値: {date=2026-10-26, item=交通費, amount=1280}
    → {"id":"EXP-2026-0012","item":"交通費","status":"submitted"}  -> 201 (0.057934 s)
# b 品目に引用符を含む(会議費 "A社")
    送った本文: {"date":"2026-10-26","item":"会議費 \"A社\"","amount":3300}
    読み取った値: {date=2026-10-26, item=会議費 \, amount=3300}
    → {"id":"EXP-2026-0013","item":"会議費 \","status":"submitted"}  -> 201 (0.004617 s)
# c Python の json.dumps で送る(既定で日本語を \u でエスケープ)
    送った本文: {"date": "2026-10-26", "item": "\u4ea4\u901a\u8cbb", "amount": 1280}
    読み取った値: {date=2026-10-26, item=\u4ea4\u901a\u8cbb, amount=1280}
    → 201 {"id":"EXP-2026-0014","item":"\u4ea4\u901a\u8cbb","status":"submitted"}
# d 領収書の内訳(入れ子の amount)つき
    送った本文: {"date":"2026-10-26","amount":1280,"item":"交通費","receipts":[{"amount":500},{"amount":780}]}
    読み取った値: {date=2026-10-26, amount=780, item=交通費}
    → {"id":"EXP-2026-0015","item":"交通費","status":"submitted"}  -> 201 (0.003378 s)
  • b 引用符 ── JSON では、文字の中の " を \" と書く。正規表現は最初の " で止まるので、品目は 会議費 \。しかも、それをそのまま返事に埋めたので、返事の JSON まで壊れた
  • c \u エスケープ ── Python の json.dumps は、既定で日本語を 交… と書く。正しい JSON だが、そのまま「品目」として保存された
  • d 入れ子 ── 内訳の中の amount を拾って、金額 1,280 円が 780 円になった。エラーも出ず、201 で受け付けている

いちばん怖いのは d です。例外で止まれば気づけますが、黙って違う金額を受け付けました。JSON の決まり(RFC 8259)は短い文書ですが、文字のエスケープ、入れ子、数の書き方まで、きちんと読むと手書きでは割に合いません。書く側も同じで、値をそのまま文字列に埋めると、引用符1つで JSON が壊れ、悪くすると別の項目を紛れこませることもできてしまいます。JSON の読み書きは、第5回で Jackson に任せます。

七

JDK の HttpServer

実は JDK には、小さな HTTP サーバーが入っています。com.sun.net.httpserver パッケージ(jdk.httpserver モジュール)です。同じ申請 API を書くと 31 行でした。

    HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
    server.createContext("/api/expenses", ex -> {
        String body = new String(ex.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
        if (!ex.getRequestMethod().equals("POST")) { reply(ex, 405, "{\"error\":\"method not allowed\"}"); return; }
        var req = RawServer.naiveJson(body);  // JSON はまだ手で読む
        String id = "EXP-2026-%04d".formatted(seq.getAndIncrement());
        ex.getResponseHeaders().add("Location", "/api/expenses/" + id);
        reply(ex, 201, "{\"id\":\"%s\",\"item\":\"%s\",\"status\":\"submitted\"}".formatted(id, req.get("item")));
    });
    server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); // 1件ごとに仮想スレッド
    server.start();
…
static void reply(HttpExchange ex, int code, String json) throws IOException {
    byte[] b = json.getBytes(StandardCharsets.UTF_8);
    ex.getResponseHeaders().add("Content-Type", "application/json");
    ex.sendResponseHeaders(code, b.length);  // 長さはバイトで
    try (OutputStream os = ex.getResponseBody()) { os.write(b); }

1行目とヘッダーの読み取り、Content-Length、スレッドの割り当ては HttpServer の仕事になり、長さは sendResponseHeaders にバイト数を渡すだけです。壊れた頼みごとを送って、自作のサーバーと比べました。

# 壊れた頼みごと(HELLO の1行だけ)を、nc で送る
pc$ printf 'HELLO\r\n\r\n' | nc -w 2 expenses.example 8080
# 自作の RawServer の返事
HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 21
Connection: close

{"error":"not found"}
# JDK の HttpServer の返事
HTTP/1.1 400 Bad Request
Content-Length: 40
Content-Type: text/html
Connection: close

<h1>400 Bad Request</h1>Bad request line
# JDK の HttpServer に、ふつうの POST と GET
POST → {"id":"EXP-2026-0012","item":"交通費","status":"submitted"}  -> 201 (0.063106 s)
GET  → {"error":"method not allowed"}  -> 405

自作のサーバーは、HELLO という意味のない1行を「HELLO というメソッドの頼みごと」として受け付け、まじめに 404 を返しました。HttpServer は「1行目がおかしい」と 400 を返します。頼みごとの形を確かめるのも、サーバーの大事な仕事です。一方で、GET に 405 を返すのは自分で書いた行で、JSON はあいかわらず手で読んでいます。道具としては、テストで偽の API を立てたり、社内の小さな道具を作ったりするのにちょうどいい大きさです。

八

素手でやって分かったこと

素手でやった仕事を、だれがやってくれるか

仕事 自作 RawServer JDK HttpServer Spring Boot 接続を受け、作業員に割り当てる ○ 自分で ○ Tomcat 1行目とヘッダーを読む ○ 自分で ○ Tomcat 本文を Content-Length 分読む ○ 自分で ○ Tomcat 長さをバイトで書く △ 間違えた ○ Tomcat 壊れた頼みに 400 × 404 を返した ○ 400 Tomcat URL とメソッドで振り分け ○ 自分で △ 405 は自分で Spring MVC JSON を読む・書く × 正規表現 × 手で Jackson 黙っている客を切る × 待ち続ける (今回は未計測) Tomcat 第1回の Java 112 行の下で、これだけの仕事が動いていた ○=できた、△=一部は自分で、×=できなかった(どれも今回の実測と、書いたコードから) Tomcat の待ち時間は connectionTimeout で決まる。JSON は第5回で Jackson に任せる

第1回の Spring Boot では、Tomcat が接続を受けて作業員を割り当て、1行目とヘッダーと本文を読み、壊れた頼みごとを断り、黙っている客を一定時間で切ります。Spring MVC が URL とメソッドで窓口を選び、Jackson が JSON を読み書きします。今回、そのうちのいくつかを自分でやって、いくつかを間違えました。

POINT

素手で書くのは、仕組みを知るためです。仕事のコードでは、HTTP は実績のあるサーバー(Tomcat など)に、JSON は実績のあるライブラリ(Jackson など)に任せます。そのうえで、長さはバイト、1人に1本の作業員、待つ時間には上限、JSON は手で読まないの4つは、どの道具を使っていても、設定やログを見るときの目印になります。

新井Tomcat、えらいですね……。

古井えらい。だから、えらい人が何をしているか、一度は自分でやってみる。いにしえの弟子は、親方の仕事を一度まねてから、親方の道具を使わせてもらった。

九

自分の PC で確かめる

試したいことコマンド
ソースファイルのまま動かす(コンパイル不要)java RawServer.java 8080
送った行と受け取った行を見るcurl -v -d '{"item":"交通費"}' localhost:8080/api/expenses
長さを文字の数で書くバグjava -Dbug.cl=true RawServer.java 8080
HTTP を手で打つ(1行ずつ入力し、最後に空行)nc localhost 8080
黙っている客を作るsleep 10 | nc localhost 8080
仮想スレッドで動かすjava -Dthreads=virtual RawServer.java 8080
JDK の小さなファイルサーバー(Java 18 から)jwebserver -p 8000

Windows では、nc の代わりに WSL の端末を使うか、PowerShell の curl.exe を使ってください。サーバーを止めるときは Ctrl+C です。

結 — 一枚まとめと、次回

  1. HTTP は文字の行。1行目、空行までのヘッダー、Content-Length の分の本文。ServerSocket で読める。
  2. 本文には終わりの印がない。長さはバイトで書く。文字の数で書いたら、日本語の品目のときだけ返事が切れた。
  3. 1人ずつだと、黙っている客1人で全員が止まる。50 件に 5.26 秒。
  4. 1人に1本のスレッドで 0.2 秒。仮想スレッドは待っている客を安く抱えられるが、待つ時間の上限はやはり必要。
  5. JSON を手で読むと、引用符、\u、入れ子で化ける。金額が黙って 780 円になった。
  6. JDK の HttpServer は 31 行で 400 まで返す。Spring Boot では、これを Tomcat と Spring MVC と Jackson が分担している。

新井次は Spring Boot に戻りますか?

古井まだだ。次回は DB も素手でやる。JDBC で SQL を組み立てて、なぜ「文字をつなげて SQL を作ってはいけない」のかを、自分の DB で確かめる。

参考

記録は、筆者がクラウド上の Linux(Ubuntu 24.04、カーネル 6.18)で、OpenJDK 25.0.4、curl 8.5.0、Wireshark 4.2.2 を使って 2026年10月27日に取ったものです。PC と申請サーバーは、1台の中に作った別々のネットワーク空間(network namespace)で、ネットワーク編と同じ住所にしてあります。時間はそのときの1回の値で、動かすたびに少し変わります。表の数字や出力はすべて、そのときのコマンドの出力から読んだ実際の値です。登場人物は架空の人物です。

いにしえのバックエンド入門 第3回 — 素手で HTTP を受ける / 2026.10

← 記事一覧へ