第16章 ネットワーク

ソケット通信の基本

ソケット通信はネットワークプログラミングの基礎です。2つのプログラムが情報を送受信するためのエンドポイントを提供します。

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false) {
    echo "socket_create() 失敗: " . socket_strerror(socket_last_error()) . "\n";
} else {
    echo "socket_create() 成功\n";
}
socket_close($socket);

これはソケットを作成し、その成功または失敗を表示する基本的なソケットプログラムです。AF_INETはIPv4を意味し、SOCK_STREAMはTCPを意味します。socket_createは新しいソケットを作成し、それを閉じるためにsocket_closeを使用します。


UDPとTCP

PHPはネットワークプログラミングのための関数を提供しており、UDPやTCPを使ってデータを送受信することが可能です。以下に、それぞれのプロトコルについて簡単なサンプルコードを記述します。

TCPを使った例

サーバ側のコード

$socket = stream_socket_server("tcp://127.0.0.1:8000", $errno, $errstr);
if (!$socket) {
  echo "$errstr ($errno)
\n"; } else { while ($conn = stream_socket_accept($socket)) { fwrite($conn, 'Hello World'. "\n"); fclose($conn); } fclose($socket); }

上記のサーバコードは、8000番ポートでTCP接続を待ち受け、接続があったら"Hello World"というメッセージを送信しています。

クライアント側のコード

$fp = stream_socket_client("tcp://127.0.0.1:8000", $errno, $errstr, 30);
if (!$fp) {
    echo "$errstr ($errno)
\n"; } else { while (!feof($fp)) { echo fgets($fp, 1024); } fclose($fp); }

クライアントコードでは、サーバーの8000番ポートに接続し、受信したデータを出力しています。

UDPを使った例

サーバ側のコード

$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_bind($sock, '127.0.0.1', 8000);

while (true) {
    socket_recvfrom($sock, $message, 1024, 0, $from, $port);
    echo "Received message from $from:$port: $message\n";
}

このコードはUDPソケットを作成し、8000番ポートでデータの受信を待ち続けます。

クライアント側のコード

$message = 'Hello World';
$sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_sendto($sock, $message, strlen($message), 0, '127.0.0.1', 8000);

クライアントコードでは、サーバーの8000番ポートにメッセージを送信しています。これらのコードは簡易的なものであり、実際のプログラムではエラーハンドリングなどを行うべきです。


エコーサーバーの実装例

PHPでのソケットプログラミングについて、簡単なエコーサーバーの実装例を示します。エコーサーバーは、クライアントから送られてきたデータをそのままクライアントに返すサーバーです。以下は、エコーサーバーを作成するための簡単なPHPスクリプトです。

// TCP/IP ストリーム作成
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// ソケットオプション設定
socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);

// サーバーの IP と ポート番号設定
$address = '127.0.0.1'; // localhost を指定
$port = 12345; // 適当なポート番号を指定

// ソケットと指定したアドレス・ポートをバインド
if (!socket_bind($sock, $address, $port)) {
    echo 'socket_bind() に失敗しました: ' . socket_strerror(socket_last_error($sock)) . "\n";
}

// 接続待ちを開始
if (!socket_listen($sock, 5)) {
    echo 'socket_listen() に失敗しました: ' . socket_strerror(socket_last_error($sock)) . "\n";
}

do {
    // クライアントからの接続を待つ
    if (!$client = socket_accept($sock)) {
        echo 'socket_accept() に失敗しました: ' . socket_strerror(socket_last_error($sock)) . "\n";
        break;
    }

    // クライアントからのデータを受信し、そのままクライアントに送信
    do {
        if (false === ($buf = socket_read($client, 2048, PHP_NORMAL_READ))) {
            echo 'socket_read() に失敗しました: ' . socket_strerror(socket_last_error($client)) . "\n";
            break 2;
        }
        if (!$buf = trim($buf)) {
            continue;
        }
        if ($buf == 'quit') {
            break;
        }
        $talkback = "PHP: '{$buf}'\n";
        socket_write($client, $talkback, strlen($talkback));
    } while (true);

    // ソケットを閉じる
    socket_close($client);
} while (true);

socket_close($sock);

このスクリプトは、TCP/IPを使用してエコーサーバーを作成します。localhostの12345ポートで接続を待ち、クライアントからのメッセージを受け取ったらそのまま返します。なお、このスクリプトはシンプルな例であり、実際の運用には適していません。エラーハンドリングやセキュリティ対策が不十分です。ただし、ソケット通信の基本的な流れを理解するには十分な内容となっています。

ストリーム

PHPのストリームとは、データを一貫して扱うための抽象化レイヤーです。ファイル、ネットワーク接続、変数の中身など、全てのデータを「ストリーム」として同じように操作することができます。以下に、ファイルストリームとメモリストリームの例を示します。

ファイルストリームの例

// ファイルを開く
$handle = fopen('file.txt', 'w');
if ($handle) {
    // ファイルに書き込む
    fwrite($handle, 'Hello, World!');
    // ファイルを閉じる
    fclose($handle);
}

上記のコードは、'file.txt'という名前のファイルを開き、その中に'Hello, World!'という文字列を書き込んでいます。fopen(), fwrite(), fclose()などはストリーム関数で、ファイル操作を行います。

メモリストリームの例

// ファイルを開く
$handle = fopen('file.txt', 'w');
if ($handle) {
    // ファイルに書き込む
    fwrite($handle, 'Hello, World!');
    // ファイルを閉じる
    fclose($handle);
}

上記のコードは、メモリ上にストリームを作り、そこに'Hello, World!'という文字列を書き込み、すぐにそれを読み込んで出力しています。


ストリームコンテキスト

ストリームコンテキストは、ストリームに対してパラメータやオプションを指定するためのもので、stream_context_create関数で作成できます。これを使用することで、ストリームの挙動を柔軟に制御することが可能となります。

以下に、HTTPリクエストにおけるメソッドやヘッダを指定するためのストリームコンテキストの使用例を示します。

// ストリームコンテキストのオプションを設定
$options = [
    'http' => [
        'method' => 'POST',
        'header' => 'Content-type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'key1' => 'value1',
            'key2' => 'value2'
        ])
    ]
];

