Old-School Networking / 2026 ・ No.07

いにしえのネットワーク入門

第7回 UDP とソケット ── あいさつしない手紙と、プログラムの窓口

公開

Wireshark の画面。一覧には UDP のパケットが2つだけ。1番は 192.168.1.10 の 59437 番から 203.0.113.20 の 5000 番へ Len=59、2番はその返事で Len=46。明細の User Datagram Protocol が開かれて選ばれ、Source Port: 59437、Destination Port: 5000、Length: 67、Checksum の4つだけが並ぶ。バイトの欄では e8 2d 13 88 00 43 b4 05 の8バイトが青くなり、右に JSON の申請の文字が見える
UDP で申請を1回送ったところ。線の上には行きと帰りの2つだけ。UDP の見出しは、番号2つ・長さ・チェックサムの 8 バイトしかありません(筆者がクラウド上に作った練習用ネットワークで、2026年10月25日に記録したものを Wireshark 4.2.2 で開いて撮影。画面は英語のまま。ウインドウの上の帯と見出しは説明のために描き足したもの)

第6回の TCP は、あいさつをし、番号を振り、落ちたら送り直す、とても丁寧な運び方でした。今回の前半は、そのどれもしない UDP です。丁寧さを捨てると何が速くなり、何を自分で引き受けることになるのかを、同じいたずら線で確かめます。
後半は、TCP も UDP も、プログラムからは ソケット という窓口を通して使うという話です。申請を送る Python と、受け取る Node.js の中で何が起きているかを、strace でのぞきます。

一

丁寧さを選ぶ層

新井TCP の丁寧さ、感動しました。でも前回の最後に、丁寧さが邪魔になるって。

古井電話で話していて、1 秒前の「もしもし」を言い直されたら困るだろう。古い声より、いまの声がほしい。

新井たしかに……。じゃあ、言い直さない運び方もあるんですか?

古井それが UDP だ。第4層には、丁寧な TCP と、ほとんど何もしない UDP の2つがあって、使う側が選ぶ。

全12回の路線図(いまここ:第7回)

1 全体図 2 物理層 3 データリンク層 4 IP アドレス 5 ルーター 6 TCP 7 UDP・ソケット 8 TLS 9 DNS 10 HTTP 11 開発者の網 12 切り分け いまここ 全体図 第1層 第2層 第3層 第4層 第5・6層 第7層 実践
  • UDP ── 8 バイトの見出し、区切りが残る、誰もいない番号、断片(実測)
  • 先頭のつかえ ── 1つ落ちたとき、TCP と UDP で後ろの知らせはどうなるか(実測)
  • ソケット ── socket・connect・accept、strace で見る中身(実測)
  • サーバーの裏側 ── 受け付け待ちの行列、epoll で多数の客をさばく(実測)

練習用ネットワークは第6回と同じで、PC(192.168.1.10)とサーバー(203.0.113.20)の間に、片道 20 ミリ秒のいたずら線をはさんでいます。いたずら線は、UDP の小包も落とせるように少し書き足しました。

二

UDP:あいさつしない手紙

試しに、申請を UDP で送ってみます。サーバーには、UDP の 5000 番で申請を受け付ける小さなプログラムを置きました。

# UDP で申請を1回(片道 20 ms の線)
pc$ python3 cli7.py udp
    sent 59 bytes, got 46 bytes in 40.9 ms: {"id": "EXP-2026-0100", "status": "submitted"}
# 線の上を流れたのは、行きと帰りの2個だけ(第6回の TCP は 10 個)
  0.000  pc  → srv  UDP 59437 → 5000  length 67  (frame 101 bytes)
  0.041  srv → pc   UDP 5000 → 59437  length 54  (frame 88 bytes)

線の上を流れたのは、行きと帰りの2つだけです。あいさつも、受け取りの ACK も、別れのあいさつもありません。返事まで 0.041 秒、ちょうど1往復でした。第6回の TCP では、同じ申請に 10 個の小包と、別れまで 0.132 秒を使っていました。

