はじめに
PHPでネットワークサーバーを実装する際、まず必要になるのが「待ち受け用ソケット」の作成です。stream_socket_server は、TCP・UDP・UNIXドメインソケットなど、さまざまな種類のリスニングソケットをシンプルなAPIで作成できる関数です。
stream_socket_accept・stream_socket_recvfrom・stream_select と組み合わせることで、HTTPサーバー・チャットサーバー・プロキシ・UDPサービスなど、本格的なサーバーアプリケーションをPHPで構築できます。
この記事では、基本的な使い方から実践的なサーバー実装まで、クラスを用いた具体例とともに丁寧に解説します。
stream_socket_server とは
| 項目 | 内容 |
|---|---|
| 関数名 | stream_socket_server |
| PHPバージョン | PHP 5.0.0以降 |
| カテゴリ | ストリーム関数 |
| 返り値 | resource(成功時)、false(失敗時) |
構文
stream_socket_server(
string $address,
int &$error_code = null,
string &$error_message = null,
int $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
?resource $context = null
): resource|false
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$address | string | バインドするアドレス(スキーム付き) |
&$error_code | int | 失敗時のエラーコードが格納される |
&$error_message | string | 失敗時のエラーメッセージが格納される |
$flags | int | 動作フラグ(下表参照) |
$context | ?resource | ストリームコンテキスト(SSL設定など) |
フラグ定数
| 定数 | 説明 |
|---|---|
STREAM_SERVER_BIND | アドレスにバインドする |
STREAM_SERVER_LISTEN | 接続の待ち受けを開始する(TCP用) |
UDPの場合:
STREAM_SERVER_BINDのみ指定します。UDPにはLISTENの概念がありません。
サポートされるアドレス形式
tcp://0.0.0.0:8080 → TCPサーバー(全インターフェース)
tcp://127.0.0.1:8080 → TCPサーバー(ループバックのみ)
udp://0.0.0.0:9090 → UDPサーバー
ssl://0.0.0.0:8443 → TLS/SSLサーバー
unix:///var/run/app.sock → UNIXドメインソケット
tcp://[::]:8080 → IPv6 TCPサーバー
基本的な使い方
<?php
// TCPサーバーソケットを作成
$server = stream_socket_server(
'tcp://0.0.0.0:8080',
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
);
if ($server === false) {
die("サーバー起動失敗 [{$errno}]: {$errstr}");
}
echo "TCPサーバー起動: :8080" . PHP_EOL;
// クライアントの接続を受け付ける
$client = stream_socket_accept($server, 30, $peerName);
if ($client) {
echo "接続: {$peerName}" . PHP_EOL;
fwrite($client, "Hello!\n");
fclose($client);
}
fclose($server);
実践例(クラスを使った実装)
例1:再利用可能なサーバーソケットファクトリー
TCP・UDP・UNIXドメインソケットを統一的に作成する設定クラスです。
<?php
class ServerSocketFactory
{
/**
* TCPリスニングソケットを作成する
*/
public static function createTcp(
string $host = '0.0.0.0',
int $port = 8080,
bool $reuseAddr = true
): resource {
$context = stream_context_create([
'socket' => [
'so_reuseport' => $reuseAddr,
'backlog' => 128,
],
]);
$server = stream_socket_server(
"tcp://{$host}:{$port}",
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$context
);
if ($server === false) {
throw new RuntimeException("TCP起動失敗 [{$errno}]: {$errstr}");
}
return $server;
}
/**
* UDPバインドソケットを作成する
*/
public static function createUdp(string $host = '0.0.0.0', int $port = 9090): resource
{
$server = stream_socket_server(
"udp://{$host}:{$port}",
$errno,
$errstr,
STREAM_SERVER_BIND // UDP は LISTEN 不要
);
if ($server === false) {
throw new RuntimeException("UDP起動失敗 [{$errno}]: {$errstr}");
}
return $server;
}
/**
* UNIXドメインソケットを作成する
*/
public static function createUnix(string $path): resource
{
// 既存のソケットファイルを削除
if (file_exists($path)) {
unlink($path);
}
$server = stream_socket_server(
"unix://{$path}",
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
);
if ($server === false) {
throw new RuntimeException("UNIX起動失敗 [{$errno}]: {$errstr}");
}
chmod($path, 0660);
return $server;
}
/**
* SSL/TLSサーバーソケットを作成する
*/
public static function createTls(
string $host,
int $port,
string $certFile,
string $keyFile
): resource {
$context = stream_context_create([
'ssl' => [
'local_cert' => $certFile,
'local_pk' => $keyFile,
'verify_peer' => false,
'allow_self_signed' => true,
],
]);
$server = stream_socket_server(
"ssl://{$host}:{$port}",
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$context
);
if ($server === false) {
throw new RuntimeException("TLS起動失敗 [{$errno}]: {$errstr}");
}
return $server;
}
/**
* サーバーソケットの情報を返す
*/
public static function describe(resource $server): array
{
$name = stream_socket_get_name($server, false);
$meta = stream_get_meta_data($server);
return [
'address' => $name ?: 'unknown',
'stream_type' => $meta['stream_type'],
'mode' => $meta['mode'],
'blocked' => $meta['blocked'],
];
}
}
// 使用例
$tcpServer = ServerSocketFactory::createTcp('127.0.0.1', 19200);
$udpServer = ServerSocketFactory::createUdp('127.0.0.1', 19201);
$tcpInfo = ServerSocketFactory::describe($tcpServer);
$udpInfo = ServerSocketFactory::describe($udpServer);
echo "=== サーバーソケット情報 ===" . PHP_EOL;
foreach (['TCP' => $tcpInfo, 'UDP' => $udpInfo] as $type => $info) {
echo "{$type}:" . PHP_EOL;
foreach ($info as $key => $val) {
echo " {$key}: " . var_export($val, true) . PHP_EOL;
}
}
fclose($tcpServer);
fclose($udpServer);
出力例:
=== サーバーソケット情報 ===
TCP:
address: 127.0.0.1:19200
stream_type: tcp_socket/server
mode: r+
blocked: true
UDP:
address: 127.0.0.1:19201
stream_type: udp_socket
mode: r+
blocked: true
例2:シングルプロセスで複数ポートを同時に待ち受けるマルチポートサーバー
stream_select と組み合わせて、1プロセスで複数のリスニングソケットを効率よく管理します。
<?php
class MultiPortServer
{
private array $servers = []; // port => resource
private array $clients = []; // id => resource
private array $peerNames= []; // id => string
private bool $running = false;
private array $stats = [];
public function listen(int $port, string $host = '127.0.0.1'): void
{
$server = stream_socket_server(
"tcp://{$host}:{$port}",
$errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
);
if ($server === false) {
throw new RuntimeException("ポート{$port}の起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($server, false);
$this->servers[$port] = $server;
$this->stats[$port] = ['connections' => 0, 'bytes' => 0];
echo "待ち受け開始: {$host}:{$port}" . PHP_EOL;
}
public function run(int $maxIterations = 50): void
{
$this->running = true;
$iterations = 0;
while ($this->running && $iterations < $maxIterations) {
$iterations++;
// 監視対象:全サーバーソケット + 全クライアントソケット
$read = array_merge(array_values($this->servers), array_values($this->clients));
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 100_000);
if ($changed === false || $changed === 0) continue;
foreach ($read as $sock) {
// サーバーソケット → 新規接続
$port = array_search($sock, $this->servers, true);
if ($port !== false) {
$this->acceptClient($sock, (int)$port);
continue;
}
// クライアントソケット → データ受信
$this->handleClient($sock);
}
}
}
private function acceptClient(resource $server, int $port): void
{
$client = stream_socket_accept($server, 0, $peerName);
if ($client === false) return;
stream_set_blocking($client, false);
$id = (int) $client;
$this->clients[$id] = $client;
$this->peerNames[$id] = $peerName;
$this->stats[$port]['connections']++;
echo " [:{$port}] 接続: {$peerName}" . PHP_EOL;
fwrite($client, "ポート{$port}へようこそ\r\n");
}
private function handleClient(resource $client): void
{
$id = (int) $client;
$data = fread($client, 4096);
if ($data === false || $data === '' || feof($client)) {
$peer = $this->peerNames[$id] ?? 'unknown';
echo " 切断: {$peer}" . PHP_EOL;
fclose($client);
unset($this->clients[$id], $this->peerNames[$id]);
return;
}
fwrite($client, "受信: " . trim($data) . "\r\n");
}
public function printStats(): void
{
echo PHP_EOL . "=== ポート別統計 ===" . PHP_EOL;
foreach ($this->stats as $port => $stat) {
echo " :{$port} → 接続数: {$stat['connections']}" . PHP_EOL;
}
}
public function shutdown(): void
{
foreach ($this->clients as $c) fclose($c);
foreach ($this->servers as $s) fclose($s);
$this->running = false;
}
}
// 使用例:3つのポートを同時に待ち受け
$srv = new MultiPortServer();
$srv->listen(19210);
$srv->listen(19211);
$srv->listen(19212);
// テストクライアントから各ポートに接続
$tc1 = stream_socket_client('tcp://127.0.0.1:19210', $e, $es, 1);
$tc2 = stream_socket_client('tcp://127.0.0.1:19211', $e, $es, 1);
$tc3 = stream_socket_client('tcp://127.0.0.1:19212', $e, $es, 1);
$srv->run(20);
foreach ([$tc1, $tc2, $tc3] as $tc) {
if ($tc) fclose($tc);
}
$srv->printStats();
$srv->shutdown();
出力例:
待ち受け開始: 127.0.0.1:19210
待ち受け開始: 127.0.0.1:19211
待ち受け開始: 127.0.0.1:19212
[:19210] 接続: 127.0.0.1:54441
[:19211] 接続: 127.0.0.1:54442
[:19212] 接続: 127.0.0.1:54443
=== ポート別統計 ===
:19210 → 接続数: 1
:19211 → 接続数: 1
:19212 → 接続数: 1
例3:シンプルなHTTPサーバー ─ リクエストをパースしてレスポンスを返す
stream_socket_server で作ったリスニングソケットにブラウザからアクセスできるHTTPサーバーです。
<?php
class SimpleHttpServer
{
private $server;
private array $routes = [];
private int $port;
private array $accessLog = [];
public function __construct(int $port = 8080, string $host = '127.0.0.1')
{
$this->port = $port;
$this->server = stream_socket_server(
"tcp://{$host}:{$port}",
$errno, $errstr
);
if (!$this->server) {
throw new RuntimeException("HTTP起動失敗 [{$errno}]: {$errstr}");
}
echo "HTTP サーバー起動: http://{$host}:{$port}/" . PHP_EOL;
}
public function get(string $path, callable $handler): void
{
$this->routes['GET'][$path] = $handler;
}
public function run(int $maxRequests = 10): void
{
for ($i = 0; $i < $maxRequests; $i++) {
$client = stream_socket_accept($this->server, 10.0, $peerName);
if ($client === false) continue;
stream_set_timeout($client, 5);
$this->handleRequest($client, $peerName);
}
fclose($this->server);
}
private function handleRequest(resource $client, string $peer): void
{
$raw = '';
while (!feof($client)) {
$raw .= fread($client, 4096);
if (str_contains($raw, "\r\n\r\n")) break;
$meta = stream_get_meta_data($client);
if ($meta['timed_out']) break;
}
[$method, $path, $protocol] = $this->parseRequestLine($raw);
$headers = $this->parseHeaders($raw);
$this->accessLog[] = [
'time' => date('H:i:s'),
'peer' => $peer,
'method' => $method,
'path' => $path,
];
echo "[{$peer}] {$method} {$path}" . PHP_EOL;
$handler = $this->routes[$method][$path] ?? null;
if ($handler) {
[$status, $contentType, $body] = $handler($path, $headers);
} else {
$status = 404;
$contentType = 'text/plain; charset=UTF-8';
$body = "404 Not Found: {$path}";
}
$response = $this->buildResponse($status, $contentType, $body);
fwrite($client, $response);
fclose($client);
}
private function parseRequestLine(string $raw): array
{
$line = strtok($raw, "\r\n");
$parts = explode(' ', $line ?? '');
return [
$parts[0] ?? 'GET',
$parts[1] ?? '/',
$parts[2] ?? 'HTTP/1.1',
];
}
private function parseHeaders(string $raw): array
{
$headers = [];
$lines = explode("\r\n", explode("\r\n\r\n", $raw)[0] ?? '');
array_shift($lines); // リクエスト行を除く
foreach ($lines as $line) {
if (str_contains($line, ':')) {
[$k, $v] = explode(':', $line, 2);
$headers[strtolower(trim($k))] = trim($v);
}
}
return $headers;
}
private function buildResponse(int $status, string $contentType, string $body): string
{
$texts = [200 => 'OK', 201 => 'Created', 404 => 'Not Found', 500 => 'Internal Server Error'];
$text = $texts[$status] ?? 'Unknown';
$len = strlen($body);
return implode("\r\n", [
"HTTP/1.1 {$status} {$text}",
"Content-Type: {$contentType}",
"Content-Length: {$len}",
"Connection: close",
"X-Powered-By: PHP/" . PHP_VERSION,
"",
$body,
]);
}
public function printAccessLog(): void
{
echo PHP_EOL . "=== アクセスログ ===" . PHP_EOL;
foreach ($this->accessLog as $entry) {
echo " [{$entry['time']}] {$entry['peer']} {$entry['method']} {$entry['path']}" . PHP_EOL;
}
}
}
// ルート定義
$http = new SimpleHttpServer(19220);
$http->get('/', fn($p, $h) => [200, 'text/html; charset=UTF-8',
'<h1>トップページ</h1><p>PHPサーバーへようこそ!</p>']);
$http->get('/api/status', fn($p, $h) => [200, 'application/json',
json_encode(['status' => 'ok', 'time' => date('c'), 'php' => PHP_VERSION])]);
$http->get('/api/echo', fn($p, $h) => [200, 'application/json',
json_encode(['headers' => $h, 'path' => $p])]);
// テスト用クライアントでリクエスト送信
$requests = ['/', '/api/status', '/api/echo', '/notfound'];
foreach ($requests as $path) {
$c = stream_socket_client('tcp://127.0.0.1:19220', $e, $es, 1);
if ($c) {
fwrite($c, "GET {$path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n");
fclose($c);
}
}
$http->run(count($requests));
$http->printAccessLog();
出力例:
HTTP サーバー起動: http://127.0.0.1:19220/
[127.0.0.1:54451] GET /
[127.0.0.1:54452] GET /api/status
[127.0.0.1:54453] GET /api/echo
[127.0.0.1:54454] GET /notfound
=== アクセスログ ===
[12:00:01] 127.0.0.1:54451 GET /
[12:00:01] 127.0.0.1:54452 GET /api/status
[12:00:01] 127.0.0.1:54453 GET /api/echo
[12:00:01] 127.0.0.1:54454 GET /notfound
例4:SO_REUSEADDRでポートの即時再利用を可能にする
サーバー再起動時に「Address already in use」エラーを回避するコンテキスト設定です。
<?php
class ResilientServer
{
private $server = null;
private string $address;
private int $maxRestarts;
private int $restartCount = 0;
public function __construct(string $address, int $maxRestarts = 3)
{
$this->address = $address;
$this->maxRestarts = $maxRestarts;
}
public function start(): void
{
$context = stream_context_create([
'socket' => [
// SO_REUSEADDR:TIME_WAIT 状態のポートを即時再利用
'so_reuseport' => true,
// バックログキューサイズ(同時接続待ち数)
'backlog' => 256,
// TCP_NODELAY:Nagleアルゴリズムを無効化(低遅延)
'tcp_nodelay' => true,
],
]);
$this->server = stream_socket_server(
$this->address,
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$context
);
if ($this->server === false) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($this->server, false);
echo "サーバー起動: {$this->address}" . PHP_EOL;
}
/**
* サーバーを安全に再起動する
*/
public function restart(): void
{
if ($this->restartCount >= $this->maxRestarts) {
throw new RuntimeException("最大再起動回数 ({$this->maxRestarts}) に達しました");
}
echo "再起動中... ({$this->restartCount}/{$this->maxRestarts})" . PHP_EOL;
if (is_resource($this->server)) {
fclose($this->server);
$this->server = null;
}
sleep(1);
$this->start();
$this->restartCount++;
}
public function tick(): bool
{
if (!is_resource($this->server)) return false;
$read = [$this->server];
$write = $except = null;
$changed = stream_select($read, $write, $except, 0, 100_000);
if ($changed > 0) {
$client = stream_socket_accept($this->server, 0, $peerName);
if ($client) {
stream_set_timeout($client, 3);
$data = fread($client, 1024);
fwrite($client, "Echo: " . trim($data) . "\r\n");
fclose($client);
echo " 処理: {$peerName}" . PHP_EOL;
return true;
}
}
return false;
}
public function getRestartCount(): int { return $this->restartCount; }
public function shutdown(): void
{
if (is_resource($this->server)) {
fclose($this->server);
$this->server = null;
echo "シャットダウン完了" . PHP_EOL;
}
}
}
// 使用例
$server = new ResilientServer('tcp://127.0.0.1:19230');
$server->start();
// テスト接続
$c = stream_socket_client('tcp://127.0.0.1:19230', $e, $es, 1);
if ($c) {
fwrite($c, "Hello\r\n");
$server->tick();
echo "応答: " . trim(fgets($c)) . PHP_EOL;
fclose($c);
}
// 再起動テスト
$server->restart();
echo "再起動回数: " . $server->getRestartCount() . PHP_EOL;
$server->shutdown();
出力例:
サーバー起動: tcp://127.0.0.1:19230
処理: 127.0.0.1:54461
応答: Echo: Hello
再起動中... (0/3)
サーバー起動: tcp://127.0.0.1:19230
再起動回数: 1
シャットダウン完了
例5:UNIXドメインソケットサーバー ─ 高速ローカルIPC
ネットワークスタックを使わないUNIXドメインソケットでのプロセス間通信サーバーです。
<?php
class UnixSocketServer
{
private $server;
private string $socketPath;
private array $requestLog = [];
public function __construct(string $socketPath)
{
$this->socketPath = $socketPath;
// 既存ソケットファイルを削除
if (file_exists($socketPath)) {
unlink($socketPath);
}
$this->server = stream_socket_server(
"unix://{$socketPath}",
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
);
if (!$this->server) {
throw new RuntimeException("UNIX起動失敗 [{$errno}]: {$errstr}");
}
chmod($socketPath, 0660);
echo "UNIXソケットサーバー起動: {$socketPath}" . PHP_EOL;
}
/**
* JSONリクエスト/レスポンスプロトコルで処理する
*/
public function serveJsonRpc(int $maxRequests = 10, float $timeout = 5.0): void
{
stream_set_blocking($this->server, false);
$deadline = microtime(true) + $timeout;
$handled = 0;
while ($handled < $maxRequests && microtime(true) < $deadline) {
$read = [$this->server];
$write = $except = null;
if (!stream_select($read, $write, $except, 0, 100_000)) continue;
$client = stream_socket_accept($this->server, 0);
if (!$client) continue;
stream_set_timeout($client, 3);
$raw = '';
while (!feof($client)) {
$raw .= fread($client, 4096);
if (str_ends_with(rtrim($raw), '}')) break;
$meta = stream_get_meta_data($client);
if ($meta['timed_out']) break;
}
$request = json_decode(trim($raw), true);
$response = $this->dispatch($request ?? []);
fwrite($client, json_encode($response) . "\n");
fclose($client);
$handled++;
}
}
private function dispatch(array $request): array
{
$method = $request['method'] ?? '';
$params = $request['params'] ?? [];
$id = $request['id'] ?? null;
$this->requestLog[] = ['method' => $method, 'time' => date('H:i:s')];
$result = match($method) {
'ping' => ['pong' => true, 'ts' => microtime(true)],
'echo' => ['echo' => $params],
'add' => ['result' => array_sum($params)],
'status' => ['php' => PHP_VERSION, 'pid' => getmypid(), 'mem' => memory_get_usage(true)],
default => null,
};
if ($result === null) {
return ['id' => $id, 'error' => ['code' => -32601, 'message' => "Method not found: {$method}"]];
}
return ['id' => $id, 'result' => $result];
}
public function printLog(): void
{
echo PHP_EOL . "=== リクエストログ ===" . PHP_EOL;
foreach ($this->requestLog as $entry) {
echo " [{$entry['time']}] {$entry['method']}" . PHP_EOL;
}
}
public function cleanup(): void
{
if (is_resource($this->server)) fclose($this->server);
if (file_exists($this->socketPath)) unlink($this->socketPath);
}
}
// 使用例
$sockPath = sys_get_temp_dir() . '/php_rpc_' . getmypid() . '.sock';
$rpcServer = new UnixSocketServer($sockPath);
// クライアント側でリクエスト
$calls = [
['method' => 'ping', 'id' => 1],
['method' => 'add', 'params' => [10, 20, 30], 'id' => 2],
['method' => 'status', 'id' => 3],
['method' => 'echo', 'params' => ['hello', 'world'], 'id' => 4],
['method' => 'unknown','id' => 5],
];
// クライアントから非同期で送信
foreach ($calls as $call) {
$c = stream_socket_client("unix://{$sockPath}", $e, $es, 1);
if (!$c) continue;
fwrite($c, json_encode($call));
$rpcServer->serveJsonRpc(1, 1.0);
$resp = fgets($c);
$data = json_decode(trim($resp), true);
echo "ID:{$data['id']} → " . json_encode($data['result'] ?? $data['error'], JSON_UNESCAPED_UNICODE) . PHP_EOL;
fclose($c);
}
$rpcServer->printLog();
$rpcServer->cleanup();
出力例:
UNIXソケットサーバー起動: /tmp/php_rpc_12345.sock
ID:1 → {"pong":true,"ts":1716000001.123}
ID:2 → {"result":60}
ID:3 → {"php":"8.3.0","pid":12345,"mem":2097152}
ID:4 → {"echo":["hello","world"]}
ID:5 → {"code":-32601,"message":"Method not found: unknown"}
=== リクエストログ ===
[12:00:01] ping
[12:00:01] add
[12:00:01] status
[12:00:01] echo
[12:00:01] unknown
例6:グレースフルシャットダウン付きサーバーライフサイクルマネージャー
シグナルハンドラと組み合わせ、処理中のリクエストを完結させてから安全に終了します。
<?php
class ServerLifecycleManager
{
private $server;
private array $activeClients = [];
private bool $shutdownRequested = false;
private array $lifecycle = [];
private int $totalHandled = 0;
public function __construct(string $address)
{
$this->server = stream_socket_server($address, $errno, $errstr);
if (!$this->server) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
stream_set_blocking($this->server, false);
$this->log('started', "アドレス: {$address}");
// SIGTERM/SIGINT でグレースフルシャットダウン
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGTERM, [$this, 'requestShutdown']);
pcntl_signal(SIGINT, [$this, 'requestShutdown']);
}
}
public function requestShutdown(): void
{
$this->shutdownRequested = true;
$this->log('shutdown_requested', "アクティブ接続: " . count($this->activeClients));
echo PHP_EOL . "シャットダウン要求受信(アクティブ: " . count($this->activeClients) . ")" . PHP_EOL;
}
public function run(int $maxIterations = 30): void
{
$this->log('running');
for ($i = 0; $i < $maxIterations && !($this->shutdownRequested && empty($this->activeClients)); $i++) {
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
$read = array_merge(
$this->shutdownRequested ? [] : [$this->server],
array_values($this->activeClients)
);
$write = $except = null;
if (empty($read)) {
usleep(100_000);
continue;
}
$changed = stream_select($read, $write, $except, 0, 100_000);
if (!$changed) continue;
foreach ($read as $sock) {
if ($sock === $this->server) {
$this->accept();
} else {
$this->handle($sock);
}
}
}
$this->shutdown();
}
private function accept(): void
{
$client = stream_socket_accept($this->server, 0, $peerName);
if (!$client) return;
stream_set_blocking($client, false);
$id = (int) $client;
$this->activeClients[$id] = $client;
$this->log('accept', $peerName);
echo " [接続] {$peerName}(アクティブ: " . count($this->activeClients) . ")" . PHP_EOL;
}
private function handle(resource $client): void
{
$id = (int) $client;
$data = fread($client, 4096);
if ($data === false || $data === '' || feof($client)) {
$this->log('disconnect');
fclose($client);
unset($this->activeClients[$id]);
return;
}
fwrite($client, "ACK: " . trim($data) . "\r\n");
$this->totalHandled++;
}
private function shutdown(): void
{
// 残存クライアントを全部閉じる
foreach ($this->activeClients as $client) {
fwrite($client, "サーバーは停止します\r\n");
fclose($client);
}
$this->activeClients = [];
if (is_resource($this->server)) fclose($this->server);
$this->log('stopped', "合計処理: {$this->totalHandled}");
echo "シャットダウン完了(合計処理: {$this->totalHandled})" . PHP_EOL;
}
private function log(string $event, string $detail = ''): void
{
$this->lifecycle[] = [
'time' => date('H:i:s'),
'event' => $event,
'detail' => $detail,
];
}
public function printLifecycle(): void
{
echo PHP_EOL . "=== ライフサイクルログ ===" . PHP_EOL;
foreach ($this->lifecycle as $entry) {
$detail = $entry['detail'] ? " ({$entry['detail']})" : '';
echo " [{$entry['time']}] {$entry['event']}{$detail}" . PHP_EOL;
}
}
}
// 使用例
$mgr = new ServerLifecycleManager('tcp://127.0.0.1:19240');
// テストクライアント
$clients = [];
for ($i = 0; $i < 3; $i++) {
$c = stream_socket_client('tcp://127.0.0.1:19240', $e, $es, 1);
if ($c) $clients[] = $c;
}
$mgr->run(15);
foreach ($clients as $c) fclose($c);
$mgr->printLifecycle();
出力例:
[接続] 127.0.0.1:54471(アクティブ: 1)
[接続] 127.0.0.1:54472(アクティブ: 2)
[接続] 127.0.0.1:54473(アクティブ: 3)
シャットダウン完了(合計処理: 0)
=== ライフサイクルログ ===
[12:00:01] started (アドレス: tcp://127.0.0.1:19240)
[12:00:01] running
[12:00:01] accept (127.0.0.1:54471)
[12:00:01] accept (127.0.0.1:54472)
[12:00:01] accept (127.0.0.1:54473)
[12:00:01] stopped (合計処理: 0)
関連する関数との比較
| 関数 | 役割 |
|---|---|
stream_socket_server | リスニングソケットを作成する |
stream_socket_accept | クライアントの接続を受け付ける(TCP) |
stream_socket_client | サーバーへ接続する(クライアント側) |
stream_socket_recvfrom | UDPデータグラムを受信する |
stream_socket_sendto | UDPデータグラムを送信する |
stream_select | 複数ソケットを効率よく監視する |
stream_socket_get_name | ソケットのアドレス情報を取得する |
TCP vs UDP のフラグ使い分け
// TCP:BIND + LISTEN の両方が必要
$tcp = stream_socket_server(
'tcp://0.0.0.0:8080',
$errno, $errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN // デフォルトと同じ
);
// → stream_socket_accept() で接続を受け付ける
// UDP:BIND のみ(LISTEN は不要)
$udp = stream_socket_server(
'udp://0.0.0.0:9090',
$errno, $errstr,
STREAM_SERVER_BIND // LISTEN は指定しない
);
// → stream_socket_recvfrom() でデータグラムを受信する
| 観点 | TCP | UDP |
|---|---|---|
| フラグ | BIND | LISTEN | BIND のみ |
| 接続受け付け | stream_socket_accept() | 不要(コネクションレス) |
| データ受信 | fread() / fgets() | stream_socket_recvfrom() |
| 信頼性 | 保証あり | 保証なし |
よくある注意点・落とし穴
1. ポートが使用中は $error_code / $error_message を確認する
$server = stream_socket_server('tcp://0.0.0.0:80', $errno, $errstr);
if ($server === false) {
// $errno=98(Linux: EADDRINUSE)など、OSのエラーコードが入る
echo "エラー [{$errno}]: {$errstr}" . PHP_EOL;
}
2. 1024 番未満のポートはroot権限が必要(Linux/macOS)
ポート80や443など特権ポートは、root 権限がないと stream_socket_server が失敗します。開発中は 8080 など高番号ポートを使いましょう。
3. プロセス終了後のポート再利用
サーバーを停止してすぐ同じポートで再起動すると Address already in use になることがあります。so_reuseport コンテキストオプションで回避できます。
$ctx = stream_context_create(['socket' => ['so_reuseport' => true]]);
$server = stream_socket_server('tcp://0.0.0.0:8080', $e, $es,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx);
4. UNIXソケットファイルは終了時に削除する
プロセスが異常終了するとソケットファイルが残ります。再起動時に file_exists で確認して削除するか、register_shutdown_function で登録しておきましょう。
$path = '/tmp/myapp.sock';
register_shutdown_function(function() use ($path) {
if (file_exists($path)) unlink($path);
});
まとめ
| 項目 | 内容 |
|---|---|
| 関数名 | stream_socket_server(string $address, ...): resource|false |
| 主な用途 | TCP/UDP/TLS/UNIXドメインのリスニングソケット作成 |
| TCPフラグ | STREAM_SERVER_BIND | STREAM_SERVER_LISTEN(デフォルト) |
| UDPフラグ | STREAM_SERVER_BIND のみ |
| セットで使う関数 | stream_socket_accept(TCP)、stream_socket_recvfrom(UDP) |
| 注意点 | 特権ポートはroot必要、UNIXソケットは終了時に削除、so_reuseport で即時再利用 |
| PHP バージョン | PHP 5.0.0 以上 |
stream_socket_server は、PHPでネットワークサーバーを構築する際の出発点となる関数です。TCP・UDP・TLS・UNIXドメインソケットを統一的なAPIで作成でき、stream_select と組み合わせることで1プロセスで多数の接続を効率よく処理できます。
stream_socket_accept / stream_socket_recvfrom / stream_socket_sendto と組み合わせて覚えることで、PHPによるネットワークプログラミングの全体像が完成します。ぜひ実際のサービス開発に活用してみてください。
