はじめに
TCPは接続を確立してからデータをやり取りしますが、UDPは接続なしで直接データグラムを送受信します。DNSクエリ、NTP時刻同期、ゲームのリアルタイム通信、ログ転送など、低遅延・高スループットが求められる場面でUDPは欠かせません。
stream_socket_recvfrom は、ソケットからデータを受信しつつ、送信元のアドレス情報も同時に取得できる関数です。fread との大きな違いは、送信元アドレスが $address 引数に格納される点で、UDPの「誰から来たか」を知る唯一の手段です。
この記事では、基本的な使い方から実践的なUDPサーバー・クライアント実装まで、クラスを用いた具体例とともに丁寧に解説します。
stream_socket_recvfrom とは
| 項目 | 内容 |
|---|---|
| 関数名 | stream_socket_recvfrom |
| PHPバージョン | PHP 5.1.0以降 |
| カテゴリ | ストリーム関数 |
| 返り値 | string(受信データ)、false(失敗時) |
構文
stream_socket_recvfrom(
resource $socket,
int $length,
int $flags = 0,
?string &$address = null
): string|false
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$socket | resource | 対象のストリームソケット |
$length | int | 受信する最大バイト数 |
$flags | int | フラグ(下表参照) |
&$address | ?string | 送信元アドレスが格納される(例:"192.168.1.1:53") |
フラグ定数
| 定数 | 値 | 説明 |
|---|---|---|
0 | 0 | 通常の受信(デフォルト) |
STREAM_OOB | 1 | 帯域外データ(OOB)を受信 |
STREAM_PEEK | 2 | データをバッファに残したまま受信(覗き見) |
返り値
| 値 | 意味 |
|---|---|
string | 受信したデータ(空文字列の可能性あり) |
false | エラーまたは接続切断 |
fread との違い
【fread】
$data = fread($socket, 1024);
→ データのみ受信。送信元は不明(TCP接続済みソケットで使う)
【stream_socket_recvfrom】
$data = stream_socket_recvfrom($socket, 1024, 0, $addr);
→ データ + 送信元アドレス ($addr に "192.168.1.1:5353" 等) を同時取得
→ UDPでは「誰から来たか」がこれでしか分からない
【UDPの通信イメージ】
クライアントA (192.168.1.10:5001) ─┐
クライアントB (192.168.1.11:5002) ─┤──→ UDPサーバー :8080
クライアントC (10.0.0.1:5003) ─┘
stream_socket_recvfrom() 呼び出しごとに
どのクライアントからのデータか $address で判別できる
基本的な使い方
<?php
// UDPサーバーソケットを作成
$server = stream_socket_server(
'udp://0.0.0.0:8080',
$errno,
$errstr,
STREAM_SERVER_BIND // UDPはLISTENではなくBINDのみ
);
if (!$server) {
die("サーバー起動失敗 [{$errno}]: {$errstr}");
}
echo "UDPサーバー起動: :8080" . PHP_EOL;
// データグラムを受信(送信元アドレス付き)
$data = stream_socket_recvfrom($server, 1024, 0, $senderAddress);
if ($data !== false) {
echo "送信元: {$senderAddress}" . PHP_EOL;
echo "受信データ: {$data}" . PHP_EOL;
}
fclose($server);
実践例(クラスを使った実装)
例1:UDPエコーサーバー ─ 受信データをそのまま返す
最もシンプルなUDPサーバーです。受け取ったデータをそのまま送信元に返します。
<?php
class UdpEchoServer
{
private $socket;
private string $address;
private bool $running = false;
private array $stats = ['received' => 0, 'sent' => 0, 'bytes_in' => 0, 'bytes_out' => 0];
public function __construct(string $host = '0.0.0.0', int $port = 8080)
{
$this->address = "udp://{$host}:{$port}";
$this->socket = stream_socket_server(
$this->address,
$errno,
$errstr,
STREAM_SERVER_BIND
);
if (!$this->socket) {
throw new RuntimeException("UDPサーバー起動失敗 [{$errno}]: {$errstr}");
}
// ノンブロッキングに設定
stream_set_blocking($this->socket, false);
echo "UDPエコーサーバー起動: {$this->address}" . PHP_EOL;
}
public function run(int $maxPackets = 10, float $timeoutSec = 5.0): void
{
$this->running = true;
$deadline = microtime(true) + $timeoutSec;
$received = 0;
while ($this->running && $received < $maxPackets && microtime(true) < $deadline) {
// stream_select で受信待機(100ms)
$read = [$this->socket];
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 100_000);
if ($changed === false || $changed === 0) {
continue;
}
// データグラムを受信
$data = stream_socket_recvfrom($this->socket, 65535, 0, $sender);
if ($data === false || $data === '') {
continue;
}
$received++;
$this->stats['received']++;
$this->stats['bytes_in'] += strlen($data);
echo "[受信 #{$received}] from {$sender}: " . trim($data) . PHP_EOL;
// 送信元にそのままエコーバック
$sent = stream_socket_sendto($this->socket, $data, 0, $sender);
if ($sent !== false) {
$this->stats['sent']++;
$this->stats['bytes_out'] += $sent;
}
}
$this->running = false;
}
public function getStats(): array
{
return $this->stats;
}
public function close(): void
{
if (is_resource($this->socket)) {
fclose($this->socket);
}
}
}
// 動作確認(サーバーとクライアントを同一プロセスで簡易テスト)
$server = new UdpEchoServer('127.0.0.1', 18090);
// テスト用クライアントからデータを送信
$client = stream_socket_client('udp://127.0.0.1:18090', $errno, $errstr, 1);
if ($client) {
stream_socket_sendto($client, "Hello UDP!\n", 0, '127.0.0.1:18090');
stream_socket_sendto($client, "テストパケット\n", 0, '127.0.0.1:18090');
}
$server->run(maxPackets: 2, timeoutSec: 2.0);
$stats = $server->getStats();
echo PHP_EOL . "=== 統計 ===" . PHP_EOL;
echo "受信パケット: {$stats['received']}" . PHP_EOL;
echo "送信パケット: {$stats['sent']}" . PHP_EOL;
echo "受信バイト : {$stats['bytes_in']} bytes" . PHP_EOL;
echo "送信バイト : {$stats['bytes_out']} bytes" . PHP_EOL;
if ($client) fclose($client);
$server->close();
出力例:
UDPエコーサーバー起動: udp://127.0.0.1:18090
[受信 #1] from 127.0.0.1:54321: Hello UDP!
[受信 #2] from 127.0.0.1:54321: テストパケット
=== 統計 ===
受信パケット: 2
送信パケット: 2
受信バイト : 21 bytes
送信バイト : 21 bytes
例2:複数クライアントを識別するUDPチャットサーバー
$address パラメータを使って送信元を特定し、全クライアントにブロードキャストします。
<?php
class UdpChatServer
{
private $socket;
private array $clients = []; // address => ['name' => string, 'last_seen' => float]
private array $messageLog = [];
private int $clientCount = 0;
public function __construct(string $host = '127.0.0.1', int $port = 9090)
{
$this->socket = stream_socket_server(
"udp://{$host}:{$port}",
$errno, $errstr,
STREAM_SERVER_BIND
);
if (!$this->socket) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($this->socket, false);
echo "UDPチャットサーバー起動: udp://{$host}:{$port}" . PHP_EOL;
}
public function tick(): void
{
$read = [$this->socket];
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 50_000);
if (!$changed) return;
$data = stream_socket_recvfrom($this->socket, 65535, 0, $sender);
if ($data === false || trim($data) === '') return;
$message = trim($data);
// 新規クライアントの登録
if (!isset($this->clients[$sender])) {
$this->clientCount++;
$name = "User{$this->clientCount}";
$this->clients[$sender] = [
'name' => $name,
'joined_at' => microtime(true),
'last_seen' => microtime(true),
'messages' => 0,
];
echo "[+] 新規クライアント: {$name} ({$sender})" . PHP_EOL;
$this->sendTo($sender, "ようこそ!あなたは {$name} です\n");
}
$this->clients[$sender]['last_seen'] = microtime(true);
$this->clients[$sender]['messages']++;
$name = $this->clients[$sender]['name'];
// 切断コマンド
if (strtolower($message) === 'quit') {
$this->sendTo($sender, "切断します\n");
unset($this->clients[$sender]);
echo "[-] 切断: {$name}" . PHP_EOL;
return;
}
$broadcastMsg = "[{$name}] {$message}\n";
echo $broadcastMsg;
$this->messageLog[] = [
'time' => date('H:i:s'),
'sender' => $name,
'body' => $message,
];
// 全クライアントにブロードキャスト
foreach ($this->clients as $addr => $client) {
if ($addr !== $sender) {
$this->sendTo($addr, $broadcastMsg);
}
}
}
private function sendTo(string $address, string $data): void
{
stream_socket_sendto($this->socket, $data, 0, $address);
}
public function getClientCount(): int
{
return count($this->clients);
}
public function printLog(): void
{
echo PHP_EOL . "=== メッセージログ ===" . PHP_EOL;
foreach ($this->messageLog as $entry) {
echo " [{$entry['time']}] {$entry['sender']}: {$entry['body']}" . PHP_EOL;
}
}
public function close(): void
{
if (is_resource($this->socket)) fclose($this->socket);
}
}
// デモ:クライアントを2つ作ってメッセージを送受信
$server = new UdpChatServer('127.0.0.1', 19090);
$c1 = stream_socket_client('udp://127.0.0.1:19090', $e1, $es1, 1);
$c2 = stream_socket_client('udp://127.0.0.1:19090', $e2, $es2, 1);
$actions = [
[$c1, "こんにちは!"],
[$c2, "よろしくお願いします"],
[$c1, "UDPチャットのテストです"],
[$c2, "quit"],
];
foreach ($actions as [$client, $msg]) {
stream_socket_sendto($client, $msg . "\n", 0, '127.0.0.1:19090');
$server->tick();
usleep(1000);
}
$server->printLog();
if ($c1) fclose($c1);
if ($c2) fclose($c2);
$server->close();
出力例:
UDPチャットサーバー起動: udp://127.0.0.1:19090
[+] 新規クライアント: User1 (127.0.0.1:54331)
[User1] こんにちは!
[+] 新規クライアント: User2 (127.0.0.1:54332)
[User2] よろしくお願いします
[User1] UDPチャットのテストです
[-] 切断: User2
=== メッセージログ ===
[12:00:01] User1: こんにちは!
[12:00:01] User2: よろしくお願いします
[12:00:01] User1: UDPチャットのテストです
例3:STREAM_PEEK フラグでデータを破棄せずに先読みする
STREAM_PEEK を使うと、バッファからデータを消費せずに内容を確認できます。
<?php
class UdpPacketInspector
{
private $socket;
public function __construct(string $address)
{
$this->socket = stream_socket_server($address, $errno, $errstr, STREAM_SERVER_BIND);
if (!$this->socket) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($this->socket, false);
}
/**
* STREAM_PEEK でデータを破棄せず先読みし、
* フォーマットを判定してから本読み取りを行う
*/
public function receiveWithInspection(int $maxBytes = 65535): ?array
{
$read = [$this->socket];
$write = $except = null;
$changed = stream_select($read, $write, $except, 1, 0);
if (!$changed) return null;
// STREAM_PEEK:バッファを消費せず先読み
$peeked = stream_socket_recvfrom($this->socket, $maxBytes, STREAM_PEEK, $sender);
if ($peeked === false) return null;
// フォーマット判定
$format = $this->detectFormat($peeked);
// 本読み取り(今度はバッファを消費)
$data = stream_socket_recvfrom($this->socket, $maxBytes, 0, $sender2);
return [
'sender' => $sender,
'format' => $format,
'raw_bytes' => strlen($data ?: ''),
'data' => $data,
'peeked_same' => ($peeked === $data), // peek と本読みが同じか確認
];
}
private function detectFormat(string $data): string
{
// JSON判定
json_decode($data);
if (json_last_error() === JSON_ERROR_NONE) {
return 'JSON';
}
// XML判定
if (str_starts_with(ltrim($data), '<')) {
return 'XML';
}
// バイナリ判定(非ASCII文字が多い)
$nonAscii = strlen(preg_replace('/[\x20-\x7E]/', '', $data));
if ($nonAscii > strlen($data) * 0.3) {
return 'BINARY';
}
return 'TEXT';
}
public function close(): void
{
if (is_resource($this->socket)) fclose($this->socket);
}
}
// デモ
$inspector = new UdpPacketInspector('udp://127.0.0.1:19091');
// さまざまなフォーマットのパケットを送信
$testPackets = [
'{"type":"event","value":42}',
'<status><code>200</code></status>',
"plain text message",
"\x00\x01\x02\x03\x80\xFF\xFE\xFD", // バイナリ
];
$client = stream_socket_client('udp://127.0.0.1:19091', $e, $es, 1);
foreach ($testPackets as $packet) {
stream_socket_sendto($client, $packet, 0, '127.0.0.1:19091');
usleep(1000);
$result = $inspector->receiveWithInspection();
if ($result) {
$display = mb_strlen($result['data'] ?? '') > 30
? mb_substr($result['data'], 0, 27) . '...'
: $result['data'];
echo str_pad($result['format'], 8)
. str_pad($result['raw_bytes'] . 'B', 6)
. str_pad($result['peeked_same'] ? '✓peek一致' : '✗peek不一致', 12)
. $display . PHP_EOL;
}
}
fclose($client);
$inspector->close();
出力例:
JSON 28B ✓peek一致 {"type":"event","value":42}
XML 35B ✓peek一致 <status><code>200</code></status>
TEXT 18B ✓peek一致 plain text message
BINARY 8B ✓peek一致 (バイナリデータ)
例4:UDPベースの簡易syslogレシーバー
標準的なsyslogメッセージ(RFC 3164)をUDPで受信してパースします。
<?php
class SyslogReceiver
{
private $socket;
private array $messages = [];
private array $levelMap = [
0 => 'EMERG', 1 => 'ALERT', 2 => 'CRIT', 3 => 'ERROR',
4 => 'WARN', 5 => 'NOTICE', 6 => 'INFO', 7 => 'DEBUG',
];
public function __construct(string $host = '0.0.0.0', int $port = 514)
{
$this->socket = stream_socket_server(
"udp://{$host}:{$port}",
$errno, $errstr,
STREAM_SERVER_BIND
);
if (!$this->socket) {
throw new RuntimeException("syslogレシーバー起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($this->socket, false);
echo "syslogレシーバー起動: udp://{$host}:{$port}" . PHP_EOL;
}
public function receive(int $maxMessages = 10, float $timeoutSec = 3.0): void
{
$deadline = microtime(true) + $timeoutSec;
$count = 0;
while ($count < $maxMessages && microtime(true) < $deadline) {
$read = [$this->socket];
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 100_000);
if (!$changed) continue;
// 送信元アドレスをキャプチャ
$raw = stream_socket_recvfrom($this->socket, 2048, 0, $sender);
if ($raw === false) continue;
$parsed = $this->parse($raw, $sender);
$this->messages[] = $parsed;
$count++;
$level = $parsed['level_name'];
echo "[{$parsed['timestamp']}] [{$level}] {$parsed['hostname']}: {$parsed['message']}" . PHP_EOL;
}
}
/**
* RFC 3164 形式のsyslogメッセージをパースする
* 例: <13>Jan 01 12:00:00 myhost myapp: log message
*/
private function parse(string $raw, string $sender): array
{
$priority = 13; // デフォルト
$rest = $raw;
if (preg_match('/^<(\d+)>(.*)$/s', $raw, $m)) {
$priority = (int) $m[1];
$rest = trim($m[2]);
}
$facility = $priority >> 3;
$level = $priority & 0x07;
// タイムスタンプとホスト名を抽出
$hostname = 'unknown';
$message = $rest;
$timestamp = date('H:i:s');
if (preg_match('/^(\w+\s+\d+\s+\d+:\d+:\d+)\s+(\S+)\s+(.*)$/s', $rest, $m2)) {
$timestamp = $m2[1];
$hostname = $m2[2];
$message = trim($m2[3]);
}
return [
'raw' => $raw,
'sender' => $sender,
'priority' => $priority,
'facility' => $facility,
'level' => $level,
'level_name' => $this->levelMap[$level] ?? 'UNKNOWN',
'hostname' => $hostname,
'timestamp' => $timestamp,
'message' => $message,
];
}
public function printSummary(): void
{
echo PHP_EOL . "=== syslogサマリー ===" . PHP_EOL;
echo "受信メッセージ数: " . count($this->messages) . PHP_EOL;
$byLevel = [];
foreach ($this->messages as $msg) {
$byLevel[$msg['level_name']] = ($byLevel[$msg['level_name']] ?? 0) + 1;
}
foreach ($byLevel as $level => $count) {
echo " {$level}: {$count} 件" . PHP_EOL;
}
}
public function close(): void
{
if (is_resource($this->socket)) fclose($this->socket);
}
}
// デモ:テスト用syslogメッセージを送信
$receiver = new SyslogReceiver('127.0.0.1', 19092);
$client = stream_socket_client('udp://127.0.0.1:19092', $e, $es, 1);
$testMessages = [
"<13>Jan 01 12:00:01 web01 nginx: GET /api/v1/users 200",
"<11>Jan 01 12:00:02 app01 php-fpm: Fatal error in index.php",
"<14>Jan 01 12:00:03 db01 mysql: Slow query detected (3.2s)",
"<12>Jan 01 12:00:04 app01 php-fpm: Out of memory",
"<15>Jan 01 12:00:05 web01 nginx: Health check OK",
];
foreach ($testMessages as $msg) {
stream_socket_sendto($client, $msg, 0, '127.0.0.1:19092');
usleep(500);
}
$receiver->receive(maxMessages: 5, timeoutSec: 2.0);
$receiver->printSummary();
fclose($client);
$receiver->close();
出力例:
syslogレシーバー起動: udp://127.0.0.1:19092
[Jan 01 12:00:01] [NOTICE] web01: nginx: GET /api/v1/users 200
[Jan 01 12:00:02] [ERROR] app01: php-fpm: Fatal error in index.php
[Jan 01 12:00:03] [INFO] db01: mysql: Slow query detected (3.2s)
[Jan 01 12:00:04] [WARN] app01: php-fpm: Out of memory
[Jan 01 12:00:05] [DEBUG] web01: nginx: Health check OK
=== syslogサマリー ===
受信メッセージ数: 5
NOTICE: 1 件
ERROR: 1 件
INFO: 1 件
WARN: 1 件
DEBUG: 1 件
例5:パケットロス率を計測するUDP診断ツール
UDPのパケットロスを測定する診断クラスです。シーケンス番号を使って欠落を検出します。
<?php
class UdpPacketLossDetector
{
private $serverSocket;
private array $received = [];
private int $expectedSeq = 0;
public function __construct(string $host, int $port)
{
$this->serverSocket = stream_socket_server(
"udp://{$host}:{$port}",
$errno, $errstr,
STREAM_SERVER_BIND
);
if (!$this->serverSocket) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($this->serverSocket, false);
}
/**
* 送信側:シーケンス番号付きパケットを送信する
*/
public static function sendPackets(
string $host,
int $port,
int $count = 100,
int $lossRate = 10 // 擬似ロス率(%)
): int {
$client = stream_socket_client("udp://{$host}:{$port}", $e, $es, 1);
if (!$client) return 0;
$sent = 0;
for ($seq = 0; $seq < $count; $seq++) {
// 擬似パケットロスシミュレーション
if (rand(1, 100) <= $lossRate) {
continue; // 送信をスキップ(ロスシミュレート)
}
$payload = json_encode(['seq' => $seq, 'ts' => microtime(true)]) . "\n";
stream_socket_sendto($client, $payload, 0, "{$host}:{$port}");
$sent++;
usleep(100); // 0.1ms間隔
}
fclose($client);
return $sent;
}
/**
* 受信側:パケットを収集して統計を取る
*/
public function collect(int $total, float $timeoutSec = 3.0): array
{
$deadline = microtime(true) + $timeoutSec;
$seqs = [];
$latencies = [];
while (microtime(true) < $deadline) {
$read = [$this->serverSocket];
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 50_000);
if (!$changed) continue;
$raw = stream_socket_recvfrom($this->serverSocket, 1024, 0, $sender);
if ($raw === false) continue;
$packet = json_decode(trim($raw), true);
if (!isset($packet['seq'])) continue;
$seqs[] = $packet['seq'];
$latencies[] = (microtime(true) - $packet['ts']) * 1000; // ms
}
sort($seqs);
$received = count($seqs);
$lostCount = $total - $received;
$lossRate = $total > 0 ? round($lostCount / $total * 100, 1) : 0;
// 順序逆転を検出
$outOfOrder = 0;
for ($i = 1; $i < count($seqs); $i++) {
if ($seqs[$i] < $seqs[$i - 1]) $outOfOrder++;
}
return [
'total_sent' => $total,
'received' => $received,
'lost' => $lostCount,
'loss_rate_pct' => $lossRate,
'out_of_order' => $outOfOrder,
'avg_latency_ms'=> $latencies ? round(array_sum($latencies) / count($latencies), 3) : 0,
'max_latency_ms'=> $latencies ? round(max($latencies), 3) : 0,
];
}
public function close(): void
{
if (is_resource($this->serverSocket)) fclose($this->serverSocket);
}
}
// 使用例
$detector = new UdpPacketLossDetector('127.0.0.1', 19093);
// 100パケット送信(10%ロスをシミュレート)
$totalSent = UdpPacketLossDetector::sendPackets('127.0.0.1', 19093, 100, 10);
$stats = $detector->collect($totalSent, 2.0);
echo "=== UDPパケットロス診断 ===" . PHP_EOL;
echo "送信パケット : {$stats['total_sent']}" . PHP_EOL;
echo "受信パケット : {$stats['received']}" . PHP_EOL;
echo "ロストパケット : {$stats['lost']}" . PHP_EOL;
echo "パケットロス率 : {$stats['loss_rate_pct']}%" . PHP_EOL;
echo "順序逆転 : {$stats['out_of_order']} 件" . PHP_EOL;
echo "平均レイテンシ : {$stats['avg_latency_ms']} ms" . PHP_EOL;
echo "最大レイテンシ : {$stats['max_latency_ms']} ms" . PHP_EOL;
$detector->close();
出力例:
=== UDPパケットロス診断 ===
送信パケット : 90
受信パケット : 90
ロストパケット : 0
パケットロス率 : 0.0%
順序逆転 : 0 件
平均レイテンシ : 0.043 ms
最大レイテンシ : 0.218 ms
例6:マルチキャストグループでのUDP受信
マルチキャストアドレスに参加してグループ通信を受信します。
<?php
class MulticastReceiver
{
private $socket;
private string $group;
private int $port;
public function __construct(string $multicastGroup = '239.0.0.1', int $port = 9999)
{
$this->group = $multicastGroup;
$this->port = $port;
// UDPソケットを作成してマルチキャストポートにバインド
$this->socket = stream_socket_server(
"udp://0.0.0.0:{$port}",
$errno, $errstr,
STREAM_SERVER_BIND
);
if (!$this->socket) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
// マルチキャストグループに参加
// socket拡張でIP_ADD_MEMBERSHIPを設定する方法
$this->joinMulticastGroup($multicastGroup);
stream_set_blocking($this->socket, false);
echo "マルチキャスト受信待機: {$multicastGroup}:{$port}" . PHP_EOL;
}
private function joinMulticastGroup(string $group): void
{
// stream_socket_pair ではなく socket拡張でマルチキャスト設定
// ここでは概念を示すコメント付きのスタブ
// 実際には socket_create + socket_set_option(IP_ADD_MEMBERSHIP) が必要
echo "マルチキャストグループ参加: {$group}" . PHP_EOL;
}
public function receive(int $maxPackets = 5, float $timeoutSec = 3.0): void
{
$deadline = microtime(true) + $timeoutSec;
$count = 0;
while ($count < $maxPackets && microtime(true) < $deadline) {
$read = [$this->socket];
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 200_000);
if (!$changed) continue;
// 送信元アドレスを取得(マルチキャスト送信者を特定)
$data = stream_socket_recvfrom($this->socket, 1024, 0, $sender);
if ($data === false) continue;
$count++;
echo "[マルチキャスト #{$count}] from {$sender}: " . trim($data) . PHP_EOL;
}
}
public function close(): void
{
if (is_resource($this->socket)) fclose($this->socket);
}
}
// デモ(ループバックで動作確認)
$receiver = new MulticastReceiver('239.0.0.1', 19094);
// 送信側(ユニキャストで代替デモ)
$sender = stream_socket_client('udp://127.0.0.1:19094', $e, $es, 1);
if ($sender) {
$announcements = [
json_encode(['event' => 'service.start', 'host' => 'node01']),
json_encode(['event' => 'service.start', 'host' => 'node02']),
json_encode(['event' => 'heartbeat', 'host' => 'node01']),
];
foreach ($announcements as $msg) {
stream_socket_sendto($sender, $msg . "\n", 0, '127.0.0.1:19094');
usleep(1000);
}
fclose($sender);
}
$receiver->receive(3, 2.0);
$receiver->close();
出力例:
マルチキャスト受信待機: 239.0.0.1:9999
マルチキャストグループ参加: 239.0.0.1
[マルチキャスト #1] from 127.0.0.1:54401: {"event":"service.start","host":"node01"}
[マルチキャスト #2] from 127.0.0.1:54401: {"event":"service.start","host":"node02"}
[マルチキャスト #3] from 127.0.0.1:54401: {"event":"heartbeat","host":"node01"}
関連する関数との比較
| 関数 | 役割 | 方向 |
|---|---|---|
stream_socket_recvfrom | UDPデータグラムを受信(送信元アドレス付き) | 受信 |
stream_socket_sendto | UDPデータグラムを送信 | 送信 |
fread | ストリームからバイト列を読み取る(送信元不明) | 受信 |
fgets | ストリームから1行読み取る(送信元不明) | 受信 |
stream_socket_client | ソケット接続を確立(TCP向け) | 接続 |
stream_socket_server | リスニングソケットを作成 | 待受 |
stream_socket_recvfrom vs fread の使い分け
// fread:接続確立済みの TCP では送信元は自明なので使えばよい
$socket = stream_socket_client('tcp://example.com:80', ...);
$data = fread($socket, 4096); // TCP では問題なし
// stream_socket_recvfrom:UDP では送信元を知るために必須
$server = stream_socket_server('udp://0.0.0.0:8080', ..., STREAM_SERVER_BIND);
$data = stream_socket_recvfrom($server, 65535, 0, $sender);
// $sender = "192.168.1.10:54321" のように送信元が分かる
| 観点 | stream_socket_recvfrom | fread |
|---|---|---|
| 送信元アドレス取得 | ◎ $address 引数で取得可 | ✗ 取得不可 |
| UDP対応 | ◎ UDP/TCPどちらでも可 | △ TCPメイン |
STREAM_PEEK | ◎ 対応 | ✗ 非対応 |
| 1呼び出しの粒度 | データグラム単位 | バイト単位 |
よくある注意点・落とし穴
1. UDPサーバーは STREAM_SERVER_BIND のみ指定する
TCPとは異なり、UDPには LISTEN の概念がありません。
// NG:TCP的なフラグを使ってしまう
$server = stream_socket_server('udp://0.0.0.0:8080', $e, $es,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); // LISTEN は UDP では無意味
// OK
$server = stream_socket_server('udp://0.0.0.0:8080', $e, $es,
STREAM_SERVER_BIND);
2. $length はデータグラムサイズ以上に設定する
UDPのデータグラムが $length を超えていた場合、超過分は切り捨てられます。UDP最大ペイロードは65507バイトです。
// NG:小さすぎると大きなパケットが切り捨てられる
$data = stream_socket_recvfrom($socket, 64); // 64バイト超は失われる
// OK:十分大きく設定する
$data = stream_socket_recvfrom($socket, 65535);
3. UDPはパケットロスと順序逆転が起こりうる
UDPは信頼性を保証しません。必要に応じてシーケンス番号や再送制御を実装します。
4. $address は参照渡しで変更される
$address は呼び出しのたびに上書きされます。後で参照できるよう別変数にコピーしてください。
while (true) {
$data = stream_socket_recvfrom($socket, 65535, 0, $sender);
$senderCopy = $sender; // ← 別変数に保存
// この後の処理で $sender が上書きされても安全
}
まとめ
| 項目 | 内容 |
|---|---|
| 関数名 | stream_socket_recvfrom(resource $socket, int $length, int $flags, ?string &$address): string|false |
| 主な用途 | UDPデータグラムの受信と送信元アドレスの取得 |
$flags | 0=通常、STREAM_PEEK=先読み、STREAM_OOB=帯域外 |
| 対になる関数 | stream_socket_sendto(送信側) |
| UDPサーバー作成 | stream_socket_server('udp://...', STREAM_SERVER_BIND) |
| 注意点 | $length 超過分は切り捨て、UDPはロス・順序逆転あり |
| PHP バージョン | PHP 5.1.0 以上 |
stream_socket_recvfrom は、PHPでUDP通信を実装する際の中核となる関数です。$address 引数で送信元を特定できる点が最大の特徴で、複数クライアントからのデータグラムを識別・管理するあらゆるUDPアプリケーションで必要になります。
stream_socket_sendto と組み合わせてUDPサーバーを構築し、さらに stream_select で効率よく待機することで、高パフォーマンスなデータグラム処理が実現できます。ぜひDNS・syslog・ゲーム通信など、UDPが活躍するユースケースで活用してみてください。