UDP の見出しは 8 バイト、TCP は 20 バイト以上

UDP の見出し 送り元の番号 宛先の番号 長さ チェックサム 8 バイト。これで全部 TCP の見出し 送り元の番号 宛先の番号 Seq Ack 長さ・フラグ 窓 チェックサム 緊急 20 バイト+オプション 今回は 32 バイト 横幅が 32 ビット(4 バイト)。UDP には、番号と確認、窓、フラグがない

表紙の写真の明細を見ると、UDP の見出しは番号2つ・長さ・チェックサムだけです。Seq も Ack も窓もありません。つまり UDP は、IP の小包に部屋番号を書き足しただけのもので、届いたかどうか、順番どおりかどうかは、何も保証しません。

TCPUDP
あいさつあり(1往復)なし。いきなり送る
落ちたら送り直すそれっきり
順番並べ直す着いた順
受け手・道の都合窓と混み具合で加減送りたいだけ送る
区切りなし(バイトの流れ)残る(1回送ったら1つ)
おもな使い道Web、メール、データベースDNS、音声・映像、ゲーム、QUIC
三

区切りが残る

第6回で「TCP は区切りを覚えない」と書きました。UDP と並べて確かめます。100 バイトを3回送り、サーバーは 0.3 秒待ってから読みます。

# PC:100 バイトを3回送る(TCP は小包をためずにすぐ送る設定 TCP_NODELAY)
pc$ send(b'A' * 100); send(b'B' * 100); send(b'C' * 100)
# 線の上では、どちらも 100 バイトの小包が3個
    TCP  Len=100  Len=100  Len=100      UDP  Len=100  Len=100  Len=100
# サーバー:0.3 秒待ってから読むと
    TCP recv() → [300] bytes
    UDP recv() → [100, 100, 100] bytes

TCP は1本の流れ、UDP は1通ずつ

TCP AAA BBB CCC 100 バイトを3回 send AAA…BBB…CCC… AAA BBB CCC recv 1回で 300 バイト 一本のバイトの流れ UDP AAA BBB CCC 100 バイトを3回 send AAA BBB CCC recv 3回、100 バイトずつ 3つのまとまり(データグラム)

線の上では、どちらも 100 バイトの小包が3つでした。ところが TCP の受け手は、1回の recv() で 300 バイトをまとめて受け取りました。小包の区切りは、受け手の箱に入った時点で消えてしまうのです。UDP では、1回送ったものが1通の手紙(データグラム)として、そのまま1回ずつ受け取れます。TCP で自前のやりとりを作るときは、長さを先に書く、改行で区切るなど、区切り方を決めておく必要があります。

四

誰もいない番号

TCP では、誰もいない番号へつなぐと RST が返りました。UDP には RST がありません。代わりに、相手の OS が ICMP の「その番号は使っていない(Port unreachable)」を返します。

# 誰もいない 5009 番へ UDP を送る。サーバーは ICMP で「その番号は使っていない」と返す
  0.000 192.168.1.10 → 203.0.113.20 UDP 47 33355 → 5009 Len=5
  0.040 203.0.113.20 → 192.168.1.10 ICMP 75 Destination unreachable (Port unreachable)
# connect() したソケットは、この知らせをエラーとして受け取れる。しなかったソケットは知らずに待ち続ける
    connected socket: ConnectionRefusedError: [Errno 111] Connection refused after 40.7 ms
    unconnected socket: TimeoutError: timed out after 2002 ms

同じ知らせが届いているのに、プログラムの受け取り方は2通りでした。connect() で相手を決めておいたソケットは、0.041 秒で Connection refused のエラーになりました。決めずに sendto() したソケットは、知らせが自分あてだと結びつけられず、2 秒の時間切れまで待ち続けました。UDP でも、相手が1つなら connect() しておくとよい、という小さなコツです。もっとも、途中で落ちた場合は何も返ってこないので、時間切れの備えはいつでも必要です。

