localhostのポート競合を気にせずサーバーを起動する
Live With Linux::technique
- TOP
- Articles
- Live With Linux
- localhostのポート競合を気にせずサーバーを起動する
序
TCPはそもそもUnixにおいて内部的に使われてきたものである。 何の話かというと、システムでは良く知られているサーバー以外に、普通にPCを使っている上でまぁまぁサーバーが起動したりしているということだ。
私の手元の環境だと
❯ ss -tuna | grep LISTEN
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:631 0.0.0.0:*
tcp LISTEN 0 50 127.0.0.1:6342 0.0.0.0:*
tcp LISTEN 0 50 127.0.0.1:6341 0.0.0.0:*
tcp LISTEN 0 4096 [::1]:631 [::]:*
意外とそうでもなかったが、22
, 631
,
6341
, 6342
が使われていることがわかる。
22
は言わずと知れたsshdであり、631
はcupsd。
6341
と6342
はmegasyncである。1
最近はローカルでwebサーバーを起動する機会も多い。
webサーバーを起動するアプリケーションにすれば、簡単にGUIを用意できるのは大きなメリットだ。
だが、webサーバーはだいたい1080
とか8000
とか8080
とか8888
を使いたがるので、これらのポートは奪い合いになりやすい。
自分が書いているプログラム単独だと良くても、なんだかんだそのようなポート競合を気にしないといけない状況というのも出てくる。
この問題を解決する方法として、Linuxの伝家の宝刀とも言えるnamespaceを使う方法もあるが、そんなことをしなくても実は簡単に解決できる。
What is localhost?
Linuxとネットワークの基礎知識に関する話をしよう。
localhost
は直接解決可能なホスト名だ。/etc/hosts
を見ると
# Standard host addresses
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
のように書かれている。
つまり、localhost
はIPv4では127.0.0.1
に、IPv6では::1
に解決される。
IPアドレスはネットワークとホストの2セクションからなっている。 これはルーティングに絡んでいる。
例えば、192.168.1.0/24
とした場合、上位の24ビットがネットワークのアドレス、下位の8ビットがホストのアドレスであるという意味になる。
おっと、そもそもIPv4は32ビットの値を8ビットずつに区切って10進数で表し、.
でつないでいるという知識も必要だ。
192.168.1.0/24
の/24
はネットマスク255.255.255.0
に等しい。
ネットマスクはビットが1
である部分がネットワーク、0
である部分がホストを示す。
ネットマスクは途中で0
と1
が交じるような場合はネットマスク表記が必要になるが、普通は上位に1
が並ぶ構造なので、/N
の形で書ける。
IPアドレスはホストではなく、NICに対して与えられる。
NICのアドレスが192.168.1.10/24
であるとしよう。
この場合、宛先が192.168.1.5
だったとすると、宛先はそのNICが属しているネットワークに同じく属しているということになる。
そのため、192.168.1.10
のNICからパケットを出せば届いてくれるはずだ、ということで、192.168.1.5
宛てのパケットは192.168.1.10
から出ていく。
自分のNICがどれも所属していないネットワークが宛先になっている場合に配送を依頼するために使われる宛先がデフォルトゲートウェイだ。 なお、ルーティングテーブルを手動で定義すれば、それとは別に「この宛先の場合このNICから出す」というのを指定できる。
さて、ではlocalhost
の話に戻ろう。
127.0.0.1
や::1
はループバックアドレスといい、そのコンピュータ自身と通信するために予約されているアドレスになる。
コンピュータにはループバックを使うためのループバックインターフェイスという仮想NICが用意されている。
Linuxではlo
だ。
❯ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host noprefixroute
valid_lft forever preferred_lft forever
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether aa:aa:bb:cc:dd:ee brd ff:ff:ff:ff:ff:ff
altname enp8s0
inet 192.168.1.60/24 brd 192.168.1.255 scope global noprefixroute eno1
valid_lft forever preferred_lft forever
inet6 fe80::aaaa:bbbb:cccc:dddd/64 scope link noprefixroute
valid_lft forever preferred_lft forever
lo
というNICにはinet
(IPv4)として127.0.0.1/8
が、inet6
(IPv6)として::1/128
が割り当てられていることがわかる。
このため、localhost
というホスト名を指定すると127.0.0.1
に解決され、127.0.0.1
はlo
を経由して自分自身と通信する。
だが良く見てほしい。lo
のネットワークは/8
なのだ。
つまり、ループバックアドレスとしては127.0.0.1
だけでなく、127.0.0.1
から127.255.255.254
までの16777214個のアドレスが存在している。
普通のルールに則って考えれば127.0.0.1
でも127.255.255.254
でも、lo
を経由して自分自身と通信するため、そんなにたくさんのアドレスが存在する意味は全くない、ように見える。
実際、ループバックアドレスとして127.0.0.1
以外が使われることは全くないため、IPv6では::1/128
と1個だけになった。
ところが少なくともLinuxではそうではない。
ループバックアドレスは、lo
にバインドされているアドレスには関係なく、すべてのループバックアドレスが固有のループバックインターフェイスに結びついている。
(127.0.0.1
と::1
は同じNICである。)
そして、ポートはNICに対してバインドされる。0.0.0.0
を指定してすべてのNICに対してバインドすることはできるが、そもそもNICごとにポートが存在しているのだ。
もう一度ss
の結果を見てみよう。
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.1:631 0.0.0.0:*
22
ポートは0.0.0.0
に対してバインドされているから、すべてのNICからアクセスできる。
しかし、631
のほうは127.0.0.1
に対してバインドされているため、127.0.0.1
から入ってきた場合にしかつながらない。
そろそろ気づいただろうか。
そう、127.0.0.1:8000
と127.0.0.2:8000
は別物としてアクセスできるのだ。
実際に使う
次のスクリプトは、カレントディレクトリをルートとしてwebサーバーを起動するRubyスクリプトだ。
#!/usr/bin/ruby
require 'webrick'
= WEBrick::HTTPServer.new({ DocumentRoot: Dir.pwd,
srv BindAddress: 'localhost',
Port: 8000 })
trap("INT"){ srv.shutdown }
.start srv
この場合、webサーバーはlocalhost:8000
に対してバインドされるわけだが、言い換えれば127.0.0.1:8000
に対してバインドされる。そして、http://127.0.0.1:8000
でアクセス可能だ。
ではBindAddress
を変更してみよう。
#!/usr/bin/ruby
require 'webrick'
= WEBrick::HTTPServer.new({ DocumentRoot: Dir.pwd,
srv BindAddress: '127.0.0.2',
Port: 8000 })
trap("INT"){ srv.shutdown }
.start srv
今度は127.0.0.2:8000
に対してバインドされるようになった。
こうなると、http://127.0.0.1:8000
やhttp://localhost:8000
でアクセスすることはできず、http://127.0.0.2:8000
でのみアクセスできる。
これでも衝突する可能性はゼロではないが、127.0.0.1
以外のループバックアドレスを使う例はほとんどないこと、そしてポート番号と比べ膨大な数が存在することから、衝突を避けるのはずっと容易だ。
localhost
は127.0.0.1
あるいは::1
に解決されるため、127.0.0.2
を表すことはできない。
アドレスを変えたためにアドレスで表記することになるのは嫌、と考えるかもしれない。
その場合は/etc/hosts
に書いておくといい。
127.0.0.2 myapp
これでhttp://myapp:8000
というアドレスでアクセスできるようになった。
ループバックアドレスは外部ホストからは完全に分離されているため、そのマシン固有のこととして考えて良いため、そのホストの/etc/hosts
に書けばよい。
ただ、配布するスクリプトにbind
addressとしてmyapp
と書くことはできない。
他のホストはその名前でループバックインターフェイスが解決できないからだ。
だが、そもそもbind addressに書くアドレスで名前解決させるのは、そもそもあまりよろしくない。
注意点
127.0.0.1
と127.0.0.2
が別のインターフェイスになっているのは、あくまでLinuxの挙動なので、他のOSだとそうではないかもしれない。
なお、仮想NICを増やして同じようなことをすることもできるが、Linux的にはループバックアドレスを使い分けるのが事前設定も不要で圧倒的に簡単だ。
おまけ
PureBuilder
Simplyは現状、テストサーバーを127.0.0.1
に固定でバインドする仕様だが、手元のブランチではtestserver_address
によって指定できるようになっている。
127.0.0.2
を指定できるだけでなく、0.0.0.0
を指定して他のホストからアクセスすることもできるようになった。