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

ES2015でWebSocketの基本 / 2017-10-27 (金)

残念ながらES2015書法には対応していないWebSocket

ヒジョーに残念なことに,2017年5月末現在,Google ChromeのJavaScriptエンジンはWebSocketクラスの継承等に対応していない様子である.したがって「用意されているクラスを継承し,関数をオーバーライドして自分のクラスを作成し,そのインスタンスを生成して使う」通常の書法は使えない.

そこで,旧来のJavaScriptのスタイル(プロトタイプ型オブジェクト指向)の手法である「先にインスタンスを作り,その後にインスタンスのプロパティとして上書きするイベントハンドラ関数を追加していく」という方法をとる.

手法自体はこれまで行ってきた「windowインスタンスのプロパティとして変数(グローバル変数)や関数(例えばonload関数やonunload関数)を追加していく」のと同じ方法である.

「https://」ではなく「http://」でアクセスする

これまで授業でやってきたように,2017年10月現在,Google ChromeでJavaScriptで記述したAJAXアプリを実行する場合,「セキュアhttp」接続で接続する必要がある,すなわち「https://」で始まるURLでアクセスする必要がある.

しかし,WebSocketを記述したJavaScriptを実行するhtmlファイルに「https://」でアクセスすると,WebSocket接続の方も強制的に「セキュアws」すなわち「wss://」で始まるURLを使用しないとエラーになる,という問題がある.

セキュアWS「wss://」をやるためには,サーバプログラム側で証明書を用意する必要があるなど,手順が面倒くさいので,この授業課題ではセキュアWSを用いない.(実際に運用するWebアプリケーションを作るときは,もちろんセキュアWSを採用するべきである)

そこで,この授業課題を実行するときには「http://」でアクセスすること.

コード例

クライアント側.ボタンやテキストフィールドを配置したHTMLファイルと,JavaScriptファイル.

<!DOCTYPE html>
<!-- WSTextMessageJSClient.html -->

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

  <title>WebSocketでテキストメッセージのやり取り</title>
  <script type="text/javascript" src="WSTextMessageJSClient.js">
  </script>
  
</head>
<body>
  <div id="controll_field">
    <p>
      <input type="button" value="接続開始" onclick="startConnection()" />
      <input type="button" value="接続終了" onclick="exitConnection()" />
    </p>
    <form>
      <input type="text" name="text_field" value="ここに入力して「送信」ボタンを押してください." />
      <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>
// WSTextMessageJSClient.js
'use strict';
window.addEventListener('load', function(){
    // 内部で使うプロパティは全てグローバルのものなので,アローによる定義の必要はない
    // グローバル変数で(windowオブジェクトのプロパティとして)
    // WebSocketクラスのインスタンスを保持する変数を作っておく
    window.webSocket = null;
    window.wsServerUrl = 'ws://localhost:30005/simple_ws';
}, false);

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

window.startConnection = function(event){
    // WebSocketクラスのインスタンス化
    window.webSocket = new WebSocket(wsServerUrl);
    // Override Methods
    // WebSocketクラスのインスタンスのプロパティとして
    // WebSocket通信に必要な関数を上書きしていく
    // つまり関数オブジェクトを「プロパティ」として追加していく
    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);
}

サーバ側,Python3+Tornadoライブラリの場合.

Tornadoが原理的に一番わかりやすく高性能なので,今後これを使う.

tornado.websocket.WebSocketHandlerを継承したクラスを定義し,そのクラス定義オブジェクトをtornado.web.Application関数に与えることで,クライアントが接続する毎にWebSocketサーバオブジェクトを一つずつ生成する.つまり「1クライアント:1サーバ」である.

# -*- coding: utf-8 -*-
# WSTextMessageTornadoServer.py
import tornado.ioloop
import tornado.web
import tornado.websocket
import tornado.template