五

大きすぎる手紙

UDP の手紙は、1通で最大 6万5507 バイトまで書けます。でも線の MTU は 1500 バイト(第2回)。入りきらない手紙は、IP が断片に切り分けて送ります。3000 バイトを1通送りました。

# 3000 バイトの UDP を1つ送る(線の MTU は 1500)
  0.000 192.168.1.10 → 203.0.113.20 IPv4 1514 Fragmented IP protocol (proto=UDP 17, off=0, ID=56e0)
  0.000 192.168.1.10 → 203.0.113.20 IPv4 1514 Fragmented IP protocol (proto=UDP 17, off=1480, ID=56e0)
  0.000 192.168.1.10 → 203.0.113.20 UDP 82 58842 → 5000 Len=3000
  0.041 203.0.113.20 → 192.168.1.10 UDP 56 5000 → 58842 Len=14
    3000 bytes -> reply {"size": 3000} in 41.2 ms
# 2つ目の断片を線で落とすと
    3000 bytes -> no reply (2 s timeout)
# 30 秒後、サーバーが「組み立て時間切れ」を返してきた
  30.470 203.0.113.20 → 192.168.1.10 ICMP 590 Time-to-live exceeded (Fragment reassembly time exceeded)
Wireshark のパケット一覧を2つ縦に並べたもの。上は Fragmented IP protocol (proto=UDP 17, off=0) と (off=1480)、続いて UDP 58842 → 5000 Len=3000、最後にサーバーの返事。下は同じく3つの断片を送ったあと、30.470528 秒にサーバーから ICMP Time-to-live exceeded (Fragment reassembly time exceeded) が届いている
3000 バイトの UDP は、IP の断片3つに分かれて送られます。下は2つ目の断片を線で落としたとき。サーバーは残りを 30 秒抱えたあと捨て、「組み立て時間切れ」を知らせてきました(筆者がクラウド上に作った練習用ネットワークで、2026年10月25日に記録したものを Wireshark 4.2.2 で開いて撮影。画面は英語のまま。ウインドウの上の帯と見出しは説明のために描き足したもの)

断片のどれか1つが落ちると、手紙は丸ごと届かない

3008 バイト UDP データグラム(見出し 8 B + 3000 B) 1480 B off=0 1480 B off=1480 48 off=2960 3つとも届けば、サーバーで組み立てて 1 つの UDP に戻す 1つ落ちると全部むだ。30 秒待って捨てる 断片に分かれると、落ちる確率が足し算で増える。UDP では大きな塊を避けるのが定石

3000 バイトに見出し 8 バイトを足した 3008 バイトが、1480・1480・48 バイトの3つに分かれ、サーバーで組み立て直されました。ところが、2つ目の断片を線で落とすと、残り2つは届いているのに、手紙は1通も届きません。サーバーは 30 秒待ってから断片を捨て、「組み立て時間切れ」を返してきました(30.47 秒)。UDP は送り直さないので、これで終わりです。

このため UDP では、1通を線に収まる大きさにするのが定石です。DNS では、2020 年に関係者が足並みをそろえ、UDP の返事を 1232 バイトまでに抑える既定値を推奨しました(DNS Flag Day 2020、第9回)。

六

先頭がつかえる

新井落ちたらそれっきりなんて、やっぱり TCP のほうが安心じゃないですか。

古井音声を送るつもりで比べてみよう。20 ミリ秒ごとに小さな知らせを 50 個送って、10 個目だけを落とす。

# 20 ms ごとに 100 バイトの知らせを 50 個。10 個目だけを線で落とす。届くまでの時間(ms)
    No.      7      8      9     10     11     12     13     14     15
# UDP:49 個届いた。10 個目は届かず、ほかは待たされない
    UDP   20.6   20.6   20.3     —    20.3   20.4   20.4   20.4   20.4
