PythonでWebSocketのお勉強

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のプロトコル自体は、一度クライアント側から接続してしまえば、後はサーバ側から自由にデータをプッシュできるようになっているので、必要に応じて、今回のサンプルをベースに拡張していけば良いかと思います。