PHPで学ぶ
ソケットプログラミング入門
PHPカンファレンス2016
2016/11/3
宇都宮 諒(@ryo511)
JavaScript入門 ~Webで見たあの機能を自分で作る~(日経BP、2016/10/31)
Webアプリの作り方(日経ソフトウエア2016年11月号)
PHP7の新機能(日経ソフトウエア2016年8月号)
L7: アプリケーション層
L6: プレゼンテーション層
L5: セッション層
L4: トランスポート層
L3: ネットワーク層
L2: データリンク層
L1: 物理層
ソケットを使うと、
TCPやUDPの上で
動作するアプリケーションを
作成できる
ネットワーク上でコンピュータを一意に特定する識別子
IPv4は32bit整数
127.0.0.1 のように8bit毎に「.」で区切って十進法で表記するのが一般的
IPv6は128bit整数
0:0:0:0:0:0:0:1(省略形は ::1) のように16bit毎に「:」で区切って十六進法で表記する
ちなみに、 127.0.0.1 や ::1 は、自分自身を指すアドレスなので「ループバックアドレス」と呼ばれる
コンピュータ上で、ネットワークアプリケーションを特定するための番号
0-65535までの番号がある
0-1023はwell-knownポート
待ち受けポートとして使うべきではない
49152-65535はephemeral(短命)ポート
OSが自動で割り振る番号なので、使うべきではない
自前のネットワークアプリケーションの待ち受けポートには1024-49151を使えばよさそう…?
49151-65535がエフェメラルポートなのは、
RFC6335に沿って実装されたOSのみ
最近のWindowsやBSD系のUNIXはRFC6335に沿っている
Linuxは32768-61000
結論:待受ポートには1024-32767を使うのが安全
IANAに登録されてる既存アプリがないか、あったとして自分のアプリに影響しそうかだけ確認しておく
TCP/IPでは、IPアドレスとポート番号の組み合わせによって、通信相手のコンピュータとアプリケーションを特定する
「127.0.0.1:8000」と書くと、「127.0.0.1というIPアドレスのコンピュータの8000番ポート」という意味になる
ソケットはIPアドレスとポート番号に紐づく
サーバソケットは以下のような流れで動作する
create (ソケットの作成)
bind (ソケットを特定のIPアドレスとポートに紐付け)
listen (接続の待受を開始)
accept (接続を受信)
close (接続を切断)
<?php
// TCPソケット(IPv4)
$tcpSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// UDPソケット(IPv4)
$udpSocket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_create()はリソースオブジェクトを返す
socket(2) システムコールに相当
※システムコール:OSの機能の呼び出し
このリソースオブジェクトの操作にはソケット拡張の提供する関数を使用する必要がある
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000); //0.0.0.0:8000に紐付け
socket_bind() によってソケットがIPアドレスとポートに紐付けられる
bind(2) システムコールに相当
IPアドレスは自分のコンピュータに使用可能なものである必要がある
0.0.0.0 はループバックも含め、全てのインタフェースに対するコネクションを受け付けるアドレス
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5); // 待ち受けを開始し、最大5件の接続待ちをキューに溜める
socket_listen()によって接続待ちを開始する
listen(2) システムコールに相当
接続待ちクライアントの最大数を設定できる
設定可能な最大値はSOMAXCONN定数で参照可能
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5);
$remote = socket_accept($socket);
socket_accept()によって接続を受け取る
accept(2) システムコールに相当
接続が来るまで、プロセスをブロックする
↑のコードは、クライアントからの接続が来ないと、socket_accept()を呼び出したところで固まったまま終了しなくなる
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5);
$remote = socket_accept($socket);
// 何か処理をする
socket_close($remote);
socket_close($socket);
socket_close()によって接続を受け取る
close(2) システムコールに相当
ファイル操作と同様、使い終わったらcloseするのがお作法
クライアントソケットは以下のような流れで動作する
create (ソケットの作成)
bind(ソケットを特定のIPアドレスとポートに紐付け)
connect (リモートソケットに接続)
close (接続を切断)
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// クライアントの場合、bindは不要
ソケットの作成方法はサーバと同じ
クライアントは、基本的にbindを行わない
bindしなければ、OSによってエフェメラルポートが割り振られる
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '0.0.0.0', 8000);
// 何か処理をする
socket_close($socket);
socket_connect()で別のソケットに接続できる
connect(2)システムコールに相当
基本は以下
socket_read() で受信
socket_write() で送信
他にも、微妙に異なるバリエーションの関数がいくつかある
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5);
$remote = socket_accept($socket);
$data = socket_read($remote, 1024); // 1024バイトまで読み込み
echo $data . PHP_EOL;
socket_close($remote);
socket_close($socket);
socket_read():データを受信する
read(2)システムコールに相当
一度に読み込めるのはバッファの最大長まで
↑の例では、1025バイト以上データが送られると、取りこぼしが発生する
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5);
$remote = socket_accept($socket);
while ($data = socket_read($remote, 1024)) {
echo $data . PHP_EOL;
}
socket_close($remote);
socket_close($socket);
socket_read()は読み込むデータが無くなると空文字列を返す
空文字列が返ってくるまで読みこめばよい
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5);
$remote = socket_accept($socket);
socket_set_nonblock($remote); // リモートソケットをノンブロッキングモードにする
while ($data = socket_read($remote, 1024)) {
echo $data . PHP_EOL;
}
socket_close($remote);
socket_close($socket);
socket_read()はデータが全く送られてこないと止まってしまう
データが送られてこない場合はすぐに結果を返すようにするには、ノンブロッキングモードを使用する
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '0.0.0.0', 8000);
socket_write($socket, 'hi');
socket_close($socket);
socket_write()でデータを書き込める
write(2)システムコールに相当
<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8000);
socket_listen($socket, 5);
while ($remote = socket_accept($socket)) {
$data = '';
while ($buffer = socket_read($remote, 1024)) {
$data .= $buffer;
}
socket_write($remote, $data);
socket_close($remote);
}
echoサーバとは、クライアントから受け取ったデータをオウム返しにクライアントに返すサーバのこと
acceptをループにすることで実現
主に stream_xxx() といった名前の関数群
PHP 5以降ではコンパイルオプション関係なく有効
ファイルの入出力に加えて、ソケットも同じAPIで扱うことができる
<?php
$socket = stream_socket_server('tcp://127.0.0.1:8000');
while ($remote = stream_socket_accept($socket)) {
$data = stream_get_contents($remote);
fwrite($remote, $data);
fclose($remote);
}
fclose($socket);
echoサーバをストリームを使って実装すると↑のようになる
stream_socket_server()はcreate, bind, listenを行う
<?php
$socket = stream_socket_client('tcp:127.0.0.1:80');
fwrite($socket, 'hi');
fclose($socket);
stream_socket_client()はcreateとconnectを行う
1プロセス1コネクションモデルでは、サーバの高速化には限界がある
遅いクライアントがあるとサーバのスループットが出なくなる
サーバのスループットを改善するには、コネクションを並列処理する必要がある
※並列処理を入れるとプログラムは複雑化するので、そもそもどの程度のパフォーマンスが必要か検討すること
非同期IO(select(2)等の非同期APIを使用)
socket_select()またはstream_select()
Libevent拡張
非同期IO(Reactorパターン)
reactphp/socket等
マルチプロセス
pcntl拡張
マルチスレッド
pthreads拡張
まずは非同期APIを使うべき
非同期APIを使っても1プロセスでは足りない場合…
(1) Reactorパターンを使用
Pros: PHP組み込みの関数だけで実装可能
Cons: 耐障害性が低い
(2) マルチプロセスを使用
Pros: 耐障害性が高い
Cons: Windowsだと遅い
(3) マルチスレッドを使うなら別言語のほうが…
リソースの取り扱いが難しい
select(2)は、複数のリソースの状態をチェックするシステムコール
ソケットの配列をselect(2)に渡すと、読み込みや書き込みが可能なソケットだけが返却される
<?php
$read = [$socket]; // $socketはサーバソケットのリソース
$write = null;
$except = null;
$changed = socket_select($read, $write, $except);
if ($changed > 0) {
foreach ($read as $socket) {
// ソケットからデータを読み取り
}
}
select(2)は、監視するソケットの数が多いと遅くなる
epoll(2)やkqueue(2)といった、より高速なシステムコールもあるが、OSによって使えるものが異なる
Linuxはepoll(2)、BSDはkqueue(2)
epoll(2)、kqueue(2)等のシステムコールを抽象化したlibeventというライブラリがある
PHPでlibeventを使うには、Libevent拡張を使用する
// 「PHP libevent で多重化エコーサーバー」
// http://qiita.com/d_nishiyama85/items/7e9a72a69f90487a892d より引用
<?php
// サーバーソケットの作成
$socket = stream_socket_server('tcp://0.0.0.0:8000', $errno, $errstr);
if (!$socket) {
die("$errstr ($errno)\n");
}
stream_set_blocking($socket, 0);
$base = event_base_new();
$event = event_new();
event_set($event, $socket, EV_READ | EV_PERSIST, 'ev_accept', $base);
event_base_set($event, $base);
event_add($event);
event_base_loop($base);
// 接続中のクライアントを管理する変数
$GLOBALS['connections'] = [];
// クライアントの接続をバッファリングするオブジェクトを管理する変数
$GLOBALS['buffers'] = [];
// さらに50行ほど続く
<?php
$server = stream_socket_server('tcp://127.0.0.1:8080');
stream_set_blocking($server, 0);
$loop = React\EventLoop\Factory::create();
$loop->addReadStream($server, function ($server) use ($loop) {
$conn = stream_socket_accept($server);
$data = stream_get_contents();
$loop->addWriteStream($conn, function ($conn) use (&$data, $loop) {
$written = fwrite($conn, $data);
if ($written === strlen($data)) {
fclose($conn);
$loop->removeStream($conn);
} else {
$data = substr($data, $written);
}
});
});
$loop->run();
非同期IOを抽象化したライブラリ
Libevent拡張がインストールされていればLibeventを使用する
Libeventが使えない場合はstream_select()を使用する
イベント駆動でコールバック関数を実行するモデル
1プロセスで処理可能な接続数を増やす
代表的な実装例はNode.js
PHPなら、reactphp/socketを使うと簡単に実装できる
<?php
$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$socket->on('connection', function ($conn) {
$conn->on('data', function ($data) use ($conn) {
$conn->write($data);
$conn->close();
});
});
$socket->listen(1337);
$loop->run();
pcntl拡張を使用
プロセスの制御はそれなりに面倒
ソケットを使うと、普段PHPで触っているよりも下の世界に触れることができる
(ソケットのような)低レイヤーの基礎的な技術は、枯れていて変わりにくいので、投資する価値がある
ソケットプログラミングについて丁寧に解説
サンプルコードはRuby
オススメ
PHP Socket Programming Handbook
サンプルコードがPHP!(GitHubで公開中)
Working With〜 に比べると解説が駆け足
TCP/IPソケットプログラミング C言語編
2003年の本で、原書は改訂版が出ている