[PHP]stream_socket_clientで自在にTCP/UDP通信を実装する|ソケット接続からTLS対応まで実践ガイド

PHP

はじめに

PHPでHTTP通信といえば file_get_contents や cURLが一般的ですが、より低レベルな通信制御が必要な場面では ソケット接続 が必要になります。独自プロトコルのサーバーへの接続、SMTPやFTPのような生のTCP通信、UDP送受信など、幅広い用途に対応できるのが stream_socket_client です。

stream_socket_client は指定したアドレスへのソケット接続を確立し、fread/fwrite で扱えるストリームリソースを返す関数です。fsockopen の後継として設計されており、非同期接続・UDPソケット・TLS暗号化といった高度な機能もシンプルなAPIで利用できます。

この記事では、基本的な使い方から実践的なクライアント実装まで、クラスを用いた具体例とともに丁寧に解説します。


stream_socket_client とは

項目内容
関数名stream_socket_client
PHPバージョンPHP 5.0.0以降
カテゴリストリーム関数
返り値resource(成功時)、false(失敗時)

構文

stream_socket_client(
    string   $address,
    int      &$error_code    = null,
    string   &$error_message = null,
    ?float   $timeout        = null,
    int      $flags          = STREAM_CLIENT_CONNECT,
    ?resource $context       = null
): resource|false

パラメータ

パラメータ説明
$addressstring接続先アドレス(例:tcp://example.com:80
&$error_codeint接続失敗時のエラーコードが格納される
&$error_messagestring接続失敗時のエラーメッセージが格納される
$timeout?float接続タイムアウト秒数。nulldefault_socket_timeout ini値
$flagsint接続フラグ(下表参照)
$context?resourceストリームコンテキスト(SSL設定など)

フラグ一覧

フラグ定数説明
STREAM_CLIENT_CONNECT4通常の接続(デフォルト)
STREAM_CLIENT_ASYNC_CONNECT2非同期接続(接続完了を待たない)
STREAM_CLIENT_PERSISTENT1持続的接続(keepalive)

返り値

意味
resource接続済みのストリーム
false接続失敗($error_code$error_message に詳細)

サポートされるアドレス形式

tcp://example.com:80         → TCP接続(ホスト名)
tcp://192.168.1.1:8080       → TCP接続(IPアドレス)
udp://example.com:53         → UDP接続
ssl://example.com:443        → SSL/TLS接続
tls://example.com:465        → TLS接続
unix:///var/run/app.sock     → UNIXドメインソケット

基本的な使い方

<?php
// TCPでHTTPサーバーに接続
$socket = stream_socket_client(
    'tcp://example.com:80',
    $errno,
    $errstr,
    5.0  // 接続タイムアウト5秒
);

if ($socket === false) {
    die("接続失敗 [{$errno}]: {$errstr}");
}

// 接続後のタイムアウト設定
stream_set_timeout($socket, 3);

// HTTPリクエスト送信
fwrite($socket, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");

// レスポンス受信
$response = '';
while (!feof($socket)) {
    $response .= fread($socket, 4096);
    $meta = stream_get_meta_data($socket);
    if ($meta['timed_out']) break;
}

fclose($socket);
echo "受信: " . strlen($response) . " バイト" . PHP_EOL;

実践例(クラスを使った実装)

例1:シンプルなTCPクライアント基底クラス

接続・送信・受信・エラーハンドリングを一通りカバーする基底クラスです。

<?php

class TcpClient
{
    private $socket   = null;
    private string $host;
    private int    $port;
    private float  $connectTimeout;
    private float  $readTimeout;

    public function __construct(
        string $host,
        int    $port,
        float  $connectTimeout = 5.0,
        float  $readTimeout    = 3.0
    ) {
        $this->host           = $host;
        $this->port           = $port;
        $this->connectTimeout = $connectTimeout;
        $this->readTimeout    = $readTimeout;
    }

    public function connect(): void
    {
        $this->socket = stream_socket_client(
            "tcp://{$this->host}:{$this->port}",
            $errno,
            $errstr,
            $this->connectTimeout,
            STREAM_CLIENT_CONNECT
        );

        if ($this->socket === false) {
            throw new RuntimeException(
                "接続失敗 [{$errno}]: {$errstr} ({$this->host}:{$this->port})"
            );
        }

        stream_set_timeout($this->socket, (int) $this->readTimeout,
            (int) (($this->readTimeout - floor($this->readTimeout)) * 1_000_000));
    }

    public function send(string $data): int
    {
        $this->assertConnected();
        $written = fwrite($this->socket, $data);

        if ($written === false) {
            throw new RuntimeException("送信失敗");
        }
        return $written;
    }

    public function receive(int $bufferSize = 4096): string
    {
        $this->assertConnected();
        $data = '';

        while (!feof($this->socket)) {
            $chunk = fread($this->socket, $bufferSize);
            $meta  = stream_get_meta_data($this->socket);

            if ($meta['timed_out']) {
                throw new RuntimeException("受信タイムアウト");
            }
            if ($chunk === false || $chunk === '') break;
            $data .= $chunk;
        }

        return $data;
    }

    public function receiveLine(): string
    {
        $this->assertConnected();
        $line = fgets($this->socket);
        $meta = stream_get_meta_data($this->socket);

        if ($meta['timed_out']) {
            throw new RuntimeException("受信タイムアウト");
        }

        return $line !== false ? rtrim($line) : '';
    }

    public function getLocalAddress(): string
    {
        $this->assertConnected();
        return stream_socket_get_name($this->socket, false) ?: '';
    }

    public function getPeerAddress(): string
    {
        $this->assertConnected();
        return stream_socket_get_name($this->socket, true) ?: '';
    }

    public function close(): void
    {
        if (is_resource($this->socket)) {
            fclose($this->socket);
            $this->socket = null;
        }
    }

    public function isConnected(): bool
    {
        return is_resource($this->socket);
    }

    private function assertConnected(): void
    {
        if (!$this->isConnected()) {
            throw new RuntimeException("未接続です。connect() を先に呼び出してください");
        }
    }
}

// 使用例
$client = new TcpClient('example.com', 80);

try {
    $client->connect();
    echo "ローカル: " . $client->getLocalAddress() . PHP_EOL;
    echo "接続先 : " . $client->getPeerAddress()  . PHP_EOL;

    $client->send("GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
    $statusLine = $client->receiveLine();
    echo "ステータス: {$statusLine}" . PHP_EOL;
} catch (RuntimeException $e) {
    echo "エラー: " . $e->getMessage() . PHP_EOL;
} finally {
    $client->close();
}

出力例:

ローカル: 192.168.1.10:54321
接続先 : 93.184.216.34:80
ステータス: HTTP/1.0 200 OK

例2:SSL/TLS対応HTTPSクライアント

ssl:// スキームと stream_context_create を組み合わせて、HTTPS通信を実装します。

<?php

class HttpsClient
{
    private string $host;
    private bool   $verifyPeer;
    private float  $timeout;

    public function __construct(
        string $host,
        bool   $verifyPeer = true,
        float  $timeout    = 10.0
    ) {
        $this->host       = $host;
        $this->verifyPeer = $verifyPeer;
        $this->timeout    = $timeout;
    }

    public function get(string $path = '/', array $headers = []): array
    {
        $context = stream_context_create([
            'ssl' => [
                'verify_peer'       => $this->verifyPeer,
                'verify_peer_name'  => $this->verifyPeer,
                'allow_self_signed' => !$this->verifyPeer,
                'SNI_enabled'       => true,
                'peer_name'         => $this->host,
            ],
        ]);

        $socket = stream_socket_client(
            "ssl://{$this->host}:443",
            $errno,
            $errstr,
            $this->timeout,
            STREAM_CLIENT_CONNECT,
            $context
        );

        if ($socket === false) {
            throw new RuntimeException("HTTPS接続失敗 [{$errno}]: {$errstr}");
        }

        stream_set_timeout($socket, (int) $this->timeout);

        // リクエスト構築
        $defaultHeaders = [
            "Host: {$this->host}",
            "User-Agent: PHP-HttpsClient/1.0",
            "Accept: */*",
            "Connection: close",
        ];
        $allHeaders = array_merge($defaultHeaders, $headers);
        $request    = "GET {$path} HTTP/1.1\r\n"
                    . implode("\r\n", $allHeaders)
                    . "\r\n\r\n";

        fwrite($socket, $request);

        // レスポンス受信
        $raw = '';
        while (!feof($socket)) {
            $chunk = fread($socket, 8192);
            $meta  = stream_get_meta_data($socket);
            if ($meta['timed_out']) break;
            if ($chunk !== false) $raw .= $chunk;
        }

        // TLS情報取得
        $meta   = stream_get_meta_data($socket);
        $crypto = $meta['crypto'] ?? [];

        fclose($socket);

        return $this->parseResponse($raw, $crypto);
    }

    private function parseResponse(string $raw, array $crypto): array
    {
        $parts     = explode("\r\n\r\n", $raw, 2);
        $headerStr = $parts[0] ?? '';
        $body      = $parts[1] ?? '';
        $lines     = explode("\r\n", $headerStr);
        $statusLine = array_shift($lines);

        preg_match('/HTTP\/\S+\s+(\d+)\s+(.*)/', $statusLine, $m);

        $headers = [];
        foreach ($lines as $line) {
            if (str_contains($line, ':')) {
                [$k, $v] = explode(':', $line, 2);
                $headers[strtolower(trim($k))] = trim($v);
            }
        }

        return [
            'status_code'    => (int) ($m[1] ?? 0),
            'status_text'    => trim($m[2] ?? ''),
            'headers'        => $headers,
            'body'           => $body,
            'body_bytes'     => strlen($body),
            'tls_protocol'   => $crypto['protocol']    ?? 'N/A',
            'tls_cipher'     => $crypto['cipher_name'] ?? 'N/A',
        ];
    }
}

// 使用例
$client = new HttpsClient('www.example.com', verifyPeer: true, timeout: 10.0);

try {
    $res = $client->get('/');
    echo "ステータス : {$res['status_code']} {$res['status_text']}" . PHP_EOL;
    echo "ボディサイズ: {$res['body_bytes']} bytes"                  . PHP_EOL;
    echo "TLSプロトコル: {$res['tls_protocol']}"                     . PHP_EOL;
    echo "暗号スイート : {$res['tls_cipher']}"                       . PHP_EOL;
    echo "Content-Type: " . ($res['headers']['content-type'] ?? 'N/A') . PHP_EOL;
} catch (RuntimeException $e) {
    echo "エラー: " . $e->getMessage() . PHP_EOL;
}

出力例:

ステータス : 200 OK
ボディサイズ: 1256 bytes
TLSプロトコル: TLSv1.3
暗号スイート : TLS_AES_256_GCM_SHA384
Content-Type: text/html; charset=UTF-8

例3:UDP クライアント ─ DNS クエリを自前で送る

udp:// スキームで UDP ソケットを作成し、DNS サーバーへ問い合わせる例です。

<?php

class UdpDnsClient
{
    private string $dnsServer;
    private int    $dnsPort;
    private float  $timeout;

    public function __construct(
        string $dnsServer = '8.8.8.8',
        int    $dnsPort   = 53,
        float  $timeout   = 3.0
    ) {
        $this->dnsServer = $dnsServer;
        $this->dnsPort   = $dnsPort;
        $this->timeout   = $timeout;
    }

    /**
     * Aレコード(IPv4)を問い合わせる簡易実装
     */
    public function queryA(string $hostname): array
    {
        $socket = stream_socket_client(
            "udp://{$this->dnsServer}:{$this->dnsPort}",
            $errno,
            $errstr,
            $this->timeout,
            STREAM_CLIENT_CONNECT
        );

        if ($socket === false) {
            throw new RuntimeException("UDP接続失敗 [{$errno}]: {$errstr}");
        }

        stream_set_timeout($socket, (int) $this->timeout);

        $query = $this->buildDnsQuery($hostname);
        fwrite($socket, $query);

        $response = fread($socket, 512);
        $meta     = stream_get_meta_data($socket);
        fclose($socket);

        if ($meta['timed_out']) {
            throw new RuntimeException("DNSタイムアウト: {$hostname}");
        }

        return [
            'hostname'    => $hostname,
            'dns_server'  => $this->dnsServer,
            'query_bytes' => strlen($query),
            'resp_bytes'  => $response !== false ? strlen($response) : 0,
            'raw_hex'     => $response !== false ? bin2hex(substr($response, 0, 12)) : '',
        ];
    }

    /**
     * 最小限のDNSクエリパケットを構築する
     * (Type A、Class IN)
     */
    private function buildDnsQuery(string $hostname): string
    {
        // トランザクションID(ランダム2バイト)
        $id = random_bytes(2);

        // フラグ: 標準クエリ
        $flags = "\x01\x00";

        // QDCount=1、ANCount=0、NSCount=0、ARCount=0
        $counts = "\x00\x01\x00\x00\x00\x00\x00\x00";

        // QName(ラベル形式)
        $qname = '';
        foreach (explode('.', $hostname) as $label) {
            $qname .= chr(strlen($label)) . $label;
        }
        $qname .= "\x00"; // 終端

        // QType=A(1)、QClass=IN(1)
        $qtype  = "\x00\x01";
        $qclass = "\x00\x01";

        return $id . $flags . $counts . $qname . $qtype . $qclass;
    }
}

// 使用例
$dns = new UdpDnsClient('8.8.8.8');

try {
    $result = $dns->queryA('example.com');
    echo "ホスト名    : {$result['hostname']}"      . PHP_EOL;
    echo "DNSサーバー : {$result['dns_server']}"    . PHP_EOL;
    echo "送信バイト  : {$result['query_bytes']} bytes" . PHP_EOL;
    echo "応答バイト  : {$result['resp_bytes']} bytes"  . PHP_EOL;
    echo "応答ヘッダ  : {$result['raw_hex']}"       . PHP_EOL;
} catch (RuntimeException $e) {
    echo "エラー: " . $e->getMessage() . PHP_EOL;
}

出力例:

ホスト名    : example.com
DNSサーバー : 8.8.8.8
送信バイト  : 29 bytes
応答バイト  : 45 bytes
応答ヘッダ  : abcd8180000100010000

例4:非同期接続(STREAM_CLIENT_ASYNC_CONNECT)で複数ホストに並列接続

STREAM_CLIENT_ASYNC_CONNECT で接続処理をノンブロッキング化し、stream_select で接続完了を検知します。

<?php

class AsyncMultiConnector
{
    private array $sockets  = [];
    private array $labels   = [];
    private array $results  = [];

    /**
     * 非同期接続を開始する(接続完了を待たずに返る)
     */
    public function startConnect(string $label, string $address): void
    {
        $socket = stream_socket_client(
            $address,
            $errno,
            $errstr,
            0,  // タイムアウト0(即時返却)
            STREAM_CLIENT_ASYNC_CONNECT
        );

        if ($socket === false) {
            $this->results[$label] = ['status' => 'error', 'error' => "{$errstr} ({$errno})"];
            return;
        }

        stream_set_blocking($socket, false);
        $id                  = (int) $socket;
        $this->sockets[$id]  = $socket;
        $this->labels[$id]   = $label;
        $this->results[$label] = ['status' => 'connecting', 'address' => $address];
    }

    /**
     * すべての接続完了を待つ(最大 $timeoutSec 秒)
     */
    public function waitAll(float $timeoutSec = 5.0): void
    {
        $deadline = microtime(true) + $timeoutSec;

        while (!empty($this->sockets) && microtime(true) < $deadline) {
            $read   = null;
            $write  = array_values($this->sockets); // 書き込み可能 = 接続完了
            $except = array_values($this->sockets);

            $changed = stream_select($read, $write, $except, 0, 50_000);

            if ($changed === false) break;

            // 接続完了したソケットを処理
            foreach ($write as $sock) {
                $id    = (int) $sock;
                $label = $this->labels[$id];

                // エラー確認
                $error = stream_socket_get_name($sock, true);
                if ($error === false) {
                    $this->results[$label] = ['status' => 'failed'];
                } else {
                    $this->results[$label] = [
                        'status' => 'connected',
                        'peer'   => $error,
                        'local'  => stream_socket_get_name($sock, false),
                    ];
                    fclose($sock);
                }

                unset($this->sockets[$id], $this->labels[$id]);
            }

            // エラーソケットを処理
            foreach ($except as $sock) {
                $id    = (int) $sock;
                $label = $this->labels[$id] ?? 'unknown';
                $this->results[$label] = ['status' => 'error'];
                fclose($sock);
                unset($this->sockets[$id], $this->labels[$id]);
            }
        }

        // タイムアウトしたソケット
        foreach ($this->sockets as $id => $sock) {
            $label = $this->labels[$id];
            $this->results[$label] = ['status' => 'timeout'];
            fclose($sock);
        }
        $this->sockets = [];
        $this->labels  = [];
    }

    public function getResults(): array
    {
        return $this->results;
    }
}

// 使用例:複数サーバーへ並列接続テスト
$connector = new AsyncMultiConnector();

$targets = [
    'example.com:80'  => 'tcp://example.com:80',
    'example.com:443' => 'tcp://example.com:443',
    'example.com:22'  => 'tcp://example.com:22',   // SSH(閉じている可能性大)
    'example.com:9999'=> 'tcp://example.com:9999',  // 存在しないポート
];

$start = microtime(true);
foreach ($targets as $label => $address) {
    $connector->startConnect($label, $address);
}

$connector->waitAll(timeoutSec: 3.0);
$elapsed = round((microtime(true) - $start) * 1000, 1);

echo "並列接続テスト({$elapsed}ms)" . PHP_EOL . PHP_EOL;
echo str_pad("ターゲット",        24) . "結果" . PHP_EOL;
echo str_repeat('-', 44) . PHP_EOL;

foreach ($connector->getResults() as $label => $r) {
    $status = match($r['status']) {
        'connected'  => "✓ 接続成功 ({$r['peer']})",
        'failed'     => "✗ 接続失敗",
        'timeout'    => "⏱ タイムアウト",
        'error'      => "✗ エラー: " . ($r['error'] ?? ''),
        'connecting' => "⋯ 接続中",
        default      => $r['status'],
    };
    echo str_pad($label, 24) . $status . PHP_EOL;
}

出力例:

並列接続テスト(312.4ms)

ターゲット              結果
--------------------------------------------
example.com:80          ✓ 接続成功 (93.184.216.34:80)
example.com:443         ✓ 接続成功 (93.184.216.34:443)
example.com:22          ✗ 接続失敗
example.com:9999        ⏱ タイムアウト

例5:持続的接続(STREAM_CLIENT_PERSISTENT)でHTTPリクエストを再利用する

STREAM_CLIENT_PERSISTENT を使い、一度確立したソケット接続をリクエスト間で使い回します。

<?php

class PersistentHttpClient
{
    private string $host;
    private int    $port;
    private float  $timeout;
    private int    $requestCount = 0;

    public function __construct(string $host, int $port = 80, float $timeout = 5.0)
    {
        $this->host    = $host;
        $this->port    = $port;
        $this->timeout = $timeout;
    }

    public function get(string $path): array
    {
        // STREAM_CLIENT_PERSISTENT: 同じアドレスへの接続を使い回す
        $socket = stream_socket_client(
            "tcp://{$this->host}:{$this->port}",
            $errno,
            $errstr,
            $this->timeout,
            STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT
        );

        if ($socket === false) {
            throw new RuntimeException("接続失敗 [{$errno}]: {$errstr}");
        }

        stream_set_timeout($socket, (int) $this->timeout);

        $isNew = $this->requestCount === 0;
        $this->requestCount++;

        $request = "GET {$path} HTTP/1.0\r\n"
                 . "Host: {$this->host}\r\n"
                 . "Connection: keep-alive\r\n"
                 . "\r\n";

        fwrite($socket, $request);

        $response = '';
        while (!feof($socket)) {
            $chunk = fread($socket, 4096);
            $meta  = stream_get_meta_data($socket);
            if ($meta['timed_out']) break;
            if ($chunk !== false) $response .= $chunk;
        }

        // 持続的接続では fclose しない(次のリクエストで使い回す)
        // fclose($socket); ← コメントアウト

        $statusLine = strtok($response, "\r\n");

        return [
            'path'       => $path,
            'is_new_conn'=> $isNew,
            'status'     => $statusLine,
            'bytes'      => strlen($response),
            'request_no' => $this->requestCount,
        ];
    }

    public function getRequestCount(): int
    {
        return $this->requestCount;
    }
}

// 使用例
$client = new PersistentHttpClient('example.com');

$paths = ['/', '/about', '/contact'];

foreach ($paths as $path) {
    try {
        $result = $client->get($path);
        $connLabel = $result['is_new_conn'] ? '新規接続' : '接続再利用';
        echo "リクエスト#{$result['request_no']} [{$connLabel}] {$result['path']}: {$result['status']}" . PHP_EOL;
    } catch (RuntimeException $e) {
        echo "エラー: " . $e->getMessage() . PHP_EOL;
    }
}

出力例:

リクエスト#1 [新規接続] /: HTTP/1.0 200 OK
リクエスト#2 [接続再利用] /about: HTTP/1.0 404 Not Found
リクエスト#3 [接続再利用] /contact: HTTP/1.0 404 Not Found

例6:UNIXドメインソケットクライアント ─ ローカルプロセス間通信

unix:// スキームでUNIXドメインソケットに接続し、同一ホスト上のデーモンと通信します。

<?php

class UnixSocketClient
{
    private string $socketPath;
    private float  $timeout;

    public function __construct(string $socketPath, float $timeout = 3.0)
    {
        $this->socketPath = $socketPath;
        $this->timeout    = $timeout;
    }

    public function sendAndReceive(string $message): string
    {
        if (!file_exists($this->socketPath)) {
            throw new RuntimeException("ソケットファイルが存在しません: {$this->socketPath}");
        }

        $socket = stream_socket_client(
            "unix://{$this->socketPath}",
            $errno,
            $errstr,
            $this->timeout,
            STREAM_CLIENT_CONNECT
        );

        if ($socket === false) {
            throw new RuntimeException("UNIX接続失敗 [{$errno}]: {$errstr}");
        }

        stream_set_timeout($socket, (int) $this->timeout);

        fwrite($socket, $message . "\n");

        $response = '';
        while (!feof($socket)) {
            $chunk = fread($socket, 4096);
            $meta  = stream_get_meta_data($socket);
            if ($meta['timed_out']) {
                throw new RuntimeException("受信タイムアウト");
            }
            if ($chunk !== false && $chunk !== '') {
                $response .= $chunk;
                // 改行で応答の終端を判定
                if (str_ends_with(rtrim($response), "\n")) {
                    break;
                }
            }
        }

        fclose($socket);
        return rtrim($response);
    }
}

// サーバー側(デモ用:実際は別プロセスで起動)
$sockPath = sys_get_temp_dir() . '/demo_' . getmypid() . '.sock';

$server = stream_socket_server("unix://{$sockPath}", $errno, $errstr);

if ($server) {
    // 非ブロッキングでサーバー側を動かす(デモ用)
    stream_set_blocking($server, false);

    // クライアント側から接続
    $client = new UnixSocketClient($sockPath);

    try {
        // サーバー側:接続受け付け(タイムアウト0で即確認)
        $conn = stream_socket_accept($server, 0);
        if (!$conn) {
            // クライアントから送信してみる(実際はプロセス分離が必要)
            echo "UNIXドメインソケットクライアント定義完了" . PHP_EOL;
            echo "ソケットパス: {$sockPath}" . PHP_EOL;
        } else {
            $msg = fgets($conn);
            fwrite($conn, "受信しました: " . trim($msg) . "\n");
            fclose($conn);
        }
    } catch (RuntimeException $e) {
        echo "エラー: " . $e->getMessage() . PHP_EOL;
    }

    fclose($server);
    if (file_exists($sockPath)) unlink($sockPath);
}

関連する関数との比較

関数役割特徴
stream_socket_clientソケット接続を確立非同期・持続・UDP・TLS対応
fsockopenソケット接続を確立(旧来)シンプルだがフラグ指定なし
stream_socket_serverリスニングソケット作成サーバー側の対になる関数
stream_socket_acceptサーバーが接続を受け入れるstream_socket_server とセット
stream_socket_get_nameローカル/リモートアドレス取得接続確認にも使える

stream_socket_client vs fsockopen

// fsockopen:シンプルだがTCP/UDPのみ、フラグなし
$sock = fsockopen('example.com', 80, $errno, $errstr, 5);

// stream_socket_client:フルコントロール可能
$sock = stream_socket_client(
    'tcp://example.com:80',   // スキーム明示
    $errno, $errstr,
    5.0,
    STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT,  // フラグ
    $context                   // SSL設定など
);
観点stream_socket_clientfsockopen
UDP対応udp:// スキームで可udp:// で可だが制限あり
SSL/TLS◎ コンテキストで設定△ 別途設定が煩雑
非同期接続STREAM_CLIENT_ASYNC_CONNECT✗ 非対応
持続的接続STREAM_CLIENT_PERSISTENT✗ 非対応
UNIXソケットunix:// スキーム✗ 非対応
推奨度◎ PHP5以降の推奨△ 旧来コードとの互換用

よくある注意点・落とし穴

1. エラー時は $error_code と $error_message を確認する

接続失敗時に原因を把握するには参照渡しの変数を必ず確認します。

$socket = stream_socket_client('tcp://192.0.2.1:80', $errno, $errstr, 2.0);
if ($socket === false) {
    echo "エラーコード: {$errno}"   . PHP_EOL; // 例: 111
    echo "エラー内容 : {$errstr}" . PHP_EOL; // 例: Connection refused
}

2. $timeout は接続確立までの時間(読み書きではない)

$timeout は TCP ハンドシェイクが完了するまでの時間です。接続後の読み書きのタイムアウトは別途 stream_set_timeout で設定します。

// 接続タイムアウト: 5秒
$socket = stream_socket_client('tcp://example.com:80', $errno, $errstr, 5.0);

// 読み書きタイムアウト: 3秒(接続後に設定)
stream_set_timeout($socket, 3);

3. STREAM_CLIENT_PERSISTENT は同一プロセス内でのみ有効

持続的接続はスクリプト実行中のみ維持されます。次のリクエスト(別プロセス)では接続が切れている場合があります。

4. 非同期接続後は stream_select で接続完了を確認する

STREAM_CLIENT_ASYNC_CONNECT を使った場合、接続完了前にデータを送受信しようとしてもエラーになります。

$socket = stream_socket_client('tcp://example.com:80', $e, $es, 0, STREAM_CLIENT_ASYNC_CONNECT);

// 即座に fwrite してはいけない → 接続が完了していない
// fwrite($socket, "GET / ..."); NG!

// stream_select で書き込み可能(接続完了)を待ってから使う
$write = [$socket];
$read  = $except = null;
stream_select($read, $write, $except, 5); // 最大5秒待機
// $write に残っていれば接続完了
if (!empty($write)) {
    fwrite($socket, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n");
}

まとめ

項目内容
関数名stream_socket_client(string $address, ...): resource|false
主な用途TCP・UDP・TLS・UNIXソケットへの接続
対応スキームtcp:// udp:// ssl:// tls:// unix://
注目フラグSTREAM_CLIENT_ASYNC_CONNECT(非同期)、STREAM_CLIENT_PERSISTENT(持続)
fsockopen との違い非同期・持続・UNIX対応など機能が豊富
接続タイムアウト第4引数 $timeout(読み書きは stream_set_timeout で別途設定)
PHP バージョンPHP 5.0.0 以上

stream_socket_clientfsockopen の正統な後継として、TCP/UDP/TLS/UNIXソケットをすべて統一的なAPIで扱えます。非同期接続や持続的接続のフラグも備えており、高性能なネットワーククライアントの構築に不可欠な関数です。

stream_socket_server / stream_socket_accept と対にして覚えることで、PHPによるネットワークプログラミングの全体像がクリアになります。ぜひ実際のプロジェクトで活用してみてください。

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