[PHP]pfsockopen関数の使い方を徹底解説!持続的なネットワーク接続の実装方法

PHP

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);
?>

主な違いのまとめ

項目fsockopenpfsockopen
接続の再利用なしあり
接続時間毎回かかる初回のみ
メモリ使用量低いやや高い
適した用途単発の接続繰り返しの接続
接続の寿命スクリプト終了までプロセス終了まで

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との使い分けを検討しましょう!

タイトルとURLをコピーしました