Socket通信

Pythonには必要になりそうなモジュールが一通り揃っていて、CGIサーバやTelnetクライアントが簡単に書ける。でもRaspberry Piでネットワークと外部ハードウェアを絡めてなにかしたいと思った時は、高度すぎる機能は使いにくい場合もあって、その時にはもっと低レベルなプログラムが必要になる。

ここで言う低レベルというのは稚拙という意味ではなく、OSに近い機能を使ったプログラムという意味。

Pythonで低レベルなSocket通信のサンプルを探すと、ソケットをオープンしたあと接続やデータ受信をそこで待ってしまう、いわゆるブロッキングな使い方の例が多く、通信と他の制御を平行して行いたい場合には向かない。

その場合はスレッドを使うのがエレガントでモダンな手法だが、それを試す前にUnixから引き継がれてきたレガシイな方法を試してみた。プログラムは以下の通り。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import socket
import select
import Queue
import sys

def socketio(sock):
    ilst = [sock, sys.stdin]
    olst = []
    ique = Queue.Queue() 
    oque = Queue.Queue() 
    connect = False
    while True:
        # socket i/o
        rlst, wlst, xlst = select.select(ilst, olst, [], 0)
        for si in rlst:
            # accept?
            if si == sock:
                if not connect:
                    # connect
                    conn, address = si.accept()
                    print 'connect', address
                    connect = True
                    ilst.append(conn)
                    olst.append(conn)
                else:
                    # discard
                    conn, address = si.accept()
                    conn.shutdown(socket.SHUT_RDWR)
                    conn.close
            elif si == sys.stdin:
                data = sys.stdin.readline()
                oque.put(data)
            else:
            # recv?
                data = si.recv(1024)
                if data:
                    ique.put(data)
                else:
                    # disconnect
                    print 'disconnect'
                    connect = False
                    ilst.remove(si)
                    olst.remove(si)
                    si.close
                    with ique.mutex:
                        ique.queue.clear()
                    with oque.mutex:
                        oque.queue.clear()
        for so in wlst:
            if not oque.empty():
                # send
                data = oque.get_nowait()
                so.send(data)
        # test
        if not ique.empty():
            data = ique.get_nowait()
            print data
                        
def start(host, port):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind((host, port))
        sock.listen(1)
        socketio(sock)
    except KeyboardInterrupt:
        print '\nbreak'
        sock.shutdown(socket.SHUT_RDWR)
        sock.close()

if __name__ == "__main__":
    start('', 5555)
pi@raspberrypi ~/PythonProjects $ sudo python socket1.py

でプログラムを起動して、他のPCからtelnetクライアントでRaspberry PiのIPアドレスのポート5555に接続する。Windowsの場合ならコマンドプロンプトから

C:\>telnet 192.168.1.9 5555

を実行。
あとはそれぞれの端末から入力した文字が、接続先の端末に表示される。Raspberry Pi側は一行単位の入力を使っているので、改行を入力した時点で送信。

Raspberry Pi側は^C(Ctrl+C)で終了。WindowsのtelnetクライアントはCtrl+]を押してからquitで終了させる。
socket1

Socketとコンソールからの入力待ちはselectを使っている(待ち時間ゼロなので実際には待たない)。使い方が分かり難いという人もいるけど、基本的には引き渡したディスクリプタのリストに対して、アクティブになっているディスクリプタのリストを返してくるだけなので、返ってきたリストの中身を見てそれぞれの処理を行えば良いだけだ。

C言語のselectは引き渡したリストに対して上書きで結果を返してくるので、毎回リストを設定する必要があって不便だったのだが、Pythonでは引数と別に結果が返ってくるからその必要が無い。

Socketとコンソール入出力の間は同期キュークラスでデータを受け渡し、非同期に動作するようになっている。

このプログラムではひとつのクライアントからの接続を受け付けたら、それ以外からの接続は拒否(受け付けて即切断)するようにしてある。Raspberry Piに接続したデバイスをネットワーク経由で制御する場合は、一対一の接続で間に合う場合が多いと思うので、これでも問題ない。

複数クライアントからの接続を受け付ける場合は、拒否せずにSocketをディスクリプタのリストに付け加えて、切断されたら取り除くようにすれば良い。同時にキュークラスの管理も必要となって煩雑になってしまうので、そのあたりは次の機会に。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中