# TCP:50 個届いた。10 個目は送り直しで遅れ、11・12 個目もそれを待った
    TCP   20.4   20.4   20.3   80.9   60.6   40.5   20.6   20.3   20.6

1つ落ちたとき、後ろの知らせはどうなるか(実測)

0 20 40 60 80 1 2 4 6 8 × 10 12 14 16 18 20 80.9 ms UDP(× は届かなかった) TCP 横=何個目の知らせか、縦=届くまでの時間(ms)。10 個目を線で落とした TCP は順番を守るため、1つ欠けると後ろの知らせも待たせる(先頭のつかえ)
Wireshark のパケット一覧。PC から 5102 番へ Len=100 の小包が 20 ミリ秒ごとに並び、20番の Seq=901 のあと、26番からサーバーの TCP Dup ACK 23#1、#2、#3 が黒地に赤い文字で並ぶ。27番の TCP Retransmission Seq=901 が選ばれている
TCP で知らせを 20 ミリ秒ごとに送り、10 個目(Seq=901)を線で落としたところ。サーバーは同じ Ack=901 をくり返し、PC は 27番で送り直しました。それまで 11・12 個目は、届いていても受け手のアプリには渡されません(筆者がクラウド上に作った練習用ネットワークで、2026年10月25日に記録したものを Wireshark 4.2.2 で開いて撮影。画面は英語のまま。ウインドウの上の帯と見出しは説明のために描き足したもの)

UDP では 10 個目が届かなかっただけで、ほかの 49 個はすべて約 20 ミリ秒で着きました。TCP は 50 個すべて届けましたが、10 個目は送り直しを待って 80.9 ミリ秒。さらに、先に着いていた 11 個目と 12 個目も、10 個目がそろうまでアプリに渡してもらえず、60.6 と 40.5 ミリ秒かかりました。順番を守るために、先頭の1つが後ろをつかえさせる、これを先頭のつかえ(head-of-line blocking)と呼びます。

音声なら、20 ミリ秒分が1つ欠けても、ほとんど気づきません。むしろ、遅れて届いた声のほうが邪魔です。だから音声・映像の通話やゲームは UDP を使い、欠けた分は自分で補います。DNS も、1往復で済む短い問い合わせなので UDP です(第9回)。そして、HTTP/3 が使う QUIC は、UDP の上に、送り直しや順番の管理を自前で組み立てています。中の流れごとに順番を守るので、1つの流れで落ちても、ほかの流れはつかえません(第10回)。

七

ソケット:プログラムの窓口

新井TCP や UDP って、わたしのプログラムからはどうやって使うんですか? fetch しか書いたことないです。

古井fetch も、いちばん下では OS のソケットを使っている。いわば、OS が用意したネットワークの窓口だ。

ソケットの呼び出しの順番

クライアント(PC) サーバー socket() connect() send() recv() close() socket() bind() listen() accept() recv() send() close() 3ウェイハンドシェイク 申請 返事 口を作る 番号を決める 待ち受ける 1本取り出す UDP は listen・accept がなく、socket → bind → sendto / recvfrom だけ

サーバーは、socket() で窓口を作り、bind() で番号を決め、listen() で待ち受け、accept() で客を1人ずつ取り出します。クライアントは connect() でつなぎます。この connect と accept の間で、第6回の3ウェイハンドシェイクが起きます。Python で書くと、これだけです。

# サーバー(いちばん小さい形)
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # TCP の窓口。UDP なら SOCK_DGRAM
s.bind(('0.0.0.0', 5000))
s.listen()
while True:
    c, addr = s.accept()          # あいさつの済んだ客を1人取り出す
    data = c.recv(4096)
    c.sendall(b'ok')
    c.close()

# クライアント
c = socket.create_connection(('203.0.113.20', 5000))
c.sendall(b'hello'); print(c.recv(4096)); c.close()

本当にこの順番で呼ばれているのかを、strace(プログラムが OS に頼んだことを一覧にする道具)で確かめました。上は PC で申請を1回送る Python、下はそれを受ける側の Node.js です。

