PythonでWebSocketのお勉強
最近はおもちゃづくりばっかりやっていましたが、超超超久しぶりに、基礎勉強のお話です。自分の本業の兼ね合いもあって、PythonでWebSocketを扱えるようにしておきたいと思います。いずれクラウド連携するおもちゃを作るときに役立つかもしれませんし。たまにはこういうこともやらないと、できることの幅が広がらないのです。
「そもそもWebSocketとは何ぞや?」という方は、以下の方がシンプルにまとめてくださっているので、ご参照ください。
自分はおぼろげに「WebSocketってプロトコルのレイヤ的にはHTTPの上位なんだっけ??」とか思っていたのですが、そうではなくて、レイヤ的にはHTTPと同列で、最初のハンドシェイクの部分だけをHTTPと同じ形にして、途中でプロトコルを切り替えるような感じなんですね。そうすることによって、エンドツーエンドの間の機器たちにはあたかもただのHTTP通信が始まったかのように見えるという。なるほどなー。考えた人はすごいなあ。。。
さて、今回の検証にはRaspberry Pi 3を使おうと思いますが、Raspberry Pi + Python + WebSocketについては、既に先人の方が情報を公開してくださっています。
以下では上記の内容をベースにさせて頂きつつ、サンプルコードを自分の勉強を兼ねてちょっとアレンジしていきたいと思います。
まずはサーバ側のプログラムの準備。今回は同一PC(Raspberry Pi)の中で、サーバとクライアントを通信させます。
以下のモジュールをインストールします。
$ sudo pip install git+https://github.com/Pithikos/python-websocket-server
普通に”sudo pip install websocket-server”でもインストールできるみたいですが、公開元によれば、後者のやり方だとソースが最新ではないかもしれないとのことなので、このやり方でインストールしています。
ソースコードは公開元のサンプルをベースに以下のようにしてみました。
import logging from websocket_server import WebsocketServer logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(' %(module)s - %(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) # Callback functions def new_client(client, server): logger.info('New client {}:{} has joined.'.format(client['address'][0], client['address'][1])) def client_left(client, server): logger.info('Client {}:{} has left.'.format(client['address'][0], client['address'][1])) def message_received(client, server, message): logger.info('Message "{}" has been received from {}:{}'.format(message, client['address'][0], client['address'][1])) reply_message = 'Hi! ' + message server.send_message(client, reply_message) logger.info('Message "{}" has been sent to {}:{}'.format(reply_message, client['address'][0], client['address'][1])) # Main if __name__ == "__main__": server = WebsocketServer(port=12345, host='127.0.0.1', loglevel=logging.INFO) server.set_fn_new_client(new_client) server.set_fn_client_left(client_left) server.set_fn_message_received(message_received) server.run_forever()
ローカルホストのポート12345番で待ち受けるようにします。
次に、クライアント側です。こちらは、以下のモジュールをインストールします。
$ sudo pip install websocket-client
公開元ではコネクションをすぐ切断する版と長く保つ版の二種類のサンプルソースが用意されているので、それぞれをベースに二種類作ってみます。まずはすぐ切断する版。
from websocket import create_connection import logging logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(' %(module)s - %(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) ws = create_connection("ws://127.0.0.1:12345") logger.info("Open") logger.info("Sending 'Hellow, World'...") ws.send("Hello, World") logger.info("Sent") logger.info("Receiving...") result = ws.recv() logger.info("Received '{}'".format(result)) ws.close() logger.info("Close")
続いて長く保つ版です。
import websocket import thread import time import logging logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(' %(module)s - %(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) # Callback functions def on_message(ws, message): logger.info('Received:{}'.format(message)) def on_error(ws, error): logger.info('Error:{}'.format(error)) def on_close(ws): logger.info('Close') def on_open(ws): def run(*args): logger.info('Open') for i in range(3): time.sleep(1) message = "Hello " + str(i) ws.send(message) logger.info('Sent:{}'.format(message)) time.sleep(1) ws.close() logger.info('Thread terminating...') thread.start_new_thread(run, ()) # Main if __name__ == "__main__": #websocket.enableTrace(True) websocket.enableTrace(False) ws = websocket.WebSocketApp("ws://127.0.0.1:12345", on_message = on_message, on_error = on_error, on_close = on_close) ws.on_open = on_open ws.run_forever()
以上で準備ができましたので、動作確認していきます。一つのシェルでサーバ側をバックグラウント実行してからクライアント側を実行してもいいのですが、サーバ側とクライアント側のログがごっちゃになって見にくいので、素直にRaspberry Piに別々のシェルでログインしておきます。
まずはサーバ側を実行します。
$ python server.py INFO:websocket_server.websocket_server:Listening on port 12345 for clients..
ポート番号12345で待ち受けが始まりました。続いて、クライアント側の方のシェルで「コネクションをすぐ切断する版」を実行します。
$ python client_short.py client_short - 2017-10-10 16:28:06,592 - INFO - Open client_short - 2017-10-10 16:28:06,592 - INFO - Sending 'Hello, World'... client_short - 2017-10-10 16:28:06,593 - INFO - Sent client_short - 2017-10-10 16:28:06,593 - INFO - Receiving... client_short - 2017-10-10 16:28:06,594 - INFO - Received 'Hi! Hello, World' client_short - 2017-10-10 16:28:06,596 - INFO - Close
ログ出力を見ると、”Hello, World”という文字列の送信に対し、サーバ側からは頭にHi! “を足して”Hi! Hello, World”という文字列が返ってきていることがわかります。
一方、サーバ側の方もログが出力されています。
$ python server.py INFO:websocket_server.websocket_server:Listening on port 12345 for clients.. server - 2017-10-10 16:29:06,313 - INFO - New client 127.0.0.1:51114 has joined. INFO:__main__:New client 127.0.0.1:51114 has joined. server - 2017-10-10 16:29:06,316 - INFO - Message "Hello, World" has been received from 127.0.0.1:51114 INFO:__main__:Message "Hello, World" has been received from 127.0.0.1:51114 server - 2017-10-10 16:29:06,317 - INFO - Message "Hi! Hello, World" has been sent to 127.0.0.1:51114 INFO:__main__:Message "Hi! Hello, World" has been sent to 127.0.0.1:51114 INFO:websocket_server.websocket_server:Client asked to close connection. server - 2017-10-10 16:29:06,319 - INFO - Client 127.0.0.1:51114 has left. INFO:__main__:Client 127.0.0.1:51114 has left.
…何か同じようなログが重複して出力されていますね。loggingモジュールについては自分も勉強を始めたところですが、おそらく、サーバモジュールの方でルートロガーに対してbasicConfigを設定してしまっているのが原因ではないか、と思います。ともあれ、ログを見る限りは、こちらも問題なく動作していることが確認できます。
続いて、「コネクションを長く保つ版」のクライアントを実行してみます。
$ python client_long.py client_long - 2017-10-10 16:36:15,621 - INFO - Open client_long - 2017-10-10 16:36:16,624 - INFO - Sent:Hello 0 client_long - 2017-10-10 16:36:16,626 - INFO - Received:Hi! Hello 0 client_long - 2017-10-10 16:36:17,626 - INFO - Sent:Hello 1 client_long - 2017-10-10 16:36:17,628 - INFO - Received:Hi! Hello 1 client_long - 2017-10-10 16:36:18,629 - INFO - Sent:Hello 2 client_long - 2017-10-10 16:36:18,631 - INFO - Received:Hi! Hello 2 client_long - 2017-10-10 16:36:19,731 - INFO - Close client_long - 2017-10-10 16:36:19,732 - INFO - Thread terminating...
こちらも問題ないですね。ちなみに、ソースコードの39〜40行目のwebsocket.enableTraceの引数をFalseからTrueにすると、websocket-clientモジュールのログが出力されるようになり、最初のHTTPを利用したコネクション確立のやりとりが見られるようになります。
$ python client_long.py --- request header --- GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:12345 Origin: http://127.0.0.1:12345 Sec-WebSocket-Key: CYoIi4sCzP6gvbyBQLGnnQ== Sec-WebSocket-Version: 13 ----------------------- --- response header --- HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: 9x8d0pd5uCYN4d9GJ1JffH+aSdc= ----------------------- client_long - 2017-10-10 16:42:02,020 - INFO - Open send: '\x81\x87\x11.y\xd1YK\x15\xbd~\x0eI' client_long - 2017-10-10 16:42:03,025 - INFO - Received:Hi! Hello 0 client_long - 2017-10-10 16:42:03,026 - INFO - Sent:Hello 0 send: '\x81\x87\x0e\x11\x99\xd0Ft\xf5\xbca1\xa8' client_long - 2017-10-10 16:42:04,031 - INFO - Received:Hi! Hello 1 client_long - 2017-10-10 16:42:04,031 - INFO - Sent:Hello 1 send: '\x81\x87\x02\x7f\x13\x96J\x1a\x7f\xfam_!' client_long - 2017-10-10 16:42:05,036 - INFO - Received:Hi! Hello 2 client_long - 2017-10-10 16:42:05,037 - INFO - Sent:Hello 2 send: '\x88\x82\xba)\xa4L\xb9\xc1' client_long - 2017-10-10 16:42:06,052 - INFO - Thread terminating... client_long - 2017-10-10 16:42:06,140 - INFO - Close
というわけで、同一ローカルマシン内での検証ですが、簡単にWebSocketをテストしてみました。今回はクライアント側からの送信要求に対してメッセージを返しているだけなので、動作としてはHTTPのリクエストとレスポンスとあんまり変わらないように見えるかもしれません。WebSocketのプロトコル自体は、一度クライアント側から接続してしまえば、後はサーバ側から自由にデータをプッシュできるようになっているので、必要に応じて、今回のサンプルをベースに拡張していけば良いかと思います。
ディスカッション
コメント一覧
シンプルで読みやすい記事でした。
PythonでWeb Socketをやらなければいけない作業があり、
無駄のない記事がとてもよかったです。
適度なコメントによるコマンド説明も含めて。
Rock8123様
ありがとうございます。今の自分としてはとても珍しいオモチャづくり以外の記事なので、あまり見ている人はいないかなあと思っていたのですが、お役に立てたようなら良かったです◎
わかりやすい記事をありがとうございます。
参考にさせていただきました。
一つ質問なのですが、ファイル送信をWebsocketで実施したいときはどのように書き換えを行えばよいでしょうか。可能であれば、JSON形式を送受信したいと考えております。
初心者故に拙い質問で申し訳ありませんが、よろしくお願いします。
mae様
すみません、随分前の記事で、かつ最近自分がWeb技術界隈から離れてしまったため、お答えすることができません。。。申し訳ありません。
直感ですが、JSON形式であれば結局のところ文字列として表現できるはずハズですので、とりあえず送信側はJSONファイルを読み込んで文字列として送信、受信側は受け取った文字列をJSONとしてパース、とすれば、やりとり自体はできそうな気がします。特に調べずに回答してしまっているため、不正確であれば申し訳ありません。
ピンバック & トラックバック一覧
[…] こちらは、PythonでWebSocketのお勉強を参考に作成 けっこう簡単に実装できた。 […]