class MyWebSocketHandler(tornado.websocket.WebSocketHandler):
    # Override Event Functions
    def open(self):
        print('connection opened...')
        self.write_message("The server says: 'Hello'. Connection was accepted.")

    def on_message(self, message):
        print('received:', message)
        self.write_message("The server return your message: " + message)

    def on_close(self):
        print('connection closed...')

    def check_origin(self, origin):
        # クロスオリジン(Cross-Origin)ポリシーの問題を解決する
	# 通常JavaScirptは「自分がダウンロードされてきたサーバのドメイン」にしか接続できない
	# ここで何もチェックしないでTrueを返すことで,
	# このサーバプログラムが動いているサイト以外(例えばローカルで実行している時)から
	# ダウンロードされたJavaSciprtプログラムからも,接続を許す
        return True

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

サーバ側.Node.jsでNode-Yawlライブラリを使う場合.

Node.js v6とnode-yawlが必要.Ubuntuでは,

$ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
$ sudo apt install nodejs node-yawl

ただしnode-yawlはクライアントの個別識別ができない模様.またwebsocketのホスト名以下も認識できない.

1クライアントに対し1つのWebSocketサーバオブジェクトを生成するPython Tornadoとは違い,node-yawlは「一つのWebSocketサーバオブジェクト」(コード例中のglobal.webSocketServer変数で保持しているオブジェクト)が,多数接続している全てのクライアントの要求を処理する.

// WSTextMessageYawlServer.js
// Yet Another Websocket Library
// https://github.com/andrewrk/node-yawl
'use strict';
global.yawl = require('yawl');
global.http = require('http');
global.server = http.createServer();

global.webSocketServer = yawl.createServer({
  server: global.server,
  origin: null, // set null to disable origin check
  allowTextMessages: true,
});

global.webSocketServer.on('connection', function(ws) {
    console.log('connection opened...');
    ws.sendText('The server says: Hello. Connection was accepted.');
    ws.on('textMessage', function(message) {
	console.log('received:' + message);
	ws.sendText('Server receive message: ' + message);
    });
    ws.on('closeMessage', function(statusCode, message) {
	console.log('connection closed...');
    });
});

global.port = 30005;
global.host = 'localhost';

global.server.listen(global.port, global.host, function() {
    console.log("Listening at " + global.host + ":" + global.port + "/");
});

サーバ側.C言語(C++)+libwebsocketsの場合.

libwebsocketsもnode-yawlと同じく,「1つのWebSocketサーバオブジェクトが全てのクライアントをさばく」ため,クライアントの個別識別ができない模様.またwebsocketのホスト名以下も認識できない.

// WSTextMessageLibWebSocketsServer.cpp
#include <iostream>
#include <string>
#include <libwebsockets.h>

#ifdef __WIN32
#include <io.h>
#include "gettimeofday.h"
#else
#include <syslog.h>
#include <sys/time.h>
#include <unistd.h>
#endif

volatile int force_exit;
struct lws* wsi;
static std::string _sendStr = "";//送る文字列の定義

//関数のプロトタイプ宣言
static int my_lws_callback_function(struct lws* wsi,
				    enum lws_callback_reasons reason,
				    void *user,
				    void *in,
				    size_t len);
static void sighandler(int sig);

//Unix Signalに送る関数の設定
void sighandler(int sig){
  force_exit = 1;
  //そもそもwsiポインタの中には何も入っていないので,ここでセグフォが起きる
  lws_cancel_service(lws_get_context(wsi));
}

//プロトコル定義
static struct lws_protocols my_lws_protocols[] = {
  {
    "my_websocket_protocol", //name
    my_lws_callback_function, //function pointer of callback function
    0, //per_session_data_size
    128 //max frame size / rxbuffer
  },
  {NULL, NULL, 0, 0} //Terminator
};

//コールバック関数の定義
static int my_lws_callback_function(struct lws* callback_wsi,
				    enum lws_callback_reasons reason,
				    void *user,
				    void *in,
				    size_t len){
  wsi = callback_wsi;
  if(reason != LWS_CALLBACK_GET_THREAD_ID){
    //起動すると常にスレッドIDが取れる状態であることを示す定数が入ってくるので,
    //それ以外の時,CALLBACKイベント定数値を表示する.
    //Callbackイベント定数値は以下を参照のこと
    //https://libwebsockets.org/lws-api-doc-master/html/group__usercb.html
    lwsl_notice("%d\n", reason);
  }
  //reasonの中にイベントの種類が勝手に入ってくるので,それを切り分けて,
  //それぞれの処理を書く
  switch(reason){
    //コネクション開始の場合
  case LWS_CALLBACK_ESTABLISHED:
    {
      lwsl_notice("connection opened...\n");
    }
    break;
    //コネクション終了
  case LWS_CALLBACK_PROTOCOL_DESTROY:
    {
      lwsl_notice("connection closed...\n");
    }
    break;
    //送信処理
  case LWS_CALLBACK_SERVER_WRITEABLE:
    {
      lws_write(callback_wsi, 
		(unsigned char*)_sendStr.c_str(), 
		_sendStr.length(), 
		LWS_WRITE_TEXT);
    }
    break;
    //受信処理
  case LWS_CALLBACK_RECEIVE:
    {
      lwsl_notice("ReceiveMessage: [%s]\n", (const char*)in);
      // (const char*)型のinという変数に入ってくる.
      // std::string型に変換する
      // std::stringクラスのコンストラクタに打ち込むだけでよい
      // ここで作ったstd::string型の変数はローカル変数なので,
      // ブロックを抜けたら勝手に解放されるはず
      std::string received_string((const char*)in);

      // 「サーバが発したメッセージであることを示す文字列を連結して
      // グローバル変数の_sendStrに書き込む
      _sendStr = 
	std::string("Server received: ") + received_string; 

      //_sendStrに書き込んだうえでこのコールバック関数そのものを呼ぶと
      //自動的にWRITABLEイベントモードに入って送信される.
      lws_callback_on_writable_all_protocol
	(lws_get_context(callback_wsi), my_lws_protocols);

    }
    break;
    //何かよくわからないイベント
  case LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION:
    {
      //dump_handshake_info(wsi);
    }
    break;
  default:
    {
    }
    break;
  }
  return 0;
}

int main(){
  struct lws_context *context; //コンテキストというかWSインスタンスというか
  struct lws_context_creation_info info;
  memset(&info, 0, sizeof(info));
  info.port = 30005;
  info.iface = NULL; //const char* 128文字までらしい
  info.protocols = my_lws_protocols; //上で自分で作ったプロトコルの配列

  //SSLによる暗号化接続はしないので,そのためのファイルは必要ない.
  int use_ssl = 0;
  info.ssl_cert_filepath = NULL;
  info.ssl_private_key_filepath = NULL;

  info.gid = -1; //通常ユーザのプロセスとして動かす場合は-1
  info.uid = -1; //同上
  info.options = 0; //通常のコンテキストを使う場合は特にオプションは入れない

  //C言語ライブラリを設計する時に,定数タグを作るための定数に
  //ビット演算子をうまく考慮に入れると,
  //複数の状態を1変数にまとめることができる
  // "|"は「ビットごとのOR」なので,最大32状態(32bit)の状態を1変数でキープできる
#ifdef __WIN32
  int syslog_options = LOG_PID;
#else
  int syslog_options = LOG_PID | LOG_PERROR; 
#endif

  //システムログに記録するデバッグレベル
  int debug_level = 7;  
  lws_set_log_level(debug_level, lwsl_emit_syslog);
  lwsl_notice("libwebsockets server - \n");

  //Unixシグナルに反応する関数の登録
  signal(SIGINT, sighandler);
  //syslogに登録する
  setlogmask(LOG_UPTO(LOG_DEBUG));
  openlog("lwsts", syslog_options, LOG_DAEMON);
  
  //コンテキストを生成する
  context = lws_create_context(&info);
  if(context == NULL){
    //もしコンテキストの生成に失敗したら,エラーメッセージを出して異常終了.
    lwsl_err("libwebsocket init failed\n");
    return -1;
  }

  int n = 0;
  while(n >= 0 && !force_exit){
    struct timeval tv; //secとmicrosecondsの二つが入った構造体で定義
    gettimeofday(&tv, NULL);
    n = lws_service(context, 50);
    //50msecでwhileを回す?
  }

  //whileループをforce_exitで終了した時のプログラムの終了処理
  lws_context_destroy(context);
  lwsl_notice("libwebsockets-server exited cleanly\n");
  
  return 0;
}