# PC:ソケットを直接使って申請を1回送る Python を strace で見る(右は、その呼び出しにかかった秒数)
  socket(AF_INET, SOCK_STREAM) = 3                 0.0001
  connect(3, {203.0.113.20:3000}, 16) = 0          0.0408
  sendto(3, "POST /api/ex…", 181) = 181            0.0001
  recvfrom(3, "HTTP/1.1 201…", 4096) = 175         0.0411
  recvfrom(3, "", 4096) = 0                        0.0001
  close(3) = 0                                     0.0001
# サーバー:Node.js の中で起きていること(epoll_pwait で「何か来た?」と待ち、来たら処理する)
  epoll_pwait(15, [fd 21 IN]) = 1                  1.4631
  accept4(21) = 22                                 0.0001
  epoll_pwait(15, [fd 22 IN]) = 1                  0.0000
  read(22, "POST /api/ex…", 65536) = 199           0.0000
  writev(22, ["HTTP/1.1 201…"]) = 203              0.0002
  epoll_pwait(15, []) = 0                          0.0001
  epoll_pwait(15, [fd 22 IN]) = 1                  0.0391
  read(22, "", 65536) = 0                          0.0001
  shutdown(22, SHUT_WR) = 0                        0.0002
  epoll_ctl(15, EPOLL_CTL_DEL, 22) = 0             0.0000
  close(22) = 0                                    0.0000

PC 側の socket() が返した 3 は、ファイルを開いたときと同じ「ファイル記述子」の番号です。ソケットは、OS から見ればファイルの一種なのです。connect() は 0.041 秒、つまりあいさつの1往復の間、プログラムを止めて待ちました。返事を待つ recvfrom() も 0.041 秒。最後の、中身が空の recvfrom() は、相手が別れのあいさつ(FIN)を送ってきたしるしです。

八

受け付け待ちの行列

あいさつは OS が済ませ、アプリは accept() で取り出すだけ。では、アプリが取り出さなかったら? listen(1) のまま一度も accept しないサーバーへ、4本つないでみました。

# listen(1) のまま accept() しないサーバーへ、4本つなぐ(つなぐ側は 3 秒であきらめる)
    client 1: connected in 42 ms
    client 2: connected in 41 ms
    client 3: TimeoutError after 3003 ms
    client 4: TimeoutError after 3003 ms
# サーバー側:2本はあいさつが済んで、取り出されるのを待っている
srv$ ss -tn sport = :5005 ; ss -tln sport = :5005
    State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
    ESTAB 0 0 203.0.113.20:5005 192.168.1.10:48164
    ESTAB 0 0 203.0.113.20:5005 192.168.1.10:48160
    State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
    LISTEN 2 1 0.0.0.0:5005 0.0.0.0:*

あいさつ済みの接続は、行列で accept を待つ

PC 1 PC 2 PC 3 SYN を捨てる PC 4 SYN を捨てる 受け付け待ちの行列 あいさつ済み あいさつ済み アプリ accept しない connect() が成功しても、アプリが受け取ったとは限らない 行列があふれると、SYN は黙って捨てられ、つなぐ側には timed out に見える

1本目と2本目は 0.04 秒でつながったと言われました。アプリは何もしていないのに、です。OS があいさつを済ませ、行列(LISTEN の Recv-Q 2)に並べておいたからです。行列が 2(listen の数 + 1)でいっぱいになると、3本目と4本目の SYN は黙って捨てられ、送り直しのすえ 3 秒で時間切れになりました。

サーバーが忙しくて accept が追いつかないと、客からは「つながらない」ではなく「遅い」「時間切れ」に見えます。listen の数の既定は、Node.js が 511、Python が 128 で、Linux の上限(somaxconn)はこのカーネルでは 4096 でした。

九

1人でたくさんの客をさばく

