[Work/Class/ES2015で動的Webアプリ/WebSocketを用いた動的Webアプリ]

ES2015でWebSocket - Tornadoでマルチクライアント

Tornadoの「1クライアント:1WSサーバー」→マルチクライアント

listでの管理

前項でサーバ側に使ったTornadoは,前項の説明の通り「1クライアント:1サーバ」になっていて,接続してくるクライアントを個別に管理することができる.

個別管理で「1クライアントのメッセージに対して1つの返答をする」というアプリケーションであればそれで問題ないが,同時に複数のクライアントに対して同じ情報を送ることはできない.

複数のクライアントに同じ情報を送るというのは,つまり「まとめて管理する」ということになる.これを実現するためには,グローバル変数としてlistを持ち,そこにWSのセッションがopenしたらそのlistにselfappendcloseしたらそのlistからselfremoveすることで管理する.(Pythonは関数の中でグローバル変数を扱う場合,参照・代入する前にglobal 変数名宣言が必要)

コード例

クライアント側のhtmlとJavaScriptは前項と同じもので大丈夫.

サーバ側.

つまり各イベントハンドラ関数に引数として流れてくるselfが「クライアントとサーバの1ペア(正確にはクライアントの情報を持ったサーバ1つ)」である.

import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.template

server_client_list = [] # サーバクライアントのペアを保持するためのlistの宣言

class MyWebSocketHandlerMultiClient_Server(tornado.websocket.WebSocketHandler):
    def open(self):
        global server_client_list
        if self not in server_client_list:
            server_client_list.append(self)

    def on_message(self, message):
        global server_client_list
        for a_server_client in server_client_list:
            a_server_client.write_message("The server return anyone's message: " + message)

    def on_close(self):
        global server_client_list
        if self in server_client_list:
            server_client_list.remove(self)

    def check_origin(self, origin):
        return True

if __name__ == '__main__':
    application = tornado.web.Application( \
        [('/ws_multi_client',  \
          MyWebSocketHandlerMultiClient_Server)])
    application.listen(30005)
    tornado.ioloop.IOLoop.instance().start()

任意のサーバクライアントを取り出すための連想配列での保持

dictでの管理

listの代わりにPythonの連想配列dictを用いることで,文字列と関連づけてサーバとクライアントのペアを保持することもできる.

例えば,接続して一番最初に「チャットID」のような文字列を,生成する,もしくはクライアントから送るように要求して(それはon_messageで受け取る),その文字列を使ってselfと文字列をdictに登録すれば,その「チャットID」から任意のサーバとクライアントのペアを取り出すことができる.

これを使うと「1つのクライアントから,接続している別のクライアントを指定して,そのクライアントだけにメッセージを送る」ということも可能になる.

コード例

prefixというのは,定められた意味を持つ文字列を頭に付与することにより,識別を行う,というもの.接続要求を受けると新たなクライアントID文字列を作り,それをkeyにしてサーバクライアントインスタンスがdictに登録される.接続終了するとそのサーバクライアントインスタンスを削除する.

dictオブジェクト.pop(key)は「そのkeyで登録されているオブジェクトを取り出して削除する」という関数である.(この関数はkeyで登録されたオブジェクトを戻り値として返すが,今回はその戻り値は使っていない)

サーバ側.Python3+Tornadoのコード

import tornado.ioloop
import tornado.web
import tornado.websocket

client_prefix_string = "client"
server_client_dict = {}
client_counter = 0

class MyWebSocketHandlerMultiClient_Server(tornado.websocket.WebSocketHandler):
    def open(self):
        global server_client_dict
        global client_counter
        if self not in server_client_dict:
            key = client_prefix_string + str(client_counter)
            server_client_dict[key] = self
            self.write_message('Your ID is: ' + key);
            client_counter += 1

    def on_message(self, message):
        global server_client_dict
        # '送るclientID/メッセージ内容'という形でメッセージが来るとする
        # messageをsplitして,IDとメッセージ内容に分ける
        client_id = message.split('/')[0]
        message_contents = message.split('/')[1]
        if client_id in server_client_dict:
            server_client_dict.get(client_id).write_message(message_contents)

    def on_close(self):
        global server_client_dict
        for key, server_client in server_client_dict.items():
            if server_client == self:
                server_client_dict.pop(key)
                break

    def check_origin(self, origin):
        return True

if __name__ == '__main__':
    application = tornado.web.Application( \
        [('/ws_multi_client_dict',  \
          MyWebSocketHandlerMultiClient_Server)])
    application.listen(30005)
    tornado.ioloop.IOLoop.instance().start()

クライアント側のhtmlコードとJavaScriptコード

<!DOCTYPE html>
<!-- WSTextMessageMultiClient_dictClient.html -->

<html lang="ja">
<head>
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta charset="utf-8" />

  <title>WebSocketでテキストメッセージ - マルチクライアントID付与</title>
  <script type="text/javascript" src="WSTextMessageMultiClient_dictClient.js">
  </script>
  
</head>
<body>
  <div id="controll_field">
    <p>
      <input type="button" value="接続開始" onclick="startConnection()" />
      <input type="button" value="接続終了" onclick="exitConnection()" />
    </p>
    <p>
      「クライアントID/送りたいメッセージ」という形で入力してください.
    </p>
    <form>
      <input type="text" name="text_field" value="ここにクライアントIDとメッセージを入力し,「送信」ボタンを押してください." />
      <input type="button" name="send_button" value="送信" onclick="sendMessage(this.form.elements['text_field'].value)" />
    </form>
  </div>
  <div id="view_field">
    <p>初期テキスト</p>
  </div>
</body>
</html>
// WSTextMessageMultiClient_dictClient.js
'use strict';
window.addEventListener('load', function(){
    window.webSocket = null;
    window.wsServerUrl = 'ws://sfdn-w1.sd.tmu.ac.jp:30005/ws_multi_client_dict';
}, false);

window.addEventListener('unload', function(event){
    window.exitConnection();
}, false);

window.startConnection = function(event){
    window.webSocket = new WebSocket(wsServerUrl);

    window.webSocket.onopen = function(event){
       window.document.getElementById("view_field").innerHTML =
            '<p>Success to connect WebSocket Server!</p>'
    };

    window.webSocket.onmessage = function(event){
	alert(event.data);
	window.document.getElementById("view_field").innerHTML =
	    '<p>Server: ' + event.data + '</p>'
    };

    window.webSocket.onerror = function(error){
	document.getElementById("view_field").innerHTML = 
	    '<p>WebSocket Error: ' + error + '</p>';
    };

    window.webSocket.onclose = function(event){
	if(event.wasClean){ //切断が完全に完了したかどうかを確認
	    document.getElementById("view_field").innerHTML =
	    '<p>切断完了</p><dl><dt>Close code</dt><dd>' + 
		event.Code + 
		'</dd><dt>Close Reason</dt><dd>' + 
		event.reason +
		'</dd></dl>';
	    webSocket = null;
	}
	else
	    document.getElementById("view_field").innerHTML =
	    '<p>切断未完了</p>';
    };

}

window.exitConnection = function(event){
    if(window.webSocket != null)
	window.webSocket.close(1000, '通常終了'); //onclose関数が呼ばれる
}

window.sendMessage = function(sendingMessage){
    if(window.webSocket != null)
	window.webSocket.send(sendingMessage);
}