PHPで外部APIとの通信やネットワークプログラミングを行う際、効率的な接続管理が重要になります。この記事では、PHPのpfsockopen関数について、fsockopenとの違いや実践的な使用方法を詳しく解説していきます。
pfsockopen関数とは?
pfsockopenは、持続的(Persistent)なソケット接続を開くための関数です。fsockopenと似ていますが、接続を再利用できる点が大きな違いです。同じサーバーに繰り返しアクセスする場合、接続のオーバーヘッドを削減できます。
基本的な構文
resource|false pfsockopen(
string $hostname,
int $port = -1,
int &$error_code = null,
string &$error_message = null,
float $timeout = null
)
パラメータ:
$hostname: 接続先のホスト名またはIPアドレス$port: ポート番号$error_code: エラーコードを格納する変数(参照渡し)$error_message: エラーメッセージを格納する変数(参照渡し)$timeout: 接続タイムアウト(秒単位)
戻り値:
- 成功時: ファイルポインタリソース
- 失敗時:
false
fsockopenとpfsockopenの違い
fsockopen(通常の接続)
<?php
// 毎回新しい接続を作成
$fp = fsockopen('www.example.com', 80, $errno, $errstr, 30);
// 処理後、接続は完全に閉じられる
fclose($fp);
?>
pfsockopen(持続的な接続)
<?php
// 接続をプールに保持
$fp = pfsockopen('www.example.com', 80, $errno, $errstr, 30);
// 処理後、fclose()しても接続は保持される
fclose($fp);
// 次回同じホストへの接続時、既存の接続を再利用
$fp2 = pfsockopen('www.example.com', 80, $errno, $errstr, 30);
?>
主な違いのまとめ
| 項目 | fsockopen | pfsockopen |
|---|---|---|
| 接続の再利用 | なし | あり |
| 接続時間 | 毎回かかる | 初回のみ |
| メモリ使用量 | 低い | やや高い |
| 適した用途 | 単発の接続 | 繰り返しの接続 |
| 接続の寿命 | スクリプト終了まで | プロセス終了まで |
pfsockopenを使うべき場面
適している場面
- 同じAPIサーバーに頻繁にリクエストを送る
- 定期的なヘルスチェックや監視
- バッチ処理で同じサーバーと通信
- 高頻度のデータ同期
適していない場面
- 一度だけの接続(fsockopenで十分)
- 多数の異なるサーバーへの接続
- 共有ホスティング環境(接続プールの制御が困難)
基本的な使い方
シンプルなHTTPリクエスト
<?php
function sendHttpRequest($host, $path = '/') {
$errno = 0;
$errstr = '';
// 持続的な接続を開く
$fp = pfsockopen($host, 80, $errno, $errstr, 30);
if (!$fp) {
return [
'success' => false,
'error' => "接続エラー: [{$errno}] {$errstr}"
];
}
// HTTPリクエストを送信
$request = "GET {$path} HTTP/1.1\r\n";
$request .= "Host: {$host}\r\n";
$request .= "Connection: Close\r\n\r\n";
fwrite($fp, $request);
// レスポンスを読み取る
$response = '';
while (!feof($fp)) {
$response .= fgets($fp, 128);
}
fclose($fp);
return [
'success' => true,
'response' => $response
];
}
// 使用例
$result = sendHttpRequest('www.example.com', '/api/status');
if ($result['success']) {
echo $result['response'];
}
?>
実践的な使用例
1. API接続の効率化
<?php
class PersistentApiClient {
private $host;
private $port;
private $timeout;
public function __construct($host, $port = 443, $timeout = 30) {
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
}
public function request($method, $path, $data = null) {
$errno = 0;
$errstr = '';
// SSL接続の場合
$hostname = ($this->port == 443) ? "ssl://{$this->host}" : $this->host;
$fp = pfsockopen($hostname, $this->port, $errno, $errstr, $this->timeout);
if (!$fp) {
throw new Exception("接続失敗: [{$errno}] {$errstr}");
}
// タイムアウト設定
stream_set_timeout($fp, $this->timeout);
// リクエストヘッダーを構築
$headers = "{$method} {$path} HTTP/1.1\r\n";
$headers .= "Host: {$this->host}\r\n";
$headers .= "User-Agent: PHP-PersistentClient/1.0\r\n";
if ($data !== null) {
$body = json_encode($data);
$headers .= "Content-Type: application/json\r\n";
$headers .= "Content-Length: " . strlen($body) . "\r\n";
}
$headers .= "Connection: keep-alive\r\n\r\n";
// リクエスト送信
fwrite($fp, $headers);
if ($data !== null) {
fwrite($fp, $body);
}
// レスポンス読み取り
$response = '';
while (!feof($fp)) {
$response .= fgets($fp, 1024);
}
fclose($fp);
return $this->parseResponse($response);
}
private function parseResponse($response) {
// ヘッダーとボディを分離
list($headers, $body) = explode("\r\n\r\n", $response, 2);
// ステータスコードを取得
preg_match('/HTTP\/\d\.\d\s+(\d+)/', $headers, $matches);
$statusCode = isset($matches[1]) ? (int)$matches[1] : 0;
return [
'status_code' => $statusCode,
'headers' => $headers,
'body' => $body
];
}
public function get($path) {
return $this->request('GET', $path);
}
public function post($path, $data) {
return $this->request('POST', $path, $data);
}
}
// 使用例: 複数のAPIリクエストを効率的に実行
$client = new PersistentApiClient('api.example.com', 443);
// 1回目のリクエスト(新しい接続を作成)
$response1 = $client->get('/users/123');
echo "ユーザー情報: " . $response1['body'] . "\n";
// 2回目のリクエスト(既存の接続を再利用)
$response2 = $client->post('/users/123/update', ['name' => 'John Doe']);
echo "更新結果: " . $response2['body'] . "\n";
?>
2. メールサーバーとの通信(SMTP)
<?php
class SmtpClient {
private $host;
private $port;
private $socket;
public function __construct($host, $port = 25) {
$this->host = $host;
$this->port = $port;
}
public function connect() {
$errno = 0;
$errstr = '';
$this->socket = pfsockopen($this->host, $this->port, $errno, $errstr, 30);
if (!$this->socket) {
throw new Exception("SMTP接続失敗: {$errstr}");
}
// サーバーの応答を読み取る
$response = fgets($this->socket, 512);
if (substr($response, 0, 3) != '220') {
throw new Exception("SMTPサーバーエラー: {$response}");
}
return true;
}
public function sendCommand($command, $expectedCode = '250') {
fwrite($this->socket, $command . "\r\n");
$response = fgets($this->socket, 512);
if (substr($response, 0, 3) != $expectedCode) {
throw new Exception("コマンドエラー: {$response}");
}
return $response;
}
public function sendMail($from, $to, $subject, $message) {
$this->connect();
// HELOコマンド
$this->sendCommand("HELO {$this->host}");
// MAIL FROMコマンド
$this->sendCommand("MAIL FROM:<{$from}>");
// RCPT TOコマンド
$this->sendCommand("RCPT TO:<{$to}>");
// DATAコマンド
$this->sendCommand("DATA", '354');
// メールヘッダーとボディ
$email = "From: {$from}\r\n";
$email .= "To: {$to}\r\n";
$email .= "Subject: {$subject}\r\n";
$email .= "\r\n";
$email .= $message . "\r\n";
$email .= ".\r\n";
fwrite($this->socket, $email);
$response = fgets($this->socket, 512);
// QUITコマンド
$this->sendCommand("QUIT", '221');
fclose($this->socket);
return true;
}
}
// 使用例
try {
$smtp = new SmtpClient('mail.example.com', 25);
$smtp->sendMail(
'sender@example.com',
'recipient@example.com',
'テストメール',
'これはテストメッセージです。'
);
echo "メール送信成功\n";
} catch (Exception $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
3. サーバー監視ツール
<?php
class ServerMonitor {
private $servers = [];
public function addServer($name, $host, $port) {
$this->servers[] = [
'name' => $name,
'host' => $host,
'port' => $port
];
}
public function checkAll() {
$results = [];
foreach ($this->servers as $server) {
$results[] = $this->checkServer($server);
}
return $results;
}
private function checkServer($server) {
$startTime = microtime(true);
$errno = 0;
$errstr = '';
$fp = pfsockopen($server['host'], $server['port'], $errno, $errstr, 5);
$endTime = microtime(true);
$responseTime = round(($endTime - $startTime) * 1000, 2);
if (!$fp) {
return [
'name' => $server['name'],
'host' => $server['host'],
'port' => $server['port'],
'status' => 'down',
'error' => $errstr,
'response_time' => null
];
}
fclose($fp);
return [
'name' => $server['name'],
'host' => $server['host'],
'port' => $server['port'],
'status' => 'up',
'error' => null,
'response_time' => $responseTime . ' ms'
];
}
}
// 使用例
$monitor = new ServerMonitor();
$monitor->addServer('Webサーバー', 'www.example.com', 80);
$monitor->addServer('DBサーバー', 'db.example.com', 3306);
$monitor->addServer('メールサーバー', 'mail.example.com', 25);
$results = $monitor->checkAll();
foreach ($results as $result) {
echo "サーバー: {$result['name']}\n";
echo "ステータス: {$result['status']}\n";
if ($result['status'] == 'up') {
echo "応答時間: {$result['response_time']}\n";
} else {
echo "エラー: {$result['error']}\n";
}
echo "---\n";
}
?>
4. プロキシサーバー経由の接続
<?php
function connectViaProxy($targetHost, $targetPort, $proxyHost, $proxyPort) {
$errno = 0;
$errstr = '';
// プロキシサーバーに接続
$fp = pfsockopen($proxyHost, $proxyPort, $errno, $errstr, 30);
if (!$fp) {
return ['success' => false, 'error' => "プロキシ接続失敗: {$errstr}"];
}
// CONNECTメソッドでトンネリング
$request = "CONNECT {$targetHost}:{$targetPort} HTTP/1.1\r\n";
$request .= "Host: {$targetHost}:{$targetPort}\r\n";
$request .= "Proxy-Connection: Keep-Alive\r\n\r\n";
fwrite($fp, $request);
// プロキシからの応答を確認
$response = fgets($fp, 128);
if (strpos($response, '200') === false) {
fclose($fp);
return ['success' => false, 'error' => "プロキシエラー: {$response}"];
}
// ヘッダーを読み飛ばす
while (trim(fgets($fp, 128)) !== '') {}
// ターゲットサーバーにリクエスト送信
$request = "GET / HTTP/1.1\r\n";
$request .= "Host: {$targetHost}\r\n";
$request .= "Connection: close\r\n\r\n";
fwrite($fp, $request);
$content = '';
while (!feof($fp)) {
$content .= fgets($fp, 1024);
}
fclose($fp);
return ['success' => true, 'content' => $content];
}
// 使用例
$result = connectViaProxy(
'www.example.com', 80,
'proxy.company.com', 8080
);
if ($result['success']) {
echo "接続成功\n";
} else {
echo "エラー: {$result['error']}\n";
}
?>
5. WebSocketライクな双方向通信
<?php
class PersistentConnection {
private $host;
private $port;
private $socket;
public function __construct($host, $port) {
$this->host = $host;
$this->port = $port;
}
public function connect() {
$errno = 0;
$errstr = '';
$this->socket = pfsockopen($this->host, $this->port, $errno, $errstr, 30);
if (!$this->socket) {
throw new Exception("接続失敗: {$errstr}");
}
// ノンブロッキングモードに設定
stream_set_blocking($this->socket, false);
return true;
}
public function send($data) {
if (!$this->socket) {
throw new Exception("接続が確立されていません");
}
$written = fwrite($this->socket, $data . "\n");
return $written !== false;
}
public function receive($timeout = 5) {
if (!$this->socket) {
throw new Exception("接続が確立されていません");
}
$startTime = time();
$data = '';
while ((time() - $startTime) < $timeout) {
$line = fgets($this->socket);
if ($line !== false) {
$data .= $line;
if (trim($line) === '') {
break;
}
}
usleep(100000); // 0.1秒待機
}
return $data;
}
public function close() {
if ($this->socket) {
fclose($this->socket);
$this->socket = null;
}
}
}
// 使用例
try {
$conn = new PersistentConnection('chat.example.com', 8080);
$conn->connect();
// メッセージ送信
$conn->send('Hello, Server!');
// レスポンス受信
$response = $conn->receive();
echo "サーバーからの応答: {$response}\n";
$conn->close();
} catch (Exception $e) {
echo "エラー: " . $e->getMessage() . "\n";
}
?>
よくある間違いと注意点
間違い1: 接続プールの理解不足
<?php
// ❌ 誤解: fclose()で接続が完全に閉じられると思っている
$fp = pfsockopen('example.com', 80, $errno, $errstr, 30);
fwrite($fp, $request);
$response = fgets($fp);
fclose($fp); // 実際には接続はプールに戻される
// ✅ 正しい理解: 接続は再利用可能な状態で保持される
// 次回の pfsockopen() で同じ接続が使われる可能性がある
?>
間違い2: エラーハンドリングの不備
<?php
// ❌ 悪い例
$fp = pfsockopen('example.com', 80);
fwrite($fp, $data);
// ✅ 正しい例
$errno = 0;
$errstr = '';
$fp = pfsockopen('example.com', 80, $errno, $errstr, 30);
if (!$fp) {
error_log("接続エラー: [{$errno}] {$errstr}");
die("サーバーに接続できません");
}
// タイムアウト設定
stream_set_timeout($fp, 10);
if (fwrite($fp, $data) === false) {
error_log("書き込みエラー");
}
fclose($fp);
?>
間違い3: SSL/TLS接続の設定ミス
<?php
// ❌ SSL接続なのにプロトコル指定なし
$fp = pfsockopen('secure.example.com', 443, $errno, $errstr, 30);
// ✅ 正しいSSL接続
$fp = pfsockopen('ssl://secure.example.com', 443, $errno, $errstr, 30);
// または TLS 1.2以降を指定
$fp = pfsockopen('tls://secure.example.com', 443, $errno, $errstr, 30);
?>
間違い4: リソースリーク
<?php
// ❌ 例外発生時に接続が閉じられない
function badRequest() {
$fp = pfsockopen('example.com', 80, $errno, $errstr, 30);
if (someCondition()) {
throw new Exception('エラー'); // fclose()が呼ばれない!
}
fclose($fp);
}
// ✅ try-finallyで確実に閉じる
function goodRequest() {
$fp = pfsockopen('example.com', 80, $errno, $errstr, 30);
if (!$fp) {
throw new Exception("接続失敗");
}
try {
if (someCondition()) {
throw new Exception('エラー');
}
// 通常の処理
} finally {
fclose($fp);
}
}
?>
パフォーマンスとベストプラクティス
接続の再利用を最大化する
<?php
class ConnectionPool {
private static $connections = [];
public static function getConnection($host, $port) {
$key = "{$host}:{$port}";
// 既存の接続を確認
if (isset(self::$connections[$key])) {
$fp = self::$connections[$key];
// 接続がまだ有効かチェック
if (is_resource($fp) && !feof($fp)) {
return $fp;
}
}
// 新しい接続を作成
$errno = 0;
$errstr = '';
$fp = pfsockopen($host, $port, $errno, $errstr, 30);
if ($fp) {
self::$connections[$key] = $fp;
}
return $fp;
}
}
?>
タイムアウトの適切な設定
<?php
$fp = pfsockopen('example.com', 80, $errno, $errstr, 5); // 接続タイムアウト: 5秒
if ($fp) {
// 読み書きタイムアウト: 10秒
stream_set_timeout($fp, 10);
// タイムアウト情報の取得
$info = stream_get_meta_data($fp);
if ($info['timed_out']) {
echo "タイムアウトが発生しました\n";
}
}
?>
まとめ
pfsockopen関数は、持続的なネットワーク接続を効率的に管理するための強力なツールです。以下のポイントを押さえておきましょう。
- 接続の再利用により、繰り返しの通信でパフォーマンスが向上
- 同じサーバーに頻繁にアクセスする場合に最適
fsockopenと構文は同じだが、接続の寿命が異なる- エラーハンドリングとタイムアウト設定を必ず実装
- SSL/TLS接続では
ssl://やtls://プロトコルを指定 - try-finallyで確実にリソースを解放
- 接続プールの動作を理解して使用する
適切に使用すれば、ネットワーク通信のパフォーマンスを大幅に改善できます。ただし、過度な使用や管理不足は逆効果になる可能性もあるため、用途に応じてfsockopenとの使い分けを検討しましょう!