さきほどの Python のサーバーは、recv() で待っている間、ほかの客の相手ができません。昔は客1人に1つのプロセスやスレッドを用意していましたが、何千人にもなると重すぎます。1999 年には、1台で1万の接続をさばく難しさが「C10K 問題」と名付けられました。

見張り役(epoll)に「どれか来た?」と尋ねる

epoll 見張り役 fd 21 待ち受け fd 22 客 1 fd 23 客 2 fd 24 客 3 Node.js の 1つのスレッド 準備できた fd どれか来た? Node.js も nginx も、この形で1つのスレッドから何千もの接続をさばく

いまの答えは、待つのは見張り役に任せることです。Linux の epoll に、待ち受けのソケットも客のソケットもまとめて登録し、1つのスレッドが「どれか準備できた?」と尋ねては、準備できたものだけを処理します。さきほどの strace の下半分がまさにそれで、Node.js は epoll_pwait で 1.46 秒待ち、待ち受け(21番)に客が来たので accept4 で 22 番を取り出し、22 番が読めるようになったので読み、返事を書き、また epoll に戻りました。Node.js のイベントループも nginx も、この形です。

POINT

Node.js で重い計算を同期的に書くと、その間 epoll に戻れず、すべての客が待たされます。1つのスレッドでたくさんの客をさばく代わりに、1人の客に長居させてはいけない、という約束があるわけです。

十

自分の PC で確かめる

調べたいことMacWindowsLinux
UDP の口の一覧netstat -an -p udpnetstat -an -p udpss -uan
その番号を使うプログラムlsof -i UDP:5353netstat -ano -p udpss -uanp
UDP を1通送るnc -u 宛先 5000(Python などで)nc -u 宛先 5000
プログラムの呼び出しを見るdtruss(制限あり)Process Monitorstrace -e trace=%network

Mac で netstat -an -p udp を見ると、5353 番(同じ LAN の機械を見つける mDNS)など、何もしていないつもりでも UDP の口がいくつも開いているはずです。

結 — 一枚まとめと、次回

  1. UDP は見出し 8 バイト。あいさつ・送り直し・順番・窓がない。申請は行きと帰りの2つ、1往復で済んだ。
  2. UDP は区切りが残る。100 バイト×3 は、TCP では 1 回の recv で 300 バイト、UDP では 3 回。
  3. 誰もいない UDP の番号には ICMP。connect() したソケットだけがエラーとして受け取れる。
  4. MTU を超える UDP は断片に分かれ、1つ落ちると丸ごと失われる(30 秒後に組み立て時間切れ)。
  5. 1つ落ちると、TCP は後ろもつかえ(80.9・60.6・40.5 ms)、UDP は欠けるだけで遅れない。QUIC は UDP の上で両立をねらう。
  6. プログラムはソケットで TCP・UDP を使う。ソケットはファイル記述子の一つ。
  7. あいさつは OS が済ませ、アプリは accept で取り出す。行列があふれると SYN は黙って捨てられる。多数の客は epoll で1つのスレッドがさばく。

新井表紙の写真、よく見たら申請の JSON がそのまま読めますね……。日付も、金額の 1280 も。

古井そう、ここまでの申請は、途中の誰にでも丸見えだった。書きかえることもできる。次回は第5・6層、TLS。封筒に鍵をかける話だ。

参考

記録は、筆者がクラウド上の Linux(Ubuntu 24.04、カーネル 6.18)にネットワーク名前空間で作った練習用ネットワークで、2026年10月25日に取ったものです。いたずら線は第6回のものに、UDP の小包(IP の断片を含む)を落とす機能を足しました。UDP の申請窓口・区切りの実験・受け付けないサーバー・知らせの送受信は Python、申請 API は Node.js です。strace は Ubuntu の strace パッケージで、呼び出しごとの所要時間(-T)を付けて記録しました。表の数字はすべて、この記録とそのときのコマンドの出力から読んだ実際の値です。登場人物は架空の人物です。

いにしえのネットワーク入門 第7回 — UDP とソケット / 2026.10

← 記事一覧へ