LoginSignup
4
1

More than 5 years have passed since last update.

macOS の PHP で空きポートを取得する(ランダムな未使用ポート検索)

Last updated at Posted at 2018-07-01

PHP でランダムな空きポート番号(未使用のポート番号)を取得するスクリプトです。PHP のビルトイン・ウェブサーバのポート指定時の空きポート探しなどにご利用ください。

なお「タイプ2」のスクリプトは PurePHP なので Windowsでも動作すると思います

また、Mac 用ですが $ dev chrome index.php とすると、ランダムな空きポートでビルトイン Web サーバーが立ち上がり、指定したブラウザで動作検証できるスクリプトを用意しました。

TL;DR(PHP で使用中のポートを元に空きポートを探す)

タイプ1(外部コマンド利用タイプ)

シェルの lsof コマンドで使用中のポートを取得し、そこからランダムな未使用ポートを返します。

netstat コマンドでもよかったのですが、他のプロセスで使っていたビルトインサーバのポートなど、表示されないポートがあったので lsof にしました。

タイプ1.php
function getPortRandom($port_min = 50000, $port_max = 65535)
{
    $cmd   = "lsof -i -P | grep -i 'tcp' | sed 's/\[.*\]/IP/' \
                   | sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' \
                   | awk '!a[$0]++'";
    $ports = array_filter(explode(PHP_EOL, `$cmd`));

    while (true) {
        $port = rand($port_min, $port_max);
        if (false === array_search($port, $ports)) {
            break;
        }
    }
    return $port;
}

2018/08/23 追記:$cmd 行をより正確に表示するように変更しました。
before
lsof -i -P | grep -i 'listen' 2>&1
after
lsof -i -P | grep -i 'tcp' | sed 's/\[.*\]/IP/' \
                   | sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' \
                   | awk '!a[$0]++'

以前は IPv4 の listen 行(監視対象のポート)だけを取得していたのですが、すでに接続済(establish)のポートと IPv6 も含めて、一意のポート一覧を取得するようにしました。

タイプ2(ソケット利用タイプ)

こちらは PHP の標準機能を使ったタイプです。ソケットを作成(socket_create_listen())する際に引数に「0」を渡すと空きポートをランダムに自動で割り当てられたポートを使うタイプです。ビルトイン(標準)関数であるため、おそらく Windows でも利用可能かと思います。

前述の「外部コマンド利用のタイプ1」と比べ圧倒的に速いのですが「ネットワークの受信接続の許可」ポップアップが一瞬表示されるため、連続して何度か使うにはウザいタイプです。

タイプ2.php
function getPortRandom()
{
    $socket = socket_create_listen(0);         //ランダムな空きポートの割り当て
    socket_getsockname($socket, $addr, $port); //ポート情報の取得
    socket_close($socket);                     //ソケットを解放

    return (int) $port;                        //解放されたソケットのポート番号を横取り
}

TS;DR

だって夏だから

Mac で PHP スクリプトの動作確認をするために Web サーバを建てるのも面倒なので、ビルトインサーバで検証していました。

しかし、そのたびに「空いているポート何かな」と悩むのが面倒になったのです。ウェルノウン・ポートなんかも、いちいち確認してられません。すべてが面倒なのですよ。暑いし。エアコン制限されてるし。_(:⁍」 )_

ターミナル
$ cd /path/to/my/php/script
$ ls
index.php
$ 
$ # ポート指定(このポートを決めるのが面倒)
$ PORT_TEMP=8888
$ 
$ # ブラウザ(Safari)とWebサーバ起動
$ open -a "Safari" "http://localhost:$PORT_TEMP" && php -S localhost:$PORT_TEMP
$ 
$ # ブラウザ(Chrome)とWebサーバ起動
$ open -a "Google Chrome" "http://localhost:$PORT_TEMP" && php -S localhost:$PORT_TEMP

そうだ PHP で行こう

そこで、以前作成した CentOS7 の SSH 接続用に Bash で空きポートをランダムに取得するシェル・スクリプトを使おうと思ったのですが、macOS の Bash では標準で shufコマンドに対応していないなど、ストレートに使えそうにありませんでした。

brew でインストールするのも環境依存するのでちょっと違うし、それならば OS に標準で入っている PHP で取得すればいいじゃない、と思い立ちました。

まずは PHP の標準コマンドで「使用中のポートを確認するコマンドはないか」と確認してみたのですが、該当するものはありませんでした。

しかし、マニュアルの記述には無いのですが、マニュアルのコメント欄を見ると「 socket_create_listen() 標準関数の引数を空欄にするか 0 を渡すとランダムに空いているポートを割り当ててくれるよ」という記述が。

マニュアルだと第1引数のポート番号指定は必須になっているのですが、確かにランダムなポートが割り当てられたのです。

これでいける。

ん?いま一瞬何か見えなかった・・・か? :scream:

socket_create_listen()関数でランダムに空きポートが割り当てられることが確認できたので、「PHP にランダムに空きポートを割り当てさせて、直後に解放させたら、そのポートが使えるんじゃね?」と思いつきました。

ターミナル
$ php -a
Interactive shell

php > $sock = socket_create_listen(0);
php > socket_getsockname($sock, $addr, $port);
php > socket_close($sock);
php > echo $port;
58487

「お、いけるかも」と関数化したのも束の間、あることに気づきました。なんか一瞬ピロピロっと表示されるのです。

あまりにも、一瞬すぎて何のメッセージが出ているのか分からなかったのですが、スクリプトを何度も叩くと確かに表示されているのです。

sleep() を入れてみたところ、ソケットを作成するタイミングで以下の確認メッセージが出ていました。ソケットを解放(socket_close())すると消えるのですが、どうやらポートを変えまくってるからか、Mac のファイヤーウォールに怪しまれたようです。

スクリーンショット 2018-07-02 0.38.21.png

まぁ、一回「許可」しておけば良いだけなのですが、他の環境で他の人が使う場合に面倒です。

「ポップアップが出たら許可する」ように案内するのも面倒ですし、マニュアルに書いても「よく分からないのでお断り申し上げる」と、ポチッと「拒否」されることは、まれによくあります。

それにより PHP 自体がロックされて「マニュアル通り作業しても動かないし、他のプログラムも動かなくなった!:angry:」というストーリーが走馬灯のように見えました。

そこで素直に外部コマンドでシェルを叩いて取得したプロセスを料理する方法が確実ということで、シェルと PHP の美味しいところどりをしてみました。

最終的なスクリプト

ターミナルで index.php のあるディレクトリに移動後、$ dev chorme と叩けば、そのディレクトリをルートとした WEB サーバと引数で指定したブラウザが起動するスクリプトにしました。(インストール済みのブラウザに限る)

$ cd /path/to/your/dev/dir
$ ls
index.php
$ dev opera

具体的には、以下のスクリプトのように shebang 付きの PHP スクリプトを、パスの通ったディレクトリdev というファイル名(拡張子なし)で設置して、chmod 0744 devで実行権限を与えて、「俺様簡易検証サーバ・コマンド」の完了です。

dev
#!/usr/bin/env php
<?php

$browser_default = 'safari';
$browser         = isset($argv[1]) ? strtolower($argv[1]) : $browser_default;

switch($browser){
    case 'chrome':
        $browser = 'Google Chrome';
        break;
    case 'firefox':
        $browser = 'Firefox';
        break;
    case 'opera':
        $browser = 'Opera';
        break;
    default:
        $browser = 'Safari';
        break;
}

$port = getPortRandom();
$cmd  = "open -a \"{$browser}\" \"http://localhost:{$port}\" ";
$cmd .= "&& php -S localhost:{$port}";

`$cmd`;

/* [Functions] -------------------------------------------------------------- */

function getPortRandom($port_min = 50000, $port_max = 65535)
{
    $cmd   = 'lsof -i -P | grep -i "listen" 2>&1';
    $rows  = array_filter(explode(PHP_EOL, `$cmd`));
    $ports = array_map(
        function ($row) {
            $row  = str_replace(':', ' ', $row);
            $cols = explode(' ', $row);
            $cols = array_values(array_filter($cols));
            return (int) $cols[9];
        },
        $rows
    );

    while (true) {
        $port = rand($port_min, $port_max);
        if (false === array_search($port, $ports)) {
            break;
        }
    }
    return $port;
}

2018/08/24 追記:macOS 標準のシェル・コマンドで空きポートを取得するスクリプトを書いたので、それを利用してよりコードが短くなりました。

タイプ1改訂版.php
function getPortRandom($port_min = 50000, $port_max = 65535)
{
    $cmd   = "lsof -i -P | grep -i 'tcp' | sed 's/\[.*\]/IP/' \
                   | sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' \
                   | awk '!a[$0]++'";
    $ports = array_filter(explode(PHP_EOL, `$cmd`));

    while (true) {
        $port = rand($port_min, $port_max);
        if (false === array_search($port, $ports)) {
            break;
        }
    }
    return $port;
}

参考文献

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1