被らないポート番号を取得したい
macOS のターミナルからワンライナーもしくは bash
のシェル・スクリプトで、ランダムな空きポート番号(未使用のポート番号)を取得したいのです。
- ランダムで未使用の空きポート番号を取得したい
- macOS のデフォルト状態で使いたい(
brew
などで別途インストールできない) - サーバのポート指定のために、空きポート探しに使いたい
- PHP や Golang などのビルトイン WEB サーバー
- Gitea のビルトイン SSH サーバ
- Vargant で(プロビジョンされた)サーバーで使う SSH ポート
※ コピペピピックが怖い方は TS; DR の詳細をご覧ください
TL; DR
ワンライナー
while :; do PORT="$(jot -w %i -r 1 1 65536)" ; RESULT="$(lsof -i :$PORT)"; if [ -z "$RESULT" ] ; then break ; fi ; done; echo "$PORT"
スクリプト(速いタイプ)
使い捨てで、いま現在利用されていないポート番号が欲しいだけなら lsof
コマンドで取得できます。80番ポートの場合 lsof -i :80
で、戻り値のあり・なしで判断できます。
しかし、他のアプリで予約済みのポートなどであっても未使用の場合は空きポートと判断してしまいます。SSH や WEB サーバーなどの長く使う可能性のある場合は次の「確実なタイプ」をご利用ください。
#!/usr/bin/env bash
port_min=1
port_max=65535
while :
do
port="$(jot -w %i -r 1 "$port_min" $((port_max+1)))"
result="$(lsof -i :$port)"
if [ -z "$result" ] ; then
break
fi
done
echo "$port"
スクリプト(確実なタイプ)
#!/usr/bin/env bash
function getPortUsed() {
# この取得処理が一番重いので変数に入れて使い回すために用意
echo `lsof -i -P | grep -i "tcp" | sed 's/\[.*\]/IP/' \
| sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' \
| awk '!a[$0]++'`
}
function getPortRandom() {
# 使用中のポート一覧がセットされていなければ関数内で取得
if [ -z ${port_list+x} ]; then
port_list="$(getPortUsed)"
fi
# 検索ポートの範囲指定(デフォルト範囲)
port_min=${1:-1}
port_max=${2:-65535}
# 希望するポートがセットされていない場合や範囲外の場合は初期化
if [ -z "$port" ] || [ $port_min -gt $port ] || [ $port_max -lt $port ]; then
port="$(jot -w %i -r 1 $port_min $((port_max+1)))"
fi
port=$((port+0)) #文字列の数字を数値に変換
while :
do
# ポートの衝突フラグをリセット(1なら使用中)
port_collide=0
# 使用中のポート一覧と $port を比較
for port_used in $port_list
do
if [ "$port" = "$port_used" ]; then
port_collide=1 # 使用中のポートと同じ
break # 処理を抜けてランダム番号を再取得
fi
done
# 現在の $port が使用中ポートと重ならない場合は処理を抜ける
if [ $port_collide -eq 0 ]; then
break
fi
# ランダムな番号を取得
port="$(jot -w %i -r 1 $port_min $((port_max+1)))"
done
echo "$port"
}
# [使用例] -------------------------------------------------------------------------
# 使用中のポート一覧取得(この取得処理が一番重いため変数に入れておく。なくても動く)
port_list="$(getPortUsed)"
# 使用例1(シンプル)
# -------
port="$(getPortRandom)" # ランダムな空きポート番号を取得
echo "$port" # 取得結果表示
# 使用例2(希望ポート指定)
# -------
port=8080 # 希望するポート番号
port="$(getPortRandom)" # ランダムな空きポート番号を取得
echo "$port" # 取得結果表示
# 使用例3(ポート範囲指定)
# -------
port="$(getPortRandom 10 500)" # ポート番号の範囲を指定(10〜500)
echo "$port" # 取得結果表示
# 使用例4(希望ポートと範囲指定)
# -------
port=400 # 希望するポート番号
port="$(getPortRandom 10 500)" # ポート番号の範囲を指定(10〜500)
echo $port # 取得結果表示
$ ./getport.sh
27690
8080
169
400
動作検証済み環境
- macOS HighSierra(OSX 10.13.6)
-
$ bash --version
: GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17) -
$ man lsof
: Revision-4.89 -
$ grep --version
: grep (BSD grep) 2.5.1-FreeBSD -
$ man sed
: BSD General Commands, May 10, 2005 -
$ awk --version
: awk version 20070501 -
$ man jot
: BSD General Commands, February 19, 2010
-
関連記事
- PHP版 macOS のランダムな空きポートを取得するスクリプト @ Qiita
- Who is listening on a given TCP port on Mac OS X? @ StackOverflow
TS;DR
サーバの空きポート探しで CentOS7 用のスクリプトが macOS に使えなかったので、PHP で Mac の空きポート探しをするスクリプトの Qiita 記事を書きました。しかし、やはり Mac のシェルでも使えるようにしたいと思い、勉強がてら調べてみました。
使用中のポート一覧の取得
「空きポート探し」と言っても、使用中のポート一覧を取得して、ランダムに作成した番号と重複していなければ空きポートという、単純な処理です。
以下のコマンドが、使用中の TCP のポート番号を一覧で取得するコマンドです。
lsof -i -P | grep -i "tcp" | sed 's/\[.*\]/IP/' | sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' | awk '!a[$0]++'
上記の処理を分解すると以下の7ステップになり、最初のlsof
コマンドの結果をパイプで次々とコマンドに渡し、ふるいに掛けながらポート番号を抜き出すという、ゴリゴリとした処理です。Netstat
コマンドを利用しなかったのは、どうも別のプロセスで PHP や GO などのビルトインサーバーのポートが出てこなかったためです。
また、LISTEN
(待機)しているポートだけでなく ESTABLISH
(接続)しているものなどの、全ての TCP ポートが含まれます。
-
lsof -i -P
ポート一覧を取得しています。今回の要(かなめ)とも言えるコマンドです。
lsof
コマンドは「List Open Files
」の略で、現在開いているファイルの情報を得るコマンドです。「ポートなのにファイル?」と思いました思われるかもしれませんが、Linux/UNIX ではファイル、ディレクトリ、ストリーム、ソケットなどは全て「ファイル」として扱うという、シンプルかつ柔軟な概念があります。ls
コマンドでカレントディレクトリや親ディレクトリが.
や..
としてファイル一覧に表示されるのも、ディレクトリをファイルとして扱っているからです。-i
オプションはインターネット(やネットワーク関連)の接続ファイルを表示し、-P
オプションでポート番号をウェルノウンのポート名に変換するのを防いでいます。つまりポート番号は番号のまま表示さるオプションです。-P
オプションを付けないと 80 番ポートの場合http
と置き換わってしまいましたます。実はページ冒頭にあるように「
lsof -i :${PORT_NUM}
」でポート番号を渡し、戻り値があったら使用中、なかったら未使用という処理も試したのですが、圧倒的に速い反面、予約済みのポート関係なく使用中か否かの判断になるらしく、長期的に使う予定のポートとしては正確性に欠けた(のちのち他のアプリとバッティングする可能性が高かった)ので「lsof -i -P
」で一覧情報を取得し、そこにないものを空きポートとしました。 -
grep -i "tcp"
tcp
の文字を含む行のみピックアップしてフィルターしています。 -
sed 's/\[.*\]/IP/'
IPv6 の
[ffff:ffff::ffff:ffff:ffff:ffff]
をダミー文字IP
に置換しています。これをしておかないと、この後に出てくる:
の置換でポート番号が記載される列がズレてしまうからです。 -
sed 's/:/ /'
最初に出現した
:
をスペースに置換しています。これで、各行の項目の区切りがスペース区切りになります。最初の:
の置換だけで十分なのでs/:/ /g
とはしていません。 -
sed 's/->/ /'
接続されている(
ESTABLISH
)ポートの接続元を示す->
をスペースに置換しています。この->
とポート番号の間にスペースがないため、置換しておかないと(スペースで区切られないため)意図しない情報も一緒に表示されるからです。 -
awk -F' ' '{print $10}'
ここまでの過程で、各行の、スペース区切りになった項目から 10 列目の値(ポート番号)を取得しています。
-
awk '!a[$0]++'
lsof -i
では同じポートが(IPv4 と IPv6 のぶんの)2回表示されるため、重複するポート番号をユニークな(一意の)番号にフィルターしています。具体的には
a[$0]++
で$a
の連想配列に次々と渡されるポート番号$0
をキー名として登録し、登録済みのキーがあった場合は++
で先へ進むという処理を!
で反転させることで、「同じキー名でなかった場合は次へ進む」という処理にしています。結果、ソートしないで重複行を削除することができます。こんなの普通は思い付かないですよね。
ランダムな番号の取得(範囲指定)
macOS(UNIX)や Linux などの場合、ランダムにアレをするには $RANDOM
が便利ではあるのですが、0 から 32767 までの間という制限があるため、65536 個あるポート数には足りません。
$RANDOM
を2回足すなどの工夫をしてもいいのですが、やはり PHP の rand( min, max )
関数 くらいシンプルに範囲指定したいのです。
そこで、まずは以下の様な $RANDOM
変数と剰余演算(%
,パーセント)を使った範囲指定可能な式を考えました。
MIN=50000
MAX=65535
PORT_RANDOM=$RANDOM % ($MAX + 1 - $MIN)
上記で $RANDOM の値がどんなにランダムな値であっても $MIN
から $MAX
の範囲で返ってくるので一見動いているように見えます。
しかし $MAX
が 32767 より大きい場合に偏りが出てしまいます。$RANDOM
の値が 0 〜 32767 の範囲でしか動かないためです。これは $((RANDOM+RANDOM))
などとしても(小さい値が出にくいという意味でも)同じでした。
有名な shuf
コマンドは macOS にはデフォルトで入っていないため、標準のシェル・コマンドで替わりとなるコマンドないかな、と調べてみると StackExchange でドンピシャの質問が。「jot
を使うと良いよ」とのこと。
調べてみたところ、macOS のシェル・コマンドには **jot
という「連番もしくはランダムな値を出力するコマンド」**が標準でインストールされています。標準で入っているのが良いところです。
jot
コマンドの基本的な使い方を調べてみると、引数を順に「X A B」とすると値 A から値 B の範囲で X 回出力してくれます。
$ # 5回 11 から 13 の値を取り出す
$ jot 5 11 13
11
12
12
12
13
$ # 5回 24 から 2 の値を取り出す
$ jot 5 24 2
24
18
13
8
2
一見ランダムに取得しているように見えるのですが、jot 100 1 10
とすると分かるように「与えられた範囲の数値のリスト(配列)を指定された回数にわけて順番に取り出している」だけなので、ランダムではありません。この動きを「シーケンシャル」(sequential
)と呼ぶそうです。
先の StackExchange のコメントを見ると -r
オプションを付けているので、jot
のマニュアルを見てみました。
The following options are available:
-r Generate random data instead of the default sequential data.
-r
オプションを付けるとランダムにしてくれるらしいので、以下のようにすると良い様に思えます。
$ jot -r 1 50000 65535
65310
こちらも、一見ランダムに取得しているように見えるのですが、実は、これには落とし穴があって、完全なランダムではなくシャッフルに近い動作をするようです。
マニュアル(man jot
)を読むと以下の様に書いてあります。
Random numbers are obtained through arc4random(3) when no seed is specified, and through random(3) when a seed is given.
When jot is asked to generate random integers or characters with begin and end values in the range of the random number generator function and no format is specified with one of the -w, -b, or -p options, jot will arrange for all the values in the range to appear in the output with an equal probability.
以下は筆者の意訳です。
「シード」が指定されている場合は
random(3)
、指定されていない場合はarc4random(3)
を使い、ランダム値を取得します。
jot
コマンドが-r
オプションによりランダムな出力を求められた時、開始と終了の範囲が指定され、かつ-w
,-b
,-p
の書式オプションが付かない場合は、出力される整数もしくは文字の出現率は均等になるように調整されます。
つまり、「一度出現した値は再出現しづらいという判断が出来る」ということであり、結果を絞り込めるため、完全なランダム値にならないということのようです。
そこで、-w
の書式オプションを使って、-w %i
で使用する文字に数値を指定して、そこから -r
オプションでランダムに取得するのが王道のようです。以下が最終的なサンプル。
MIN=50000
MAX=65535
jot -w %i -r 1 $MIN $((MAX+1))
以下は $RANDOM 変数と上記 jot
コマンドを 10 万回繰り返した際の出現回数をグラフに出力したものです。(X=回数, Y=出現した数値)
このように $RANDOM の方は出現する値が偏り、比べて jot
は広い範囲で出現することがわかります。
まぁ、たかがポート番号なのでそこまでシビアに乱数にこだわる必要はないのですが、気になってしまいました。おそらくこれ以上ランダム性にこだわるなら、他のプログラム言語を叩いて得るか、別途専用のコマンドをインストールした方がコストは安いのかもしれません。
参考文献
- awkで重複行を排除する方法 @ Qiita
- シェルスクリプトでランダムにアレをやる @ Qiita
- Generate random numbers in specific range @ StackExchange