はじめに
PHPでTCPサーバーを実装する際、クライアントからの接続を「受け入れる」処理が必要になります。stream_socket_server でリスニングソケットを作成した後、stream_socket_accept を呼び出すことで、実際の通信相手との接続ストリームを取得できます。
stream_socket_accept は、待ち受けているサーバーソケットに対して接続してきたクライアントを受け付け、その接続に対応したストリームリソースを返す関数です。HTTPサーバー、チャットサーバー、プロキシなど、あらゆるTCPサーバーの中核を担います。
この記事では、基本的な使い方から実践的なサーバー実装まで、クラスを用いた具体例とともに丁寧に解説します。
stream_socket_accept とは
| 項目 | 内容 |
|---|---|
| 関数名 | stream_socket_accept |
| PHPバージョン | PHP 5.0.0以降 |
| カテゴリ | ストリーム関数 |
| 返り値 | resource(成功時)、false(失敗・タイムアウト時) |
構文
stream_socket_accept(
resource $socket,
?float $timeout = null,
string &$peer_name = ''
): resource|false
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$socket | resource | stream_socket_server で作成したサーバーソケット |
$timeout | ?float | タイムアウト秒数。null でデフォルト(default_socket_timeout ini値)、0 で即時返却 |
&$peer_name | string | 接続してきたクライアントのアドレスが格納される(例:127.0.0.1:54321) |
返り値
| 値 | 意味 |
|---|---|
resource | 接続済みのクライアントストリーム |
false | タイムアウト、またはエラー |
サーバー/クライアントの関係
【stream_socket_server と stream_socket_accept の役割分担】
サーバー側 クライアント側
─────────────────────────────────────────────────────
stream_socket_server('tcp://0.0.0.0:8080')
└── リスニングソケット作成(待ち受け開始)
stream_socket_accept($server) stream_socket_client('tcp://127.0.0.1:8080')
└── 接続を受け入れ └── サーバーに接続
↓
クライアントとの通信ストリームを返す
↓
fread / fwrite で送受信
基本的な使い方
<?php
// サーバーソケットを作成
$server = stream_socket_server('tcp://0.0.0.0:8080', $errno, $errstr);
if (!$server) {
die("サーバー起動失敗 [{$errno}]: {$errstr}");
}
echo "8080番ポートで待ち受け中..." . PHP_EOL;
// クライアントの接続を受け付ける(タイムアウト30秒)
$client = stream_socket_accept($server, 30, $peerName);
if ($client === false) {
echo "タイムアウトまたは接続エラー" . PHP_EOL;
} else {
echo "接続元: {$peerName}" . PHP_EOL;
fwrite($client, "接続しました!\n");
$data = fread($client, 1024);
echo "受信: {$data}";
fclose($client);
}
fclose($server);
実践例(クラスを使った実装)
例1:シンプルなエコーサーバー
受け取ったデータをそのまま返す、最もシンプルなTCPサーバーです。
<?php
class EchoServer
{
private $server;
private string $address;
private bool $running = false;
public function __construct(string $host = '127.0.0.1', int $port = 8080)
{
$this->address = "tcp://{$host}:{$port}";
$this->server = stream_socket_server(
$this->address,
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN
);
if (!$this->server) {
throw new RuntimeException("サーバー起動失敗 [{$errno}]: {$errstr}");
}
echo "エコーサーバー起動: {$this->address}" . PHP_EOL;
}
public function run(int $maxConnections = 5): void
{
$this->running = true;
$handled = 0;
while ($this->running && $handled < $maxConnections) {
// タイムアウト5秒でクライアントを待つ
$client = stream_socket_accept($this->server, 5, $peerName);
if ($client === false) {
echo "タイムアウト(次の接続を待機中)" . PHP_EOL;
continue;
}
echo "[{$peerName}] 接続" . PHP_EOL;
$this->handleClient($client, $peerName);
$handled++;
}
}
private function handleClient(resource $client, string $peerName): void
{
stream_set_timeout($client, 5);
while (!feof($client)) {
$data = fread($client, 4096);
$meta = stream_get_meta_data($client);
if ($meta['timed_out']) {
echo "[{$peerName}] タイムアウト" . PHP_EOL;
break;
}
if ($data === false || $data === '') {
break;
}
echo "[{$peerName}] 受信: " . trim($data) . PHP_EOL;
// そのままエコーバック
fwrite($client, $data);
}
echo "[{$peerName}] 切断" . PHP_EOL;
fclose($client);
}
public function stop(): void
{
$this->running = false;
fclose($this->server);
}
}
// ※ このサーバーは実際に起動すると待ち受け状態になります
// $server = new EchoServer('127.0.0.1', 8080);
// $server->run(maxConnections: 3);
echo "EchoServer クラス定義完了(実行するにはコメントを外してください)" . PHP_EOL;
例2:HTTPレスポンスを返す最小限のHTTPサーバー
stream_socket_accept を使って、ブラウザからアクセス可能な簡易HTTPサーバーを実装します。
<?php
class MinimalHttpServer
{
private $server;
private array $routes = [];
private int $port;
public function __construct(int $port = 8080)
{
$this->port = $port;
$this->server = stream_socket_server(
"tcp://0.0.0.0:{$port}",
$errno,
$errstr
);
if (!$this->server) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
}
public function get(string $path, callable $handler): void
{
$this->routes['GET'][$path] = $handler;
}
public function run(int $maxRequests = 10): void
{
echo "HTTP サーバー起動: http://127.0.0.1:{$this->port}/" . PHP_EOL;
for ($i = 0; $i < $maxRequests; $i++) {
$client = stream_socket_accept($this->server, 30, $peerName);
if ($client === false) {
continue;
}
stream_set_timeout($client, 5);
$this->handleRequest($client, $peerName);
}
fclose($this->server);
}
private function handleRequest(resource $client, string $peerName): void
{
$rawRequest = '';
while (!feof($client)) {
$rawRequest .= fread($client, 4096);
// ヘッダー終端(空行)を検知
if (str_contains($rawRequest, "\r\n\r\n")) {
break;
}
$meta = stream_get_meta_data($client);
if ($meta['timed_out']) {
break;
}
}
[$method, $path] = $this->parseRequestLine($rawRequest);
echo "[{$peerName}] {$method} {$path}" . PHP_EOL;
$handler = $this->routes[$method][$path] ?? null;
if ($handler) {
[$statusCode, $body] = $handler($path);
} else {
$statusCode = 404;
$body = "<h1>404 Not Found</h1><p>{$path} は存在しません</p>";
}
$response = $this->buildResponse($statusCode, $body);
fwrite($client, $response);
fclose($client);
}
private function parseRequestLine(string $raw): array
{
$lines = explode("\r\n", $raw);
$parts = explode(' ', $lines[0] ?? '');
return [$parts[0] ?? 'GET', $parts[1] ?? '/'];
}
private function buildResponse(int $status, string $body): string
{
$statusTexts = [200 => 'OK', 404 => 'Not Found', 500 => 'Internal Server Error'];
$text = $statusTexts[$status] ?? 'Unknown';
$length = strlen($body);
return implode("\r\n", [
"HTTP/1.1 {$status} {$text}",
"Content-Type: text/html; charset=UTF-8",
"Content-Length: {$length}",
"Connection: close",
"",
$body,
]);
}
}
// ルート定義
$http = new MinimalHttpServer(8080);
$http->get('/', fn($path) => [200, '<h1>トップページ</h1><p>PHPサーバーへようこそ!</p>']);
$http->get('/hello', fn($path) => [200, '<h1>Hello, World!</h1>']);
$http->get('/time', fn($path) => [200, '<p>現在時刻: ' . date('Y-m-d H:i:s') . '</p>']);
// $http->run(); // 実際に起動する場合
echo "MinimalHttpServer クラス定義完了" . PHP_EOL;
echo "起動すると http://127.0.0.1:8080/ でアクセス可能" . PHP_EOL;
例3:タイムアウトとリトライを管理するアクセプターラッパー
stream_socket_accept のタイムアウト挙動をラップして、堅牢な接続受け付けを実現します。
<?php
class RobustSocketAcceptor
{
private $server;
private float $acceptTimeout;
private int $maxTimeouts;
private array $acceptLog = [];
public function __construct(
resource $server,
float $acceptTimeout = 5.0,
int $maxTimeouts = 3
) {
$this->server = $server;
$this->acceptTimeout = $acceptTimeout;
$this->maxTimeouts = $maxTimeouts;
}
/**
* 接続を待ち受け、タイムアウトが続いた場合は null を返す
*/
public function accept(): ?array
{
$timeoutCount = 0;
while ($timeoutCount < $this->maxTimeouts) {
$startTime = microtime(true);
$peerName = '';
$client = stream_socket_accept(
$this->server,
$this->acceptTimeout,
$peerName
);
$elapsed = microtime(true) - $startTime;
if ($client !== false) {
$this->acceptLog[] = [
'result' => 'accepted',
'peer' => $peerName,
'elapsed_ms' => round($elapsed * 1000, 2),
'time' => date('H:i:s'),
];
return ['stream' => $client, 'peer' => $peerName];
}
// false = タイムアウトまたはエラー
$timeoutCount++;
$this->acceptLog[] = [
'result' => 'timeout',
'peer' => '',
'elapsed_ms' => round($elapsed * 1000, 2),
'time' => date('H:i:s'),
];
echo "タイムアウト {$timeoutCount}/{$this->maxTimeouts}回目({$this->acceptTimeout}秒待機)" . PHP_EOL;
}
return null; // 最大タイムアウト到達
}
public function printLog(): void
{
echo "=== アクセプトログ ===" . PHP_EOL;
foreach ($this->acceptLog as $i => $entry) {
$icon = $entry['result'] === 'accepted' ? '✓' : '⏱';
$peer = $entry['peer'] ? " from {$entry['peer']}" : '';
echo " [{$entry['time']}] {$icon} {$entry['result']}{$peer} ({$entry['elapsed_ms']}ms)" . PHP_EOL;
}
}
}
// 使用例(デモ用:即時タイムアウトで動作確認)
$server = stream_socket_server('tcp://127.0.0.1:18080', $errno, $errstr);
if ($server) {
$acceptor = new RobustSocketAcceptor(
server: $server,
acceptTimeout: 0.1, // 100msで即タイムアウト(デモ用)
maxTimeouts: 3
);
$result = $acceptor->accept();
if ($result === null) {
echo "接続なし(最大タイムアウト到達)" . PHP_EOL;
} else {
echo "接続受け付け: {$result['peer']}" . PHP_EOL;
fclose($result['stream']);
}
$acceptor->printLog();
fclose($server);
}
出力例:
タイムアウト 1/3回目(0.1秒待機)
タイムアウト 2/3回目(0.1秒待機)
タイムアウト 3/3回目(0.1秒待機)
接続なし(最大タイムアウト到達)
=== アクセプトログ ===
[12:00:01] ⏱ timeout (100.12ms)
[12:00:01] ⏱ timeout (100.08ms)
[12:00:01] ⏱ timeout (100.11ms)
例4:stream_select と組み合わせたノンブロッキングサーバー
stream_select でリスニングソケットを監視し、接続があったときだけ stream_socket_accept を呼ぶことでブロッキングを最小化します。
<?php
class NonBlockingServer
{
private $server;
private array $clients = [];
private array $peerNames = [];
private int $maxClients;
public function __construct(string $address, int $maxClients = 10)
{
$this->maxClients = $maxClients;
$this->server = stream_socket_server($address, $errno, $errstr);
if (!$this->server) {
throw new RuntimeException("起動失敗 [{$errno}]: {$errstr}");
}
// サーバーソケットをノンブロッキングに設定
stream_set_blocking($this->server, false);
echo "ノンブロッキングサーバー起動: {$address}" . PHP_EOL;
}
public function tick(): void
{
// 監視するソケット一覧(サーバー + 全クライアント)
$readSockets = array_merge([$this->server], $this->clients);
$write = null;
$except = null;
// 変化があるまで最大100ms待機
$changed = stream_select($readSockets, $write, $except, 0, 100_000);
if ($changed === false || $changed === 0) {
return;
}
foreach ($readSockets as $sock) {
if ($sock === $this->server) {
// 新規接続
$this->acceptNew();
} else {
// 既存クライアントからデータ
$this->handleClientData($sock);
}
}
}
private function acceptNew(): void
{
if (count($this->clients) >= $this->maxClients) {
// 上限に達しているので即時accept→拒否
$client = stream_socket_accept($this->server, 0);
if ($client) {
fwrite($client, "サーバーが満員です\n");
fclose($client);
}
return;
}
$client = stream_socket_accept($this->server, 0, $peerName);
if ($client !== false) {
stream_set_blocking($client, false);
$id = (int) $client;
$this->clients[$id] = $client;
$this->peerNames[$id] = $peerName;
echo "[接続] {$peerName} (ID:{$id}, 合計:" . count($this->clients) . ")" . PHP_EOL;
}
}
private function handleClientData(resource $sock): void
{
$id = (int) $sock;
$peer = $this->peerNames[$id] ?? 'unknown';
$data = fread($sock, 4096);
if ($data === false || $data === '' || feof($sock)) {
echo "[切断] {$peer}" . PHP_EOL;
fclose($sock);
unset($this->clients[$id], $this->peerNames[$id]);
return;
}
echo "[受信] {$peer}: " . trim($data) . PHP_EOL;
// 全クライアントにブロードキャスト
foreach ($this->clients as $cid => $client) {
fwrite($client, "[{$peer}]: {$data}");
}
}
public function getClientCount(): int
{
return count($this->clients);
}
public function shutdown(): void
{
foreach ($this->clients as $client) {
fclose($client);
}
fclose($this->server);
echo "サーバーシャットダウン" . PHP_EOL;
}
}
// 使用例(10回tickして終了するデモ)
$srv = new NonBlockingServer('tcp://127.0.0.1:18081');
for ($i = 0; $i < 10; $i++) {
$srv->tick();
echo "tick {$i} 完了(接続数: {$srv->getClientCount()})" . PHP_EOL;
}
$srv->shutdown();
出力例(接続がない場合):
ノンブロッキングサーバー起動: tcp://127.0.0.1:18081
tick 0 完了(接続数: 0)
tick 1 完了(接続数: 0)
...
tick 9 完了(接続数: 0)
サーバーシャットダウン
例5:TLSサーバー ─ SSL/TLSを使った暗号化接続を受け付ける
stream_socket_server に ssl:// スキームを指定し、暗号化された接続を受け付けます。
<?php
class TlsServer
{
private $server;
private string $certFile;
private string $keyFile;
public function __construct(
string $host,
int $port,
string $certFile,
string $keyFile
) {
$this->certFile = $certFile;
$this->keyFile = $keyFile;
$context = stream_context_create([
'ssl' => [
'local_cert' => $certFile,
'local_pk' => $keyFile,
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
]);
$this->server = stream_socket_server(
"ssl://{$host}:{$port}",
$errno,
$errstr,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$context
);
if (!$this->server) {
throw new RuntimeException("TLSサーバー起動失敗 [{$errno}]: {$errstr}");
}
echo "TLSサーバー起動: ssl://{$host}:{$port}" . PHP_EOL;
}
public function acceptOne(float $timeout = 30.0): void
{
$client = stream_socket_accept($this->server, $timeout, $peerName);
if ($client === false) {
echo "タイムアウト" . PHP_EOL;
return;
}
echo "TLS接続受け付け: {$peerName}" . PHP_EOL;
// TLSネゴシエーション情報を取得
$meta = stream_get_meta_data($client);
$crypto = isset($meta['crypto']) ? $meta['crypto'] : [];
if (!empty($crypto)) {
echo "暗号化プロトコル: " . ($crypto['protocol'] ?? 'unknown') . PHP_EOL;
echo "暗号スイート : " . ($crypto['cipher_name'] ?? 'unknown') . PHP_EOL;
}
fwrite($client, "TLS接続を受け付けました\r\n");
fclose($client);
}
public function close(): void
{
if (is_resource($this->server)) {
fclose($this->server);
}
}
}
// 証明書ファイルが必要なため、ここではクラス定義のみ
// 実際の使用例:
// $tls = new TlsServer('0.0.0.0', 8443, '/path/to/cert.pem', '/path/to/key.pem');
// $tls->acceptOne(30.0);
// $tls->close();
echo "TlsServer クラス定義完了(証明書ファイルを用意して使用してください)" . PHP_EOL;
例6:接続情報を詳細に記録するコネクションロガー
接続元IP・ポート・接続時刻・通信量をすべて記録し、サーバーの通信を可視化します。
<?php
class ConnectionLogger
{
private array $connections = [];
private int $totalAccepted = 0;
/**
* stream_socket_accept の結果を記録してラップする
*/
public function accept(resource $server, float $timeout = 5.0): ?array
{
$peerName = '';
$client = stream_socket_accept($server, $timeout, $peerName);
if ($client === false) {
return null;
}
$this->totalAccepted++;
$id = $this->totalAccepted;
// ローカル側のアドレスも取得
$localName = stream_socket_get_name($client, false);
$connInfo = [
'id' => $id,
'peer' => $peerName,
'local' => $localName,
'connected_at' => microtime(true),
'bytes_read' => 0,
'bytes_written'=> 0,
'closed_at' => null,
];
$this->connections[$id] = &$connInfo;
echo "[{$connInfo['id']}] 接続: {$peerName} → {$localName}" . PHP_EOL;
return ['stream' => $client, 'id' => $id, 'info' => &$connInfo];
}
public function recordRead(int $id, int $bytes): void
{
if (isset($this->connections[$id])) {
$this->connections[$id]['bytes_read'] += $bytes;
}
}
public function recordWrite(int $id, int $bytes): void
{
if (isset($this->connections[$id])) {
$this->connections[$id]['bytes_written'] += $bytes;
}
}
public function closeConnection(int $id, resource $client): void
{
if (isset($this->connections[$id])) {
$this->connections[$id]['closed_at'] = microtime(true);
}
fclose($client);
}
public function printReport(): void
{
echo PHP_EOL . "=== 接続レポート ===" . PHP_EOL;
echo str_pad("ID", 5)
. str_pad("接続元", 24)
. str_pad("受信", 12)
. str_pad("送信", 12)
. "接続時間" . PHP_EOL;
echo str_repeat('-', 62) . PHP_EOL;
foreach ($this->connections as $conn) {
$duration = $conn['closed_at']
? round(($conn['closed_at'] - $conn['connected_at']) * 1000, 1) . 'ms'
: '接続中';
echo str_pad($conn['id'], 5)
. str_pad($conn['peer'], 24)
. str_pad($conn['bytes_read'] . 'B', 12)
. str_pad($conn['bytes_written'] . 'B', 12)
. $duration . PHP_EOL;
}
echo PHP_EOL . "合計接続数: {$this->totalAccepted}" . PHP_EOL;
}
}
// 使用例(ループで複数接続をシミュレート)
$server = stream_socket_server('tcp://127.0.0.1:18082', $errno, $errstr);
if ($server) {
$logger = new ConnectionLogger();
// 自前でクライアント接続をシミュレート(fork不使用)
$testClients = [];
for ($i = 0; $i < 3; $i++) {
$tc = stream_socket_client('tcp://127.0.0.1:18082', $e, $es, 1);
if ($tc) {
$testClients[] = $tc;
}
}
// 接続を受け付けてロギング
for ($i = 0; $i < count($testClients); $i++) {
$conn = $logger->accept($server, 0.5);
if ($conn) {
$bytes = fwrite($conn['stream'], "Hello Client!\n");
$logger->recordWrite($conn['id'], $bytes ?: 0);
$data = fread($conn['stream'], 256);
$logger->recordRead($conn['id'], strlen($data ?: ''));
$logger->closeConnection($conn['id'], $conn['stream']);
}
}
foreach ($testClients as $tc) {
fclose($tc);
}
fclose($server);
$logger->printReport();
}
出力例:
[1] 接続: 127.0.0.1:52341 → 127.0.0.1:18082
[2] 接続: 127.0.0.1:52342 → 127.0.0.1:18082
[3] 接続: 127.0.0.1:52343 → 127.0.0.1:18082
=== 接続レポート ===
ID 接続元 受信 送信 接続時間
--------------------------------------------------------------
1 127.0.0.1:52341 0B 14B 1.2ms
2 127.0.0.1:52342 0B 14B 0.9ms
3 127.0.0.1:52343 0B 14B 1.1ms
合計接続数: 3
関連する関数との比較
| 関数 | 役割 |
|---|---|
stream_socket_server | リスニングソケットを作成する |
stream_socket_accept | クライアントの接続を受け付ける |
stream_socket_client | サーバーに接続する(クライアント側) |
stream_socket_get_name | ソケットのローカル/リモートアドレスを取得 |
stream_socket_sendto | 接続なしでデータを送信(UDP向け) |
stream_select | 複数ソケットを効率よく監視する |
stream_socket_accept vs socket_accept
// stream_socket_accept: ストリームAPI(fread/fwrite で扱える)
$server = stream_socket_server('tcp://0.0.0.0:8080');
$client = stream_socket_accept($server, 30);
fwrite($client, "Hello\n"); // fwrite が使える
// socket_accept: ソケット拡張API(socket_read/socket_write が必要)
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '0.0.0.0', 8080);
socket_listen($server);
$client = socket_accept($server);
socket_write($client, "Hello\n"); // socket_write が必要
| 観点 | stream_socket_accept | socket_accept |
|---|---|---|
| 所属 | ストリーム関数 | ソケット拡張 |
| 読み書き | fread / fwrite | socket_read / socket_write |
| SSL/TLS | コンテキストで対応可 | 別途設定が必要 |
| 推奨度 | ◎(モダンなPHPで推奨) | △(低レベル操作が必要な場合) |
よくある注意点・落とし穴
1. $timeout = 0 は「即時返却」を意味する
0 を指定すると接続がなければ即座に false を返します。stream_select と組み合わせるノンブロッキングサーバーで使います。
// NG:0 を「無限待機」と勘違いしてしまう
$client = stream_socket_accept($server, 0); // 即座に false が返る場合あり
// 無限待機したい場合は null または -1
$client = stream_socket_accept($server, -1); // 接続が来るまで無限に待つ
2. クライアントストリームも必ず閉じる
fclose しないとファイルディスクリプタが枯渇します。
$client = stream_socket_accept($server, 30);
if ($client) {
// 処理...
fclose($client); // 必須
}
3. サーバーソケットとクライアントソケットの区別
stream_socket_accept が返すのはクライアントとの通信用ストリームです。サーバーソケット(リスニングソケット)とは別物で、読み書きはクライアントストリームに対して行います。
$server = stream_socket_server('tcp://0.0.0.0:8080'); // リスニング用(fwriteしない)
$client = stream_socket_accept($server, 30); // 通信用(fwrite/freadはこちら)
fwrite($server, "NG"); // サーバーソケットに書いても届かない
fwrite($client, "OK"); // クライアントに正しく届く
4. UDPでは使えない
stream_socket_accept はTCP(接続型)専用です。UDPのデータ受信には stream_socket_recvfrom を使います。
// TCP → stream_socket_accept が使える
$server = stream_socket_server('tcp://0.0.0.0:8080');
$client = stream_socket_accept($server, 30); // OK
// UDP → stream_socket_accept は使えない
$server = stream_socket_server('udp://0.0.0.0:8080');
$data = stream_socket_recvfrom($server, 1024, 0, $peer); // UDPはこちら
まとめ
| 項目 | 内容 |
|---|---|
| 関数名 | stream_socket_accept(resource $socket, ?float $timeout, string &$peer_name): resource|false |
| 主な用途 | TCPサーバーでクライアント接続を受け付ける |
| セットで使う関数 | stream_socket_server、stream_select、stream_socket_get_name |
$timeout = 0 | 即時返却(接続なければ false) |
$timeout = -1 | 接続まで無限待機 |
| UDP対応 | 不可(UDP は stream_socket_recvfrom を使う) |
| PHP バージョン | PHP 5.0.0 以上 |
stream_socket_accept は、PHPでTCPサーバーを実装する際の核となる関数です。stream_socket_server と組み合わせてリスニングを行い、stream_select を加えることでノンブロッキングな多重接続サーバーへと発展させられます。
SSL/TLSもコンテキスト設定だけで対応でき、fread / fwrite という馴染みのあるAPIで通信できる点が、低レベルなソケット拡張に比べた大きなメリットです。ぜひ実際のサーバー実装で活用してみてください。
