素人Web屋の備忘録

素人Web屋の備忘録。たまに雑記。

Twilio + Node.jsで自動音声を流す1

昨年あたりにテレビとスマホの連動企画が流行って色々な施策が世に登場しましたが、その中のひとつに、CMやドラマの役者が持っているスマホの電話番号が画面に映り込み、その番号に電話すると実際に役者が電話に出てくれるっていう施策がありました。

これはおそらくKDDIがやってるTwilioっていうプラットフォームで実装できそうなので自分でもやってみます。

Twilioについて

KDDIがやっているプラットフォームサービス(Paas)
電話をかける、電話をうける、SMS通知などを操作するAPIがそろっていて、簡単にアプリケーションとこれらの機能を連携させることができます。
例えばコールセンターに代表されるような、電話のプッシュダイヤル操作なども作れます。
無料トライアルも用意されているので簡単に試すことができます。
Twilioのホームページはこちら
https://twilio.kddi-web.com/

Twilio登録方法

f:id:tmykndr:20180718234702p:plain 1. Twilioのトップページかからサインアップ(右上)クリック


f:id:tmykndr:20180718234706p:plain 2. 利用規約とプライバシーポリシーにチェックを入れて「同意してサインアップ」クリック(チェック入れる前はグレーアウトしています)


f:id:tmykndr:20180718234710p:plain 3. 項目を埋めて「はじめる」クリック


f:id:tmykndr:20180718234713p:plain 4. 本人確認用に電話番号を入力して「Verify」クリック。SMSで認証コードが送られてきます


f:id:tmykndr:20180718234716p:plain 5. 認証コードを入力して「Submit」クリック


f:id:tmykndr:20180718234719p:plain 6. このアカウントで使用するプロジェクト名を入力して「続ける」クリック。プロジェクト名はマイページに「プロジェクト名+ダッシュボード」の形で記載されます。


f:id:tmykndr:20180718234723p:plain 7. マイページが表示されます。登録お疲れ様でした。


Twilioの電話番号から発信させる方法

まず、一番単純なかたちで、メッセージをしゃべらせてみようと思います。
Twilioが用意しているREST APIを使ってTwilioの電話番号から自分宛に電話をかけてみます。
Twilioに登録するとaccountSidとauthTokenが発行されるので、こちらを使ってユーザー認証します。(これらは自分のダッシュボードから確認できます。)

const twilio = require('twilio');
const accountSid = '自分のaccountSid';
const authToken = '自分のauthToken';
const client = twilio(accountSid, authToken);

今回しゃべらせる言葉はテキストでソース上にそのまま記述していきます。

const word = "今年の夏は猛暑でつらいですが頑張ってください。";

次にやることは、TwimlというTwilioが用意しているXMLで、実行したい処理を指定します。
TwiMLは電話やSMSを受信したときにどういう動作をさせるかを指定するマークアップ言語です。
TwiMLについての詳細はこちら

今回、メッセージを読み上げたいので、TwiMLsayメソッドを使います。
少しまえに某凄腕 腐女s 美人エバンジェリストがGoogleHomeを筋肉ボイス化しておりましたが、ここは普通の女性の声でしゃべっていただきたいのでvoicewomanに指定します。
languageをしゃべらせたい言語に指定することで各国の言葉にも対応しています。  

var twiml = '<Response><Say voice="woman" language="ja-jp">' + word + '</Say></Response>';

ここまで終わりましたら、処理を指定したTwiMLを実際にしゃべらせてみます。
そのためにやることはアウトバウンドコール(架電)を作成することです。

アウトバウンドコールを作成するため指定する項目が3つあります。
* 自分の携帯電話番号
* Twilioアカウントを作成した後に取得できるTwilio電話番号
* 作成したTwiMLをTwilioで用意している「Echo」という機能を使ってurlにセット

「Echo」機能は、http://twimlets.com/echo?Twiml="返したいTwiMLといった形式でリクエストすると、TwiMLを返してくれます。
「Echo」機能についてはTwilio LabsにTwiMLジェネレータという便利なものもあります。
Twilio Labs

ここで1点気を付けるポイントがあって、Echoでリクエストするときにクエリをパースする必要があります。
そのためにquerystringを使います。以下のようにquerystringを追加します。

const twilio = require('twilio');
const accountSid = '自分のaccountSid';
const authToken = '自分のauthToken';
const client = twilio(accountSid, authToken);
const querystring = require('querystring');

追加したら、実際にパースして指定します。
querystring.escape(twiml)で、escapeを使って指定された文字列に対してURLパーセントエンコーディングをかけます。

client.calls.create({
    to: '+818012345678',
    from: '+815012345678',
    url: 'http://twimlets.com/echo?Twiml=' + querystring.escape(twiml)
}, function (err, responseData) {
    if(err) throw err;
    console.log(responseData.from);
});

これで完了です。
ソースはこんな感じです。

//outboundCall.js

const twilio = require('twilio');
const accountSid = '自分のaccountSid';
const authToken = '自分のauthToken';
const client = twilio(accountSid, authToken);
const querystring = require('querystring');

const word = "今年の夏は猛暑でつらいですが頑張ってください。";
var twiml = '<Response><Say voice="woman" language="ja-jp">' + word + '</Say></Response>';

client.calls.create({
    to: '+818012345678',
    from: '+815012345678',
    url: 'http://twimlets.com/echo?Twiml=' + querystring.escape(twiml)
}, function (err, responseData) {
    if(err) throw err;
    console.log(responseData.from);
});

それではコンソールを出して以下のコマンドを実行します。

node outboundCall.js

これで携帯電話に電話がかかってきます。
いかがでしょうか。これで今年の猛暑も乗り越えられること間違いなしですね。

無料トライアル版だと電話応答時に数秒間広告が流れますが、有料版では流れません。
簡単にですが、以上でTwilio+Node.jsでのアウトバウンドコール作成は完成です。

ご指摘などあれば大歓迎で受け付けております!!!よろしくお願いします!!!

Vueを実践で使った話01

この前実際に仕事でVue.jsを使う機会があったので、実践編としてその使い方を残しておきます。
使いどころとしては複数のモーダルがあってモーダル表示でyoutubeを自動再生されるものです。
Gulpタスクはビルドするだけのシンプルなものにしました。
ディレクトリ構成もindex.htmlとJSに絞って書きます。

ソースはこちらにあります。
https://github.com/tomoyukionodera/vuefunc01

ディレクトリ構成

root/
 ├ gulpfile.js
 ├ js/
 │ └ main.js
 │ └ bundle.js
 └ index.html

gulpfile.js

"use strict"

const gulp = require('gulp');
const browserify = require('browserify');
const babelify = require('babelify');
const source = require('vinyl-source-stream');
const vueify = require('vueify');

gulp.task('browserify', function() {
  return browserify('./js/main.js')
  .transform(babelify,{presets:["env"]})
  .transform(vueify)
  .bundle()
  .on('error', function(err){
    console.log(err.message);
    console.log(err.stack);
  })
  .pipe(source('bundle.js'))
  .pipe(gulp.dest('./js/'));
})

main.js

const Vue = require('vue');

const bus = new Vue();

new Vue({
    el: '#trg',
    data: {
        artists: [
            {
                name: "あああああ",
                url: "https://www.youtube.com/embed/hLMJXH8TMJg?rel=0&autoplay=1"
            },
            {
                name: "いいいいい",
                url: "https://www.youtube.com/embed/PEBy3aegIvE?rel=0&autoplay=1"
            },
            {
                name: "ううううう",
                url: "https://www.youtube.com/embed/CUpgdwuEJxw?rel=0&autoplay=1"
            },
            {
                name: "えええええ",
                url: "https://www.youtube.com/embed/lTDeS-PmLgY?rel=0&autoplay=1"
            },
            {
                name: "おおおおお",
                url: "https://www.youtube.com/embed/0M3HoC2uGhM?rel=0&autoplay=1"
            },
            {
                name: "かかかかか",
                url: "https://www.youtube.com/embed/wf1MfO4V7cA?rel=0&autoplay=1"
            }
        ]
    },
    methods: {
        execute: function(artist) {
            bus.$emit('click.trg', artist.url)
        }
    }
})

new Vue({
    el: "#modal",
    data: {
        opened: false,
        url: ""
    },
    methods: {
        open: function(val){
            this.opened = true;
            this.url = val
        },
        close: function(){
            this.opened = false;
        }
    },
    created: function() {
        bus.$on('click.trg', this.open)
    }
})

index.html

<body>
    <div class="wrap">
        <ul id="trg" class="list">
            <li class="item" v-for="(artist,index) in artists" v-on:click="execute(artist)">
                <img v-bind:src="'images/voice' + index + '.jpg'" v-bind:alt="artist.name">
            </li>
        </ul>
        <div id="modal" v-cloak>
            <div class="overlay" v-if="opened" v-on:click.self="close">
                <div class="mcont">
                    <div class="btnclose" v-on:click="close"></div>
                    <iframe width="900" height="506" v-bind:src="this.url" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
                </div>
            </div>
        </div>
    </div>
    <script src="./js/bundle.js"></script>
</body>

それではmain.jsとindex.htmlについて解説していきます。

main.js

今回、トリガーとなる要素をクリックしたら、それに連動したモーダルが表示されるという動きを作成します。
想定として、トリガー要素のVueインスタンス(dataに各youtubeの埋め込みURLを持っている)をクリック=>連動したモーダルに埋め込みURLを渡しつつそれを使ってモーダル表示させます。
data受け渡しはイベントを使って行うので、イベントハブ用のVueインスタンスを最初に作っておきます。
あとは、<li>を展開するときにv-for="(artist,index) in artistsindexを渡すので、画像名には連番を振っています。
モーダル要素のVueインスタンスにはopened: false,、HTMLにはv-if="opened"を記述しているので最初はDOMにレンダリングされていません。

まずVueを読み込みます。
そしてイベントハブ用のVueインスタンスを作成します。(使い方は後ほど解説します。)

const Vue = require('vue');
const bus = new Vue();

次にトリガーとなる要素のVueインスタンスを作成していきます。

new Vue({
    el: '#trg',
    data: {
        artists: [
            {
                name: "あああああ",
                url: "https://www.youtube.com/embed/hLMJXH8TMJg?rel=0&autoplay=1"
            },
            {
                name: "いいいいい",
                url: "https://www.youtube.com/embed/PEBy3aegIvE?rel=0&autoplay=1"
            },
            {
                name: "ううううう",
                url: "https://www.youtube.com/embed/CUpgdwuEJxw?rel=0&autoplay=1"
            },
            {
                name: "えええええ",
                url: "https://www.youtube.com/embed/lTDeS-PmLgY?rel=0&autoplay=1"
            },
            {
                name: "おおおおお",
                url: "https://www.youtube.com/embed/0M3HoC2uGhM?rel=0&autoplay=1"
            },
            {
                name: "かかかかか",
                url: "https://www.youtube.com/embed/wf1MfO4V7cA?rel=0&autoplay=1"
            }
        ]
    },
    methods: {
        execute: function(artist) {
            bus.$emit('click.trg', artist.url)
        }
    }
})

そしてトリガーがクリックされたときのメソッドを定義します。

methods: {
    execute: function(artist) {
        bus.$emit('click.movie', artist.url);
    }
}

このメソッドはマウントポイント(<ul id="trg">)直下の<li>に設定します。
引数にartistを設定することで、"click.movie"イベントを発火させたときにそのartistに設定されているartist.urlを渡すことができます。

<li class="item" v-for="(artist,index) in artists" v-on:click="execute(artist)">
    <img v-bind:src="'images/voice' + index + '.jpg'" v-bind:alt="artist.name">
</li>

ここでいったんbusについて書きます。
以下の記述でトリガーがクリックされたときのメソッドを定義していますが、これは第一引数でイベント名、第二引数でイベント発火時に受けわたす値を設定しています。
これはemitという書き方をすることで実現できます。emitはイベント発火のメソッドです。

bus.$emit('click.movie', artist.url);

そしてemitで発火させたイベントを受け取る処理がモーダル要素のVueインスタンスに設定されている以下のメソッドです。onはイベント受け取りのメソッドです。
onメソッドは受け取ったイベントとメソッドを関連付ける役割をします。(javascriptのaddEventListennerみたいなもの)

トリガー要素クリック=>emitで定義したイベント名でイベント発火(このときartist.url持ってる)=>onでそのイベントに対応した(第二引数に指定した)メソッドを実行、という流れです。

created: function() {
    bus.$on('click.trg', this.open)
}

ちなみにonで関連付けたopenメソッドは以下なので受け取ったartist.urlを自分(モーダル要素のVueインスタンス)のdataに格納つつ自分のopenedプロジェクトをtrueに変えています。
これでクリックしたときにそれに対応したモーダルが表示されyoutubeが再生されます。

open: function(val){
    this.opened = true;
    this.url = val
}

ちょっとしたポイントとして、ブラウザロード時に一瞬モーダルがちらつく現象があるので、それは以下で回避しています。
index.html

<div id="modal" v-cloak>

index.css

[v-cloak] {
    display: none;
}

長くなりましたがこんな感じでモーダルを実装しました。
今回、こちらを参考にしてソースを書きました!ありがとうございます!!
https://liginc.co.jp/374210

読んでいただいた方いましたら、最後までお付き合いいただきありがとうございました!
当方経験が浅いためご指摘、アドバイスあれば教えてください。

読んでいただいてありがとうございました!!

Cloud Speech API + node.jsで音声認識をさせてみる3

今回は音声をローカルファイルに書き込まず、リアルタイムで音声認識させてconsoleに出す処理を書いていきます。
Cloud Speech API + node.jsで音声認識をさせてみる1
Cloud Speech API + node.jsで音声認識をさせてみる2

今回はリアルタイムで認識させるので、fsモジュールは必要ないので外しておきます。
リアルタイムで認識させるためにstreamingRecognizeを使います。

const record = require('node-record-lpcm16');
const speech = require('@google-cloud/speech');
const speechClient = new speech.SpeechClient();
const SAMPLE_RATE = 16000

const request = {
  config: {
    encoding: 'LINEAR16',
    sampleRateHertz: SAMPLE_RATE,
    languageCode: 'ja-jp'
  }
};

const recognizeStream = speechClient
  .streamingRecognize(request)
  .on('error', console.error)
  .on('data', (data) => {
    console.log(data.results[0]);
  });

record.start({
  sampleRate: SAMPLE_RATE,
  recordProgram: 'sox',
  thresholdStart: 0.2,
  thresholdEnd: 0.1,
  silence: '0.1'
}).pipe(recognizeStream);

まず以下でrecognizeStreamを作成します。

const recognizeStream = speechClient
  .streamingRecognize(request)
  .on('error', console.error)
  .on('data', (data) => {
    console.log(data.results[0]);
  });

そしてレコーディングを開始して、取り込んだデータをrecognizeStreamに渡します。

record.start({
  sampleRate: SAMPLE_RATE,
  recordProgram: 'sox',
  thresholdStart: 0.2,
  thresholdEnd: 0.1,
  silence: '0.1'
}).pipe(recognizeStream);

ここまで記述したindex.jsを起動します。

node ./index.js

これでマイクに向かってしゃべると内容がConsoleに表示されます。
結果はこんな感じです。

$ node index.js
{ alternatives:
   [ { words: [],
       transcript: 'あいうえお',
       confidence: 0.9614928960800171 } ],
  isFinal: true,
  stability: 0 }

今日はここまで!読んでいただいてありがとうございました!!!

Cloud Speech API + node.jsで音声認識をさせてみる2

続き書きます!

前回、音声ファイルの取り込みには成功したので、次にこの音声をテキストに変換する記述を追記していきます。

まず以下でGoogle cloud client libraryをインポートして、クライアントを作成します。

const speech = require('@google-cloud/speech');
const client = new speech.SpeechClient();

次に音声ファイルをbase64に変換する処理を書きます。

const fileName = './test.raw';
 
const file = fs.readFileSync(fileName);
const audioBytes = file.toString('base64');

次にエンコーディングやサンプリング周波数、言語などをconfigで指定します。
const audio、const configを作成してjson形式でconst requestを作成します。

const audio = {
  content: audioBytes,
};
const config = {
  encoding: 'LINEAR16',
  sampleRateHertz: 16000,
  languageCode: 'en-US',
};
const request = {
  audio: audio,
  config: config,
};

最後にrecognizeメソッドを使って音声データをテキスト化します。
recognizeメソッドはpromiseオブジェクトを戻り値で持ってるので、thenメソッドでその結果を受け取っています。

client.recognize(request)
  .then(data => {
    const response = data[0];
    const transcription = response.results
      .map(result => result.alternatives[0].transcript)
      .join('\n');
    console.log(`Transcription: ${transcription}`);
  })
  .catch(err => {
    console.error('ERROR:', err);
  });

ここまで書いたら実行します。

node ./index.js

自分は「あいうえお」と音声を取り込んでいたので結果は以下のようになります。

$ node index.js
Transcription: あいうえお

以上ざっくりと解説しましたが、ソース自体はnpm公式の@google-cloud/speechページに載っています。
https://www.npmjs.com/package/@google-cloud/speech

前回からここまでで、音声をローカルに取り込んでからローカルファイルの音声データをテキストに変える処理を書きました。
次はリアルタイムで音声を認識させてconsoleに出す方法を書きます!
読んでいただいてありがとうございました!!!

Cloud Speech API + node.jsで音声認識をさせてみる1

前回、前々回とNode.js+Express+Azureでチャットアプリを作成しました。
これからルーム機能やログイン機能を付けていったら面白そうだなと思っているのですが、一番面白そうだなと思ったのが音声入力だったので、そちらから始めてみることにしました。
前回までの記事はこちら
Node.js+Express+Azureでチャットアプリをデプロイしてみる1
Node.js+Express+Azureでチャットアプリをデプロイしてみる2

調べたところ、音声認識Google Cloud Platform(GCP)のCloud Speech APIを使って実現できるみたいなのでそちらを使ってみたいと思います。
GCPGoogleクラウド上で提供しているサービスの総称で、そこにはとても多くのAPIライブラリがあったりVMやアプリのプラットフォームがあったりします。
GCPについて
https://cloud.google.com/

実際に音声認識をさせるまでかなりハマって大変だったので、気を付けるポイントなども書いていきます。

Node.jsはインストールされている前提で、以下環境です。
環境
Window10
node.js(8.9.4)

さらに音声認識をするために必要な以下のソフトをインストールします。
sox(音声ファイルの形式を変換するソフト)
Audacity(音声ファイルが正しく記録されているか確認用なのでなくてもOK)
Macを使っていればsoxbrew install soxでOK

前準備

Cloud Speech APIを使うためにGCPに登録します。
登録はこちらを参考にしました。ありがとうございます!
https://book.mynavi.jp/manatee/detail/id=65673

プロジェクト名を設定してプロジェクトを作成したら、ナビゲーションメニューを開いて「APIとサービス」からCloud Speech APIを有効にします。
次に認証情報の「認証情報を作成」のプルダウンからサービスアカウントキーを作成します。
作成後にjsonファイル(秘密鍵)が自分のPCに保存されます。※失くさない&&公開しないようにセキュリティに気を付けて保管してください。

作成後、プロジェクトIDとjsonファイルを環境変数に登録することが必要です。

export GCLOUD_PROJECT=プロジェクトID
export GOOGLE_APPLICATION_CREDENTIALS="pass/to/createdJsonFile"

node-record-lpcm16のインストール

今回用に適当にディレクトリを作成したら、そこでまずpackage.jsonを作成します。

npm init

次にnode.jsで音声ファイルを作成するためにnode-record-lpcm16をインストールします。

npm install node-record-lpcm16 --save

インストールが完了したら、落ちてきたパッケージ内にあるindex.jsの26行目のswitch文を以下の記述に変更します。

switch (options.recordProgram) {
  // On some Windows machines, sox is installed using the "sox" binary
  // instead of "rec"
  case 'sox': // case 'sox'節に以下のコードを追加
  cmd = options.recordProgram;
  cmdArgs = [
    '-q',                                 // show no progress
    '-t', 'waveaudio',                    // input-type
    '-d',                                 // use default recording device
    '-r', options.sampleRate.toString(),  // sample rate
    '-c', '1',                            // channels
    '-e', 'signed-integer',               // sample encoding
    '-b', '16',                           // precision (bits)
    '-t', 'raw',                          // output-type
    '-'                                   // pipe
  ];
  break;
  case 'rec':
  default:
    cmd = options.recordProgram
    cmdArgs = [
      '-q',                     // show no progress
      '-r', options.sampleRate, // sample rate
      '-c', '1',                // channels
      '-e', 'signed-integer',   // sample encoding
      '-b', '16',               // precision (bits)
      '-t', 'wav',              // audio type
      '-',                      // pipe
          // end on silence
      'silence', '1', '0.1', options.thresholdStart || options.threshold + '%',
      '1', options.silence, options.thresholdEnd || options.threshold + '%'
    ]
    break
  // On some systems (RasPi), arecord is the prefered recording binary
  case 'arecord':
    cmd = 'arecord'
    cmdArgs = [
      '-q',                     // show no progress
      '-r', options.sampleRate, // sample rate
      '-c', '1',                // channels
      '-t', 'wav',              // audio type
      '-f', 'S16_LE',           // Sample format
      '-'                       // pipe
    ]
    if (options.device) {
      cmdArgs.unshift('-D', options.device)
    }
    break
}

index.jsの作成

ここまで終わったら実際に音声を取り込んで音声ファイルを吐き出すindex.jsを作成します。

const record = require('node-record-lpcm16');
const fs = require('fs');

const filename = 'test.raw';
const file = fs.createWriteStream(filename);

const encoding = 'LINEAR16';
const sampleRate = 16000;

record.start({
  sampleRateHertz: sampleRate,
  encoding: encoding,
  recordProgram: 'sox'
}).pipe(file);

setTimeout(function () {
  record.stop();
}, 7000);

index.jsを実行します。

node index.js

実際にtest.rawファイルが吐き出されたら音声ファイルの取り込みは成功です。
rest.rawの確認方法として、Audacityを使います。

Audacityを起動して、ファイル>取り込み>Rawデータの取り込みを選択します。
test.rawを選択すると、サンプリング周波数の入力画面が表示されるので16000を入力します。
取り込みが完了したら再生を押して音声が録音されているか確認できます。

問題なく音声が取り込まれていることを確認したら、次にそれをテキストに変えてConsole画面に表示させる処理を書きていきます。

長くなったので残りは後日書きます!
読んでいただいてありがとうございました!

Node.js+Express+Azureでチャットアプリをデプロイしてみる2

それでは前回の続きで、サーバ側とクライアント側の通信時の処理を書いていきます。

サーバ側

まずポートを指定します。
ポートはアプリを実行する環境によって異なるので以下の形にする。
process.envは環境変数。process.env.PORTはユーザー環境のポート番号のこと。
また、環境変数を自分で設定する場合は、nodeコマンドを実行する前にターミナルから設定できる。

const PORT = process.env.PORT || 3000;

app.get(`/`, (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

//ここに通信時の処理を書く

server.listen(PORT, () => {
  console.log(`listening on *:${PORT}`);
});

通信時の処理を書いていく。 サーバ ー クライアント間のやり取りは、クライアントがsocket接続を要求することで開始される。
サーバがクライアントの接続を確立すると、サーバ側で'connection'イベントが発生する。(クライアント側では'connect'イベント)
このイベントは引数にsocketが渡される。
クライアントからイベントが発行されたときに受け取る処理をsocket.onを定義しておく。
ここでは、'chat message' というイベントが発行されてmsgを受け取った時の処理を定義している。
そしてクライアントからイベントを受け取ったあと、io.emitでサーバ側でイベントを発行する。

io.on('connection', (socket) => {
  console.log('a user connected');

  //socket処理 個別の要件を受け付けるイベントハンドラ 
  socket.on('chat message', (msg) => {
    console.log('message: ' + msg);
    //emit → 自分を含む全員にメッセージを送信する
    io.emit('chat message', msg);
  });

});

クライアント側

まずはsocket.ioに接続。
フォームが送信されたらemitでイベントを発行して、受け渡す値を指定。($('#m').val())
次にフォームを空にして処理はいったん終了。この処理をサーバ側のonイベントで受け取る。
そしてサーバ側で処理が完了し、サーバ側からemitでイベントが発行された後に受け取るonイベントを設定する。
ここでは、イベントを受け取ってmsgをHTMLに挿入する処理を書いている。

<script>
      const socket = io();
      $('form').submit(() => {
        socket.emit('chat message', $('#m').val()) //chat messageイベント実行
        $('#m').val("");
        return false;
      })
      socket.on('chat message', (msg)=> {
        $('#messages').append($('<li>').text(msg)); //jqueryでメッセージを追加
      })
</script>

ざっくりだけどサーバ ー クライアント間の処理の流れを書きました。

Node.js+Express+Azureでチャットアプリをデプロイしてみる

socket.ioとAzureの使い方を勉強したくて簡単なチャットアプリを作りました。
とはいえ全く知識ないのでとりあえずsocket.ioのGet-startedを利用しました。

自分がわかりやすいように元々のソースを少し触っています。
長くなりそうなので2回に分けて説明しようと思います。今回は前準備編。

サーバ側の設定

index.js

// var app = require('express')(); 分かりづらかったので分解
const express = require('express');
const app = express();

// const http = require('http').Server(app); 分かりづらかったので分解
const http = require('http');
const server = http.Server(app);

// const io = require('socket.io')(server); 分かりづらかったので分解
const socketio = require('socket.io');
const io = socketio.listen(server);

const PORT = process.env.PORT || 3000;

app.get(`/`, (req, res) => {
  res.sendFile(__dirname + '/index.html');
});

io.on('connection', (socket) => {
  console.log('a user connected');
  socket.on('chat message', (msg) => {
    console.log('message: ' + msg);
    io.emit('chat message', msg);
  });
});

server.listen(PORT, () => {
  console.log(`listening on *:${PORT}`);
});

クライアント側の設定

index.html

<body>
  <ul id="messages"></ul>
  <form action="">
    <input id="m" autocomplete="off" /><button>Send</button>
  </form>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://code.jquery.com/jquery-3.1.1.slim.js"   integrity="sha256-5i/mQ300M779N2OVDrl16lbohwXNUdzL/R2aVUXyXWA=" crossorigin="anonymous"></script>
  <script>
    const socket = io();
    $('form').submit(() => {
      socket.emit('chat message', $('#m').val());
      $('#m').val('');
      return false;
    });
    socket.on('chat message', (msg) => {
        $('#messages').append($('<li>').text(msg));
    });
  </script>
</body>

上のソースの解説

 サーバ側

サーバ側でまずやらないといけないことは、Websocket機能を持つサーバオブジェクトの用意です。  

そのためにまずExpressモジュールを呼び出してExpressオブジェクトをappに格納します。

const express = require('express');
const app = express();

次にServerオブジェクトを作成して引数にapp(つまりExpressオブジェクト)を渡します。これ、ExpressはappをHTTPサーバに渡せる関数ハンドラに初期化するってことらしいんだけどピンと来ないです。。

const http = require('http');
const server = http.Server(app);

そしてsocket.ioモジュールを呼び出して、ソケットとサーバを紐づけます。

const socketio = require('socket.io');
const io = socketio.listen(server);

これでソケット機能を持ったサーバが用意できました。

クライアント側

クライアント側では、Websocketに接続することが必要になります。
そのための記述をまずは書いていきます。

まずsocket.io.jsとjqueryを読ませます。

<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-3.1.1.slim.js"   integrity="sha256-5i/mQ300M779N2OVDrl16lbohwXNUdzL/R2aVUXyXWA=" crossorigin="anonymous"></script>

次にscriptタグ内で、以下の記述でsocket.ioに接続します。公式にはTIP: io() with no args does auto-discoveryって書いてあったけどこれはargなしで自動でsocket.ioを検出するということで合ってるだろうか。。

<script>
  const socket = io();
</script>

ここまで書けたら、サーバ側、クライアント側ともに通信時の処理を書いていきます。
簡単にまとめちゃうとonとemitでやりとりします。
クライアントが最初にemitでイベント発行→それをサーバ側のonで受け取り→続いてサーバ側でemit→クライアント側のonで受け取るって感じです。
詳しくは次回書きます。

そういえば、Websocket機能付きサーバを用意する過程で、色々console.logに出してみました。

//console.log(express);

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}
//console.log(app);

function (req, res, next) {
  app.handle(req, res, next);
}
//console.log(http);

{ _connectionListener: [Function: connectionListener],
  METHODS:
   [ 'ACL',
     'BIND',
     'CHECKOUT',
     'CONNECT',
     'COPY',
     'DELETE',
     'GET',
     'HEAD',
     'LINK',
     'LOCK',
     'M-SEARCH',
     'MERGE',
     'MKACTIVITY',
     'MKCALENDAR',
     'MKCOL',
     'MOVE',
     'NOTIFY',
     'OPTIONS',
     'PATCH',
     'POST',
     'PROPFIND',
     'PROPPATCH',
     'PURGE',
     'PUT',
     'REBIND',
     'REPORT',
     'SEARCH',
     'SUBSCRIBE',
     'TRACE',
     'UNBIND',
     'UNLINK',
     'UNLOCK',
     'UNSUBSCRIBE' ],
  STATUS_CODES:
   { '100': 'Continue',
     '101': 'Switching Protocols',
     '102': 'Processing',
     '200': 'OK',
     '201': 'Created',
     '202': 'Accepted',
     '203': 'Non-Authoritative Information',
     '204': 'No Content',
     '205': 'Reset Content',
     '206': 'Partial Content',
     '207': 'Multi-Status',
     '208': 'Already Reported',
     '226': 'IM Used',
     '300': 'Multiple Choices',
     '301': 'Moved Permanently',
     '302': 'Found',
     '303': 'See Other',
     '304': 'Not Modified',
     '305': 'Use Proxy',
     '307': 'Temporary Redirect',
     '308': 'Permanent Redirect',
     '400': 'Bad Request',
     '401': 'Unauthorized',
     '402': 'Payment Required',
     '403': 'Forbidden',
     '404': 'Not Found',
     '405': 'Method Not Allowed',
     '406': 'Not Acceptable',
     '407': 'Proxy Authentication Required',
     '408': 'Request Timeout',
     '409': 'Conflict',
     '410': 'Gone',
     '411': 'Length Required',
     '412': 'Precondition Failed',
     '413': 'Payload Too Large',
     '414': 'URI Too Long',
     '415': 'Unsupported Media Type',
     '416': 'Range Not Satisfiable',
     '417': 'Expectation Failed',
     '418': 'I\'m a teapot',
     '421': 'Misdirected Request',
     '422': 'Unprocessable Entity',
     '423': 'Locked',
     '424': 'Failed Dependency',
     '425': 'Unordered Collection',
     '426': 'Upgrade Required',
     '428': 'Precondition Required',
     '429': 'Too Many Requests',
     '431': 'Request Header Fields Too Large',
     '451': 'Unavailable For Legal Reasons',
     '500': 'Internal Server Error',
     '501': 'Not Implemented',
     '502': 'Bad Gateway',
     '503': 'Service Unavailable',
     '504': 'Gateway Timeout',
     '505': 'HTTP Version Not Supported',
     '506': 'Variant Also Negotiates',
     '507': 'Insufficient Storage',
     '508': 'Loop Detected',
     '509': 'Bandwidth Limit Exceeded',
     '510': 'Not Extended',
     '511': 'Network Authentication Required' },
  Agent:
   { [Function: Agent]
     super_:
      { [Function: EventEmitter]
        EventEmitter: [Object],
        usingDomains: false,
        defaultMaxListeners: [Getter/Setter],
        init: [Function],
        listenerCount: [Function] },
     defaultMaxSockets: Infinity },
  ClientRequest: { [Function: ClientRequest] super_: { [Function: OutgoingMessag
e] super_: [Object] } },
  globalAgent:
   Agent {
     domain: null,
     _events: { free: [Function] },
     _eventsCount: 1,
     _maxListeners: undefined,
     defaultPort: 80,
     protocol: 'http:',
     options: { path: null },
     requests: {},
     sockets: {},
     freeSockets: {},
     keepAliveMsecs: 1000,
     keepAlive: false,
     maxSockets: Infinity,
     maxFreeSockets: 256 },
  IncomingMessage:
   { [Function: IncomingMessage]
     super_:
      { [Function: Readable]
        ReadableState: [Function: ReadableState],
        super_: [Object],
        _fromList: [Function: fromList] } },
  OutgoingMessage:
   { [Function: OutgoingMessage]
     super_:
      { [Function: Stream]
        super_: [Object],
        Readable: [Object],
        Writable: [Object],
        Duplex: [Object],
        Transform: [Object],
        PassThrough: [Object],
        Stream: [Object],
        _isUint8Array: [Function: isUint8Array],
        _uint8ArrayToBuffer: [Function: _uint8ArrayToBuffer] } },
  Server: { [Function: Server] super_: { [Function: Server] super_: [Object] } }
,
  ServerResponse: { [Function: ServerResponse] super_: { [Function: OutgoingMess
age] super_: [Object] } },
  createServer: [Function: createServer],
  get: [Function: get],
  request: [Function: request] 
}
//console.log(server);

Server {
  domain: null,
  _events:
   { connection: [Function: connectionListener],
     close: [Function: bound ],
     listening: [Function: bound ],
     upgrade: [Function],
     request: [Function] },
  _eventsCount: 5,
  _maxListeners: undefined,
  _connections: 1,
  _handle:
   TCP {
     bytesRead: 0,
     _externalStream: [External],
     fd: -1,
     reading: false,
     owner: [Circular],
     onread: null,
     onconnection: [Function: onconnection],
     writeQueueSize: 0 },
  _usingSlaves: false,
  _slaves: [],
  _unref: false,
  allowHalfOpen: true,
  pauseOnConnect: false,
  httpAllowHalfOpen: false,
  timeout: 120000,
  keepAliveTimeout: 5000,
  _pendingResponseData: 0,
  maxHeadersCount: null,
  _connectionKey: '6::::3000',
  [Symbol(asyncId)]: 4 
}
//console.log(io);
//console.log(server)の内容も含んだサーバオブジェクトです。

Server {
  nsps:
   { '/':
      Namespace {
        name: '/',
        server: [Object],
        sockets: [Object],
        connected: [Object],
        fns: [],
        ids: 0,
        rooms: [],
        flags: {},
        adapter: [Object],
        _events: [Object],
        _eventsCount: 1 } 
    },
  parentNsps: Map {},
  _path: '/socket.io',
  _serveClient: true,
  parser:
   { protocol: 4,
     types:
      [ 'CONNECT',
        'DISCONNECT',
        'EVENT',
        'ACK',
        'ERROR',
        'BINARY_EVENT',
        'BINARY_ACK' ],
     CONNECT: 0,
     DISCONNECT: 1,
     EVENT: 2,
     ACK: 3,
     ERROR: 4,
     BINARY_EVENT: 5,
     BINARY_ACK: 6,
     Encoder: [Function: Encoder],
     Decoder: [Function: Decoder] 
    },
  encoder: Encoder {},
  _adapter: [Function: Adapter],
  _origins: '*:*',
  sockets:
   Namespace {
     name: '/',
     server: [Circular],
     sockets: { de5NSm3ugNqo1achAAAA: [Object] },
     connected: { de5NSm3ugNqo1achAAAA: [Object] },
     fns: [],
     ids: 0,
     rooms: [],
     flags: {},
     adapter:
      Adapter {
        nsp: [Object],
        rooms: [Object],
        sids: [Object],
        encoder: Encoder {} },
     _events: { connection: [Function] },
     _eventsCount: 1 
    },
  eio:
   Server {
     clients: { de5NSm3ugNqo1achAAAA: [Object] },
     clientsCount: 1,
     wsEngine: 'ws',
     pingTimeout: 5000,
     pingInterval: 25000,
     upgradeTimeout: 10000,
     maxHttpBufferSize: 100000000,
     transports: [ 'polling', 'websocket' ],
     allowUpgrades: true,
     allowRequest: [Function: bound ],
     cookie: 'io',
     cookiePath: '/',
     cookieHttpOnly: true,
     perMessageDeflate: { threshold: 1024 },
     httpCompression: { threshold: 1024 },
     initialPacket: [ '0' ],
     ws:
      WebSocketServer {
        domain: null,
        _events: {},
        _eventsCount: 0,
        _maxListeners: undefined,
        options: [Object] },
     _events: { connection: [Function: bound ] },
     _eventsCount: 1 
    },
  httpServer:
   Server {
     domain: null,
     _events:
      { connection: [Function: connectionListener],
        close: [Function: bound ],
        listening: [Function: bound ],
        upgrade: [Function],
        request: [Function] },
     _eventsCount: 5,
     _maxListeners: undefined,
     _connections: 1,
     _handle:
      TCP {
        bytesRead: 0,
        _externalStream: [External],
        fd: -1,
        reading: false,
        owner: [Object],
        onread: null,
        onconnection: [Function: onconnection],
        writeQueueSize: 0 },
     _usingSlaves: false,
     _slaves: [],
     _unref: false,
     allowHalfOpen: true,
     pauseOnConnect: false,
     httpAllowHalfOpen: false,
     timeout: 120000,
     keepAliveTimeout: 5000,
     _pendingResponseData: 0,
     maxHeadersCount: null,
     _connectionKey: '6::::3000',
     [Symbol(asyncId)]: 4 
    },
  engine:
   Server {
     clients: { de5NSm3ugNqo1achAAAA: [Object] },
     clientsCount: 1,
     wsEngine: 'ws',
     pingTimeout: 5000,
     pingInterval: 25000,
     upgradeTimeout: 10000,
     maxHttpBufferSize: 100000000,
     transports: [ 'polling', 'websocket' ],
     allowUpgrades: true,
     allowRequest: [Function: bound ],
     cookie: 'io',
     cookiePath: '/',
     cookieHttpOnly: true,
     perMessageDeflate: { threshold: 1024 },
     httpCompression: { threshold: 1024 },
     initialPacket: [ '0' ],
     ws:
      WebSocketServer {
        domain: null,
        _events: {},
        _eventsCount: 0,
        _maxListeners: undefined,
        options: [Object] },
     _events: { connection: [Function: bound ] },
     _eventsCount: 1 
    } 
}