// ストリームコンテキストを作成
$context = stream_context_create($options);

// ストリームを開く
$data = file_get_contents('http://www.example.com', false, $context);

if ($data === false) {
    echo "読み込みエラー!\n";
} else {
    echo $data;
}

このコードでは、最初にPOSTメソッドでHTTPリクエストを送信するためのストリームコンテキストのオプションを設定しています。ここではメソッドを'POST'に設定し、Content-Typeヘッダを'application/x-www-form-urlencoded'に設定、さらにPOSTデータとしてkey-value形式のデータを送信するための設定を行っています。

次にstream_context_create関数でこのオプションを用いてストリームコンテキストを作成し、そのコンテキストをfile_get_contents関数に渡すことで、このコンテキストの設定に従ったHTTPリクエストを行うことができます。

なお、file_get_contents関数は通常、ファイルからデータを読み込むために使用しますが、ここではHTTPプロトコルを使用してリモートサーバからデータを取得しています。このように、PHPのストリームは単なるファイル入出力だけでなく、ネットワーク通信にも使用することが可能です。


APIとの通信

PHPでAPIと通信するためには、基本的にはHTTPクライアントとしての動作が必要となります。ここでは、最も基本的な形としてfile_get_contents関数を使用した例と、より高度な設定が可能なcURLを使用した例を示します。

まずはfile_get_contentsを使用した例からです。

$url = 'https://api.example.com/data'; // APIのURL
$data = file_get_contents($url);

if ($data === false) {
    echo "読み込みエラー!\n";
} else {
    $json = json_decode($data); // APIのレスポンスがJSON形式を前提とした場合
    var_dump($json);
}

この例では、指定したURLのAPIからデータを取得しています。file_get_contents関数を用いてAPIからデータを取得し、その結果をJSON形式からPHPのデータ構造に変換しています。

次に、cURLを使用した例を示します。

$url = 'https://api.example.com/data'; // APIのURL

