Raspberry Piでソケット通信事始め。受信側を作ろう(Python、socket)

 Raspberry Piは色々な楽しみ方が出来る素敵なミニPCです。趣味だけでなく産業的にも3Dプリンタやロボットなど様々な物に搭載されています。ネットワーク接続も可能で、有線LANでもOKですしWiFiも搭載されています(※ラズパイの機種によります)。

 とするとですね「ネットワークロボットとか外部機器を遠隔操作してぇ」って思うんです(強引w)

 ネットワークを介して別のPCとやりとりする方法は多分色々ありますが、今回はその一つ「ソケット通信」を見て行こうと思います。ソケット通信自体は古くからある技術ではありますが、もちろん現役バリバリ。少なくとも僕の生業であるゲーム開発のネットーワーク通信の多くはソケット通信によってその根幹が支えられています。

ソケット通信に必要なIPアドレスとポート番号

 仕組みを深く突っ込むとややこしい事になってしまいますので超ざっくり言うと、ソケット通信とは「受信待ちしている相手に対して任意のデータを送りつける」というシンプルな仕組みです。

 ざっくりは上で十分なのですが、実際にプログラムを組むためにはもう少し概念を掘り下げる必要があります。

 データを投げる送信側(クライアント)はどうやって相手(サーバー)を指定するのか?これはIPアドレスを指定します。IPアドレスは言わずもがなですが、ネットに接続されている機器を識別する一意の番号ですね。良く「ネット上の住所」とイメージされます。

 ただIPアドレスは「○○県○○市○○町1-2-3 コーポマルペケ」くらいまでの住所を表しています。惜しい、アパートの建物名(=機器本体)までわかっているのに部屋番号(=受け手のアプリケーション)が無い。これだと郵便屋さんは送り先を特定できないんですね。この部屋番号を担当するのがポート番号です。IPアドレスとポート番号がワンセットになって初めて情報が機器の中のアプリケーションまで届きます。

 ポート番号は0~65535番までをアプリケーション内である程度自由に指定する事が出来ます。ただし0~1024番までは「ウェルノウンポート(well-known port:よく知られているポート番号)」と呼ばれ、世界的に使用される通信サービスなどで使用されているため避けた方が面倒を避けられます。まぁ5桁の数字を使っておけば確実でしょう。

ルーターの設定(必要ならば)

 IPアドレスとポート番号がわかればソケット通信できます。イントラネット(家の中だけのネット)であればこれで十分。でもインターネット(世界中の通信)を介して通信する場合はルーターの設定が必要になる場合があります。

 ルーターはインターネット通信をする窓口として働いている機器です。プロバイダーさんなどから提供されていると思います。名前の通り通信のルートを管理してくれています。世界に公開されるIPアドレスはルーターのIPアドレスでして、これを指定すればルーターまで情報は届きます。

 しかし家の中(イントラネット)の個々の機器には「ローカルIPアドレス」という家の中だけで通用するIPアドレスが割り振られているため、お使いの機器に外部から情報を直接届けられないんです。

 外部からイントラネットにある機器に情報を届けるには、ルーターの設定で「ポート番号○○に届けられた情報はイントラネットの△△さんへポストします」という「ポート設定(ポートワインディング)」をする必要があります。ポート設定の方法はお使いのルーターによって異なりますので、インターネットを介したソケット通信をお考えの場合はこの辺りを設定下さい。

 イントラネット内であっても、ルーターが特定のポート番号以外を許可しない設定になっている場合もあります。その場合はルーターの設定でお使いになりたいポート番号を許可するよう変更して下さい(セキュリティーにはご注意を)。

 さて、ちょっと長くなってしまいましたが、ソケット通信、始めましょう(^-^)

Pythonでの受信側プログラム

 「始めましょう~」と言いつついきなりモチベを下げるようで恐縮なのですが、以下のプログラムはソケット通信を体験するに十分ですが実用では使えませんので注意下さい。ただベースとなる所は実用でも一緒でして無駄ではありません。実用って色々面倒なわけでして、そういう所は追々記事化していこうと思います。まずは別の機器から情報が送られる楽しさを体験してみましょう。

ソケットオブジェクトを受信可能にする

 ソケット通信は情報を送信する側(クライアント)と受ける側(サーバー)に分かれます。まずは情報を受信する側(サーバー)を作ります。ソケット通信は様々な言語でサポートされていて、C言語などでも作れますが、ここでは利便性を取ってPythonを採用します。サーバー機のOSとしてRaspberry Pi OSを想定しますが、別にWindowsでもMacでもUbuntu構いません。幅広いOSでささっと動くのがPythonの強みですね。

 作るのはクライアント側から送られた文字列データをコンソールにそのまま表示するシンプルなサーバーです。送信側から空文字を受信したらプログラムを終了します。

 ソケット通信周りのコードを利用するにはsocketライブラリをインポートします:

import socket     # ソケット通信モジュール

これは送受信双方で共通です。

 次にソケット通信をするオブジェクトを作成します:

# IPv4形式のソケット通信を行うオブジェクトを作成
sv = socket.socket( socket.AF_INET )

socketライブラリ内のsocketクラスのコンストラクタで作成できます。引数にあるsocket.AF_INETというのは「IPアドレスを用いた通信をするよ」という指定です。ライブラリの実際の定義を引用すると、以下のものが設定可能です:

AF_INET = AddressFamily.AF_INET
AF_INET6 = AddressFamily.AF_INET6
AF_APPLETALK = AddressFamily.AF_APPLETALK
AF_DECnet = AddressFamily.AF_DECnet
AF_IPX = AddressFamily.AF_IPX
AF_SNA = AddressFamily.AF_SNA
AF_UNSPEC = AddressFamily.AF_UNSPEC

 何がどれなのかさっぱりかもしれませんね(^-^;。AF_INETがIPv4、AF_INET6がIPv6に該当します。まぁ99%はAF_INETかなと。皆さんが良く知る192.168.11.15みたいな4つの数字が並ぶIPアドレスはIPv4ですので、コンストラクタでその方式を指定しています。

 次にsocket.bindメソッドに自身のIPアドレスと使用したいポート番号を指定します。これにより住所を確定しているわけです:

# IPアドレスとポートをバインドしてソケットを受け付ける
# 引数は(IPアドレス, ポート番号)のタプル
sv.bind( ( "192.168.11.16", 15769 ) )

 上の例では僕が実際使用しているRaspberry PiのローカルIPアドレスとポート番号15769(適当)を指定しています。ラズパイのIPアドレスはデスクトップ右上にある電波マークで確認出来ます:

 最後に受信受付を開始するためにlintenメソッドを呼び出します:

# 受信受付開始
sv.listen()

listen、つまり「耳をそばだてて聞きますよ~」という事ですね。

受信受付状態にする

 ここまで準備が出来たらもう外部からのソケットを受信できます。受信するには受信待ちをする必要があります。それにはacceptメソッドを呼び出します:

    # 任意のクライアントからデータ受信を待つ
    client, addr = sv.accept()

 acceptメソッドを上のように呼ぶと、プログラムはここでずーっと待機します。クライアントから何かソケットが飛び込んでこない限りずっとです。こういうプログラムがそこで待機して先に進まない状態を「ブロッキング」と言います。この節の冒頭で「実用的では使えない」と言ったのは実はこのためです。ここでプログラムが留まってしまうと、他に何にも出来ないからなんですね。もちろん世の中には色々な回避方法がありますが、それは追々。

 acceptで待機してソケットが飛び込んで来たとしましょう。するとプログラムは再び進みだし、戻り値に2つの情報が格納されます。一つはソケットを送信したクライアントの情報が入ったsocketオブジェクト(client)です。もう一つはクライアントのIPアドレスでaddrに格納されます。

 実際に受け取ったデータ(バイナリデータ)はこのclientのrecvメソッドでコピーして受け取る事が出来ます:

data = client.recv( 1024 )

 引数はコピー先の最大バッファサイズです。これ実際は送信側がどんなサイズのデータを送ってくるか分からないわけで、指定するに難しいんですよね。となると「んじゃ1000000000とかしておけばいいんじゃね?」と考えたくなりますが、それは良くないんです。このバッファサイズは受け取る度に確保されます。大き過ぎるバッファにすると確保するにもコピーするにもコストがかかり負荷増大になってしまいます。Pythonのリファレンスによると2の累乗の小さめの値に設定すべきとありまして、2048(2KB)とか4096(4KB)程度が適切のようです。

 recvメソッドの戻りとして受け取ったdata変数はbytes型です。bytes型というのはPython版のバイト配列で、要はバイナリデータがごそっとまんま格納されている配列です。送られたのが文字列なら文字列の、jpegだったらjpegのバイナリがそのまま格納されています。今回はこれが文字列だという前提にしています。その場合Pythonが文字列だと認識できるように変換する必要があります:

str = data.decode("utf-8")

 上の例ではbytes型が持つdecodeメソッドに"utf-8"と指定しています。こうするとバイト配列がUTF-8形式の文字列だとして、それをPythonが扱える文字列(string型)に変換してくれます。

 Pythonが認識可能な文字列に変換すれば、print関数でそれをコンソールに出力出来ます:

print( str )

 今回受信データが空文字の場合はプログラムを終了する仕様があります。bytes型のデータバイト数はlen関数で得る事が出来ます:

# バイトサイズを取得
dataSize = len( data )
if dataSize == 0:
   # 終了へ

 これで受信とデータ表示はばっちりです。

後処理が大切

 acceptメソッドによって受け渡されたclient変数(socketオブジェクト)は使い捨てのテンポラリです。データをコピーした後は基本使い道がありません。使い終わったclient socketオブジェクトはcloseメソッドで閉じて下さい:

# クライアントソケットを閉じる
client.close()

 クライアント側のソケットを閉じないと、サーバーとして作ったsocketオブジェクト(sv)はそのクライアントソケットがまだ使用中だと判断し、次の受信で別のクライアントソケットを消費してしまいます。このクライアントソケットの数は上限があるため、いずれ底を尽きてaccesptメソッドで受信不可能になってしまいます。closeしないでいるとリソースも無駄に食い潰すため、早々にcloseして下さい。

受信側コード全容

 という事で受信側のコードは以下のようになります:

import socket  # ソケット通信モジュール

sv = socket.socket( socket.AF_INET )
sv.bind( ( "192.168.11.16", 15769 ) )
sv.listen()

while True:
    client, addr = sv.accept()
    data = client.recv( 1024 )
    if len(data) == 0:
        break
    print( data.decode("utf-8") )
    client.close()

 こんな短いコードでネットワークを介してデータが受信できるのですから便利なもんです。

送信側コードは次回に

 一方の送信側コードですが、ちょっと記事が長くなってきましたので次回に持ち越します。送信できると俄然面白くなりますのでお楽しみに。

ではまた(^-^)/

<次回>


この記事が気に入ったらサポートをしてみませんか?