子育てしながらエンジニアしたい

現在 7 歳 女の子の子育て中エンジニアによる、技術系 + 日常系ブログ。

Python で自身の IP アドレスを取得したい - Windows 編

[2019/07/17 追記]
本記事は Windows でのみ動作します。
Ubuntu 編を改めて記載しましたので、Ubuntu の方は以下の記事をご覧ください。

edosha.hatenablog.jp


低レイヤーのネットワークプログラミングをしていると、自身の IP アドレスを取得したいことがよくあります。
ところが Python では、これを簡単に取得するということができませんでした。

Stack overflow でもこれが議論になっており、一番簡単なのは外部ライブラリ "netifaces" を使うという結論でした。
stackoverflow.com

でも、移植性を考えると、できれば外部ライブラリは使いたくありません。
なんかないかな~と探していたら、標準ライブラリでいけそうな方法を見つけたので紹介します。

Python socket モジュールで自身の IP アドレスを取得する

実行環境は Python 3.5.3 / Windows 10 です。
標準ライブラリの socket モジュールを使います。

$ python --version
Python 3.5.3 :: Anaconda 4.4.0 (64-bit)

IP アドレスを取得する socket API

以下の 2 種類があります。

  • socket.gethostbyname(hostname)
    • ホスト名を '100.50.200.5' のようなIPv4形式のアドレスに変換します。
  • socket.gethostbyname_ex(hostname)
    • (hostname, aliaslist, ipaddrlist) のタプルを返し、 hostname は ip_address で指定したホストの正式名、 aliaslist は同じアドレスの別名のリスト(空の場合もある)、 ipaddrlist は同じホスト上の同一インターフェースのIPv4アドレスのリスト(ほとんどの場合は単一のアドレスのみ)を示します。

どちらも引数は hostname です。
つまり、自身の hostname を取得できれば、上記の API に渡すことで IP アドレスを取得できるということです。

hostname を取得する socket API

そのまんまです。

上記を組み合わせて、自身の IP アドレスを取得する

まずは自身の IP アドレスを確認してみます。

$ ipconfig | grep IPv4
ipconfig | grep IPv4
   IPv4 Address. . . . . . . . . . . : 192.168.1.10
   IPv4 Address. . . . . . . . . . . : 192.168.56.1
   IPv4 Address. . . . . . . . . . . : 10.150.2.139

3 つもありました。
1 番上は有線、2 番目は Virtual Box の仮想アダプタ、3 番目は無線です。

では、Python コンソールで socket API を使ってみましょう。

>>> import socket
>>> socket.gethostname()
'edosha_notepc'
>>> socket.gethostbyname(socket.gethostname())
'192.168.1.10'
>>> socket.gethostbyname_ex(socket.gethostname())
('edosha_notepc', [], ['192.168.56.1', '10.150.2.139', '192.168.1.10'])

gethostbyname で有線の IP アドレス、gethostbyname_ex で全ての IP アドレスが取得できました。

私のように 3 つもアドレスを使うのでなければ、gethostbyname でよさそうです。
どれかを選ばせたいときは gethostbyname_ex が有効かもしれません。

Python: マルチプロセスで並列処理をさせるには multiprocessing.Pool が超便利

独立した並列処理を高速化したい

ふつう並列処理といえば、GUI と処理を切り離すなどが思い当たります。
GUI と処理を切り離すといっても、相互にメッセージなどをやり取りして協調動作する必要があります。

今回やりたいのは、そういう協調動作を必要としない、独立した並列処理です。
たとえば、大量のファイルをすべて別個に圧縮するという場合、それぞれのファイルは独立に処理することができます。
これはスレッドを分けても意味が無いので、プロセスを分けて処理する必要があります。
こんなときに、Python の multiprocessing.Pool がとても便利だったので記録として残しておきます。

課題

20 個の処理をマルチプロセスで並列処理したいとします。
CPU がクアッドコアであれば、4 個を同時に走らせることができます。
普通に考えると、プロセスの終了状況を監視して、終了したら次のタスクを割り当てるという流れが必要になります。
実際にこういう方法でプログラムを書いていたのですが、これはけっこう面倒でミスをしやすいプログラムでした。

multiprocessing モジュールの Pool クラスは、なんとこれを 1 行でやってくれます。
はじめから知っておきたかった...

サンプルプログラム

1 秒かけて与えられた数字の 2 乗を返すだけのサンプルプログラムです。
数字は range(20) で与えるようにしていますが、ここはリストなどでも大丈夫です。

出力結果は以下のようになります。

param 0 is being processed
param 2 is being processed
param 4 is being processed
param 6 is being processed
param 1 is being processed
param 3 is being processed
param 5 is being processed
param 7 is being processed
param 8 is being processed
param 10 is being processed
param 12 is being processed
param 14 is being processed
param 9 is being processed
param 11 is being processed
param 13 is being processed
param 15 is being processed
param 16 is being processed
param 18 is being processed
param 17 is being processed
param 19 is being processed
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361]

プロセスを並列で走らせ、返り値は順番どおりに返してくれる、ほんとに便利なモジュールです!

注意点としては、Pool.map は range(20) の値を 0 から順番に与えるわけではない、ということです。
出力結果を見ると、0,2,4,6 が最初に来ていることがわかります。
この順番を 1 ずつにしたい!という場合は、Pool.map にもう一つ引数を追加する必要があります。

results = pool.map(parafunc, range(20), 1)

この最後の引数 "1" は chunksize と呼ばれる引数で、まとまりを意味するようです。
これを 1 にすると 0,1,2,3,...、2 だと 0,2,4,6,...、3 なら 0,3,6,9... という順番で値が渡されます。
なお、ここを変えたとしても、渡される結果の順番は同一なので安心です。

Raspberry pi を NAT ルーター兼 DHCP + DNS サーバーにしたい

やっぱり外にもつなぎたい...

前回の記事で、クローズネットワーク内に Raspberry pi を置いて、DHCP + DNS サーバーにしました。

edosha.hatenablog.jp

当然ながらクローズネットワークなので、外には出られません。
しかし開発をしていると、どうしても外につなぎたいことがあります。

pip とか apt-get とか...

そのときにいちいち回線をつなぎ直すのは面倒です。
なので、前回使った Raspberry pi に NAT ルーターの機能を追加することにしました。

ネットワークの完成イメージ

f:id:edosha:20170706112816p:plain:w500

すでに外につながる PPPoE ルーターがあり、その配下に Raspberry pi を置くことにします。
そのまた下にクローズネットワークを構築します。

このとき、Raspberry pi 配下にある端末へのアクセスに対しては Raspberry piDNS サーバーとして動作し、
そうでない端末 (google.com とか) には、Google DNS (8.8.8.8) を使うことにします。

必要な部品

Raspberry piEthernet の口が一つしかありません。
しかし上記のようなネットワークを組むには、LAN 側と WAN 側の 2 つの Ethernet が必要になります。

そこで、WAN 側には USB-Ethernet 変換を使うことにしました。
下記の製品であれば、ドライバーがすでに Raspbian の中に入っているので、これにしました。

自分の環境では、USB ポートにさすだけで eth1 として認識されました。

Raspberry pi の設定

それでは Raspberry pi の設定をします。
前回設定した Dnsmasq の一部書き換えと、IP 転送の設定をします。

Dnsmasq の書き換え

一行だけ書き換えれば OK です。
DHCP でアドレスを配るときに、アクセスすべき DNS サーバーのアドレスを追加するようにします。

/etc/dnsmasq.more.conf を開いて、前回の記事では以下のようになっているところを...

# DHCPクライアントに通知するDNSサーバのIPアドレス
dhcp-option = option:dns-server, 192.168.0.1

192.168.0.1 のあとに 8.8.8.8 を追加するだけです。

# DHCPクライアントに通知するDNSサーバのIPアドレス
dhcp-option = option:dns-server, 192.168.0.1, 8.8.8.8

そして Dnsmasq サービスを再起動しましょう。

$ sudo service dnsmasq restart

IP 転送の許可

Stack overflow の回答を参考にしました。

raspberrypi.stackexchange.com

まずは /etc/sysctl.conf を開きます。

$ sudo emacs /etc/sysctl.conf

この中で、#net.ipv4.ip_forward=1 となっているところがあるので、その # を消します。

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward=1  # <-- コメントアウトを解除する

次に、以下のように IPv4 の転送を有効にします。

$ sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"

iptables の設定

最後に iptables の設定をします。
なお以下に記載する設定は、eth0 がクローズネットワーク側、eth1 が外側なので、ご留意ください。

まず iptables に残っているエントリを全部消去します。

$ sudo iptables -F
$ sudo iptables -X
$ sudo iptables -t nat -F
$ sudo iptables -t nat -X
$ sudo iptables -t mangle -F
$ sudo iptables -t mangle -X
$ sudo iptables -P INPUT ACCEPT
$ sudo iptables -P FORWARD ACCEPT
$ sudo iptables -P OUTPUT ACCEPT

次に eth0 - eth1 間の通信を有効にします。

$ sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
$ sudo iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
$ sudo iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE

これで、外側への通信ができるようになったはずです。

このままではリブートしたあと設定が消えてしまうので、この設定をセーブし、起動時に読み込めるようにします。
まず、設定をファイルに書き出します。

$ sudo sh -c "iptables-save > /etc/iptables.ipv4.nat"

このファイルを起動時に読み込むようにするために、/etc/rc.local の exit 0 の上に以下の記述を追加します。

sudo iptables-restore < /etc/iptables.ipv4.nat

これで再起動しても大丈夫になりました。