便利ページ：WebSocketをサクッと触れるページを作ってみた

[便利ページ：Javascriptでちょっとした便利な機能を作ってみた]( https://qiita.com/poruruba/items/6c67c7eef34ed302feb3) のシリーズです。

今回は、WebSocketをサクッと呼び出せるページを作ります。
最近のほとんどのブラウザには、WebSocket APIが実装されているため、それを使います。

もろもろは、GitHubに上げてあります。

https://github.com/poruruba/utilities

Webページはこちらからも見ることができます。ユーティリティタブでWebSocketを選択してください。

https://poruruba.github.io/utilities/

また、今回からついで、ベースとしていたフレームワークをBootstrap 3.4.1からBootstrap 5に変更し、便利ページ内の各ページをVueのコンポーネントで構成するようにしました。
こうすることで、ページもメンテナンスしやすく、追加しやすくなりました。
もし、Bootstrap3.4.1との比較を見たい場合は以下を参照してください。

　[Bootstrap 3からBootstrap 5に移行してみた](https://qiita.com/poruruba/items/bf16cd7c71077792ebe3)

画面はこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/e7c9a6e9-f7b0-2d46-8dc9-215e13860029.png)

PWAにもしているので、アプリとして登録してあげてください。
Chromeを立ち上げなくても、いつでも便利ページを呼び出せますので。WindowsでもAndroidでもOKです。

#ソースコード

といっても、大したことはやっていないので、ソースコード(Vueコンポーネント)を示しておきます。

```js:js/comp/comp_websocket.js
const log_separator = "\n----------\n";

export default {
  mixins: [mixins_bootstrap],
  template: `
<div>
  <h2 class="modal-header">WebSocket</h2>
  <label class="title">url</label>
  <input type="text" class="form-control" v-model="ws_url" placeholder="wss://192.168.1.1:443/ws">
  <div class="row">
    <label class="col-auto title">protocols</label>
    <span class="col-auto">
      <input type="text" class="form-control" v-model="ws_protocols">
    </span>
  </div>
  <button class="btn btn-secondary" v-on:click="ws_open" v-if="!ws_socket">Open</button>
  <button class="btn btn-secondary" v-on:click="ws_close" v-else>Close</button>
  <br>
  status: {{ws_status_message}}
  <br><br>
  <div class="row">
    <label class="col-auto title">message</label>
    <span class="col-auto">
      <select class="form-select" v-model="ws_send_type">
        <option value="text">text</option>
        <option value="binary">binary</option>
      </select>
    </span>
  </div>
  <input type="text" class="form-control" v-model="ws_send_message">
  <button class="btn btn-secondary" v-on:click="ws_send" v-bind:disabled="!ws_socket">送信</button>
  <br><br>
  <textarea class="form-control col" id="ws_textarea" rows="10">{{ws_console_message}}</textarea>
  <button class="btn btn-secondary" v-on:click="console_clear">クリア</button>
</div>`,
  data: function () {
    return {
      ws_socket: null,
      ws_url: "",
      ws_protocols: "",
      ws_console_message: "",
      ws_send_type: "text",
      ws_send_message: "",
      ws_status_message: "closed",
    }
  },
  methods: {
    /* WebSocket */
    ws_open: function(){
      this.ws_close();
      
      try{
        this.ws_socket = new WebSocket(this.ws_url);
        this.ws_socket.binaryType = "arraybuffer";
        this.ws_socket.onopen = (event) => {
//          console.log("websocket opened", event);
          this.console_log(this.make_console_state_message(event));
          var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" });
          this.ws_status_message = "opened from " + date_string;
        };
        this.ws_socket.onclose = (event) =>{
//          console.log("websocket closed", event);
          this.console_log(this.make_console_close_message(event));
          this.ws_socket = null;
          this.ws_status_message = "closed";
        };
        this.ws_socket.onerror = (event) =>{
//          console.log("websocket error", event);
          this.console_log(this.make_console_state_message(event));
        };
        this.ws_socket.onmessage = (event) => {
//          console.log("websocket message", event);
          if (typeof (event.data) == "string" || event.data instanceof String ){
            this.console_log(this.make_console_input_message(event));
          }else{
            this.console_log(this.make_console_input_message(event));
          }
        };
      }catch(error){
        alert(error);
      }
    },
    ws_close: function(){
      if (this.ws_socket)
        this.ws_socket.close();
    },
    ws_send: function(){
      if( this.ws_send_type == "text" ){
        this.ws_socket.send(this.ws_send_message);
        this.console_log(this.make_console_output_message("text", this.ws_send_message));
      }else{
        var data = hexStr2byteAry(this.ws_send_message);
        var array = new Uint8Array(data);
        this.ws_socket.send(array);
        this.console_log(this.make_console_output_message("binary", array));
      }
    },
    console_clear: function () {
      this.ws_console_message = "";
    },
    make_console_output_message: function (type, data) {
      var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" });
      var message;
      if (typeof (data) == "string" || data instanceof String) {
        message = data;
      } else {
//        message = new Uint8Array(data).toString();
        message = byteAry2hexStr(data);
      }
      return "[SEND] message " + type + " " + date_string + "\n" + message;
    },
    make_console_state_message: function (event) {
      var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" });
      return "[STATE] " + event.type + " " + date_string;
    },
    make_console_close_message: function (event) {
      var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" });
      return "[STATE] " + event.type + " code=" + event.code + " " + date_string;
    },
    make_console_input_message: function(event){
      var date_string = new Date().toLocaleString('ja-JP', { "hour12": false, "year": "numeric", "month": "2-digit", "day": "2-digit", "hour": "2-digit", "minute": "2-digit", "second": "2-digit" });
      var message;
      var type;
      if (typeof (event.data) == "string" || event.data instanceof String) {
        message = event.data;
        type = "text";
      } else {
//        message = new Uint8Array(event.data).toString();
        message = byteAry2hexStr(event.data);
        type = "binary";
      }
      
      return "[RECV] " + event.type + " " + type + " " + date_string + "\n" + message;
    },
    console_log: function(message){
      this.ws_console_message = message + log_separator + this.ws_console_message;
    }
  },
  mounted: function(){
    var elem_in = document.querySelector("#ws_textarea");
    elem_in.style.height = 10;
  }
};

function hexStr2byteAry(hexs, sep = '') {
  hexs = hexs.trim(hexs);
  if (sep == '') {
    var array = [];
    for (var i = 0; i < hexs.length / 2; i++)
      array[i] = parseInt(hexs.substr(i * 2, 2), 16);
    return array;
  } else {
    return hexs.split(sep).map((h) => {
      return parseInt(h, 16);
    });
  }
}

function byteAry2hexStr(bytes, sep = '', pref = '') {
  if (bytes instanceof ArrayBuffer)
    bytes = new Uint8Array(bytes);
  if (bytes instanceof Uint8Array)
    bytes = Array.from(bytes);

  return bytes.map((b) => {
    const s = b.toString(16);
    return pref + (b < 0x10 ? ('0' + s) : s);
  }).join(sep);
}
```

要は、以下でWebSocketサーバに接続して、

```
        this.ws_socket = new WebSocket(this.ws_url);
```


以下で、サーバに送信して、

```
        this.ws_socket.send(this.ws_send_message);
```


以下で、サーバから受信する。

```
        this.ws_socket.onmessage = (event) => {
```

それだけです。
何かわかんなくなったり、パラメータで微調整したい場合は、以下を参考にしてください。

接続・切断・送信・受信の動きは、テキストエリアに逐一出力していますので、時系列で終えるようにしています。

WebSocket API (WebSockets)
　https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API

#終わりに

HTTPSで立ち上げたWebサーバのJavascriptからHTTPのWebSocketサーバに接続するには、WebSocketサーバをHTTPS接続にする必要があります。
以下を参考に、Web Proxyサーバを立ち上げてください。

　[HTTPで立ち上げたWebSocketサーバに、HTTPSで接続するためのWeb Proxyの立ち上げ](https://qiita.com/poruruba/items/d0f5ef3ac4d09cfad593)

あと、こちらもご参考まで。

　[MMDダンスをM5Core2のディスプレイに表示する](https://qiita.com/poruruba/items/a2cd9606c630bea5f5bf)

以上