$ch = curl_init(); // cURLセッションを初期化
curl_setopt($ch, CURLOPT_URL, $url); // URLを設定
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // curl_execの結果を文字列で返すように設定

$data = curl_exec($ch); // URLからデータを取得

if (curl_errno($ch)) {
    echo "エラー:" . curl_error($ch);
} else {
    $json = json_decode($data); // APIのレスポンスがJSON形式を前提とした場合
    var_dump($json);
}

curl_close($ch); // cURLセッションを閉じる

cURLはより高度なHTTPリクエストを行うためのライブラリで、HTTPメソッドの指定やHTTPヘッダのカスタマイズ、SSL証明書の検証など、詳細な設定が可能です。また、エラーハンドリングもより詳細に行うことができます。

この例では、まずcurl_init関数でcURLセッションを初期化し、curl_setopt関数で各種設定を行ってからcurl_exec関数でAPIからデータを取得しています。取得したデータはson_decode関数でJSON形式からPHPのデータ構造に変換しています。

なお、APIとの通信では通常、APIのドキュメンテーションに従って適切なHTTPメソッドやHTTPヘッダを設定する必要があります。また、APIが要求する認証(APIキーの送信やOAuthなど)も適切に行う必要があります。これらの設定はfile_get_contents関数のストリームコンテキストやcURLの各種オプションで行うことができます。


WebSockets

WebSocketsとは、インターネット上でリアルタイム通信を可能にする技術の一つです。通常のHTTP通信は、クライアントがサーバーにリクエストを送信し、サーバーがそれに対するレスポンスを返す、一方通行の通信形式です。一方、WebSocketsはフルダプレックスの通信を可能にするプロトコルで、クライアントとサーバーが同時に互いにデータを送受信できます。

PHPでは、Ratchetというライブラリを用いることでWebSocketsを利用することができます。WebSocketsはHTTPと違い、サーバとクライアント間で持続的な接続を確立し、双方向の通信を可能にします。

以下に、Ratchetを使用して簡単なWebSocketサーバを作成する例を示します。

まず、Ratchetをインストールします。ターミナルに以下のコマンドを入力して実行します。

composer require cboden/ratchet

次に、WebSocketサーバのコードを書きます。下記のコードはbinディレクトリのserver.phpというファイルとして保存します。

require dirname(__DIR__) . '/vendor/autoload.php'; // Composerのオートローダーを読み込む

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use YourApp\Chat;

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new Chat() // WebSocketによるチャットアプリケーションのインスタンス
        )
    ),
    8080 // ポート番号
);

$server->run(); // サーバを起動

上記のコードは、Chatという名前のクラスをWebSocketの通信処理用のクラスとして使用しています。これは開発者自身で作成する必要があり、具体的なコードは以下のようになります。

composer require cboden/ratchet

次に、WebSocketサーバのコードを書きます。下記のコードはbinディレクトリのserver.phpというファイルとして保存します。

namespace YourApp;

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    public function onOpen(ConnectionInterface $conn) {
        // 新しい接続が開かれたときの処理
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        // メッセージが送信されたときの処理
    }

    public function onClose(ConnectionInterface $conn) {
        // 接続が閉じられたときの処理
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        // エラーが発生したときの処理
    }
}

このChatクラスは、WebSocketの各種イベント(接続開始、メッセージ受信、接続終了、エラー発生)に対応するメソッドを定義しています。これらのメソッドはそれぞれのイベントが発生したときにRatchetから呼び出され、適切な処理を行います。


非同期通信

PHPはデザイン上、同期的な実行を前提にしている言語であり、非同期通信を直接サポートしているわけではありません。そのため、非同期通信を行うには、外部ライブラリやツールを使用する、あるいは他の方法で間接的に非同期通信をエミュレートする必要があります。

PHPで非同期通信をエミュレートする一つの方法は、PHPのexec関数やpcntl_fork関数を使って、バックグラウンドプロセスを作成し、それを非同期に実行する方法です。

exec関数を使ってバックグラウンドで非同期にコマンドを実行する例

// 長時間実行されるコマンド。ここでは10秒間sleepする例を示します
$command = 'sleep 10';

// コマンドをバックグラウンドで実行する
exec($command . " > /dev/null &");

echo "コマンドがバックグラウンドで実行されています。\n";

このスクリプトは、コマンドをバックグラウンドで実行し、すぐに制御を返すため、echo文はすぐに実行されます。

ただし、この方法には次のような制限があります。

・バックグラウンドプロセスは親プロセスとは独立して実行され、親プロセスとの間で直接データを共有することはできません。

・これはコマンドラインからのPHPスクリプトにのみ適用可能で、ウェブサーバーからのPHPスクリプトでは実行できません。

したがって、完全な非同期通信を実現するためには、ReactPHPのような外部ライブラリの使用を検討するか、PHPのマルチプロセッシング機能を使用して子プロセスを作成する必要があります。

pcntl_fork関数を使った非同期処理の例

// 現在のプロセスを複製(フォーク)します
$pid = pcntl_fork();

if ($pid == -1) {
    // フォークに失敗した場合
    die("フォークに失敗しました\n");
} elseif ($pid) {
    // 親プロセスの場合
    echo "これは親プロセスです\n";
    pcntl_wait($status); // 子プロセスの終了を待つ
} else {
    // 子プロセスの場合
    echo "これは子プロセスです\n";
}

このコードを実行すると、「これは親プロセスです」、「これは子プロセスです」というメッセージが表示されます。これらのメッセージは、親プロセスと子プロセスがそれぞれ独立して実行されていることを示しています。

ただし、注意すべき点がいくつかあります

・pcntl_fork関数は、PHPがCLI(コマンドラインインターフェース)モードで実行されている場合にのみ使用できます。ウェブサーバーからのPHPスクリプトでは使用できません。

・子プロセスは親プロセスの全てのメモリを複製します。したがって、メモリを大量に消費するプログラムをフォークすると、システムのメモリを大量に消費する可能性があります。

・フォークしたプロセスは独立して動作するため、共有リソース(データベース接続、ファイルハンドルなど)を扱う際には注意が必要です。これらのリソースは、通常、プロセス間で直接共有することはできません。


その他のネットワーク関連関数

PHPには多数のネットワーク関連の関数が存在しますが、ここではいくつかの関数について具体的な例を挙げて説明します。

gethostbyname()

この関数は指定されたホスト名のIPv4アドレスを返します。

$ip = gethostbyname('www.example.com');
echo $ip;

gethostbyaddr()

この関数は指定されたIPv4アドレスのホスト名を返します。

$host = gethostbyaddr('93.184.216.34');
echo $host;

checkdnsrr()

この関数は指定されたホスト名がDNSレコードに存在するかを確認します。

if (checkdnsrr('www.example.com')) {
    echo "DNSレコードが存在します。\n";
} else {
    echo "DNSレコードが存在しません。\n";
}

inet_ntop()とinet_pton()

これらの関数はIPv6およびIPv4アドレスを処理するために使用します。inet_pton()は文字列のIPアドレスをバイナリ形式に変換し、inet_ntop()はその逆を行います。

$bin = inet_pton('2001:0db8:85a3:0000:0000:8a2e:0370:7334');
echo $bin;

$str = inet_ntop($bin);
echo $str;


練習問題1.

以下のコードの動作を説明してください。また、どのような時にこのコードを使用するでしょうか?

$url = 'https://api.example.com/data';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);


練習問題2.

以下のコードがエラーを返す理由を説明してください。

$socket = fsockopen("www.example.com", 80);
if ($socket) {
    $request = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n";
    fwrite($socket, $request);
    $response = fgets($socket, 4096);
    fclose($socket);
    echo $response;
} else {
    echo "Unable to connect.";
}


練習問題3.

以下の仕様を満たすPHPスクリプトを作成してください。

  1. 外部のAPI(例えばhttps://api.github.com/users/{username})をcurlを使って呼び出し、取得した結果を表示します。
  2. このスクリプトは任意のGitHubユーザー名を引数として受け取ります({username}の部分を置換)。