はじめてのSocket.io #1 「リアルタイムなチャットを作る」

Node.jsのキラーソフトの一つとも言われて久しいSocket.ioを使ってリアルタイム通信を行います。コツさえ分かれば非常に簡単に開発できてしまうのでプロトタイプや小規模なプロジェクトにオススメ。

今回は以下の動画のように複数のクライアント間でリアルタイムに通信が行える簡易チャットをHTML+JavaScriptで作成してみます。


www.youtube.com

基本的な解説

Socket.ioは何をやってるの?

めちゃめちゃざっくり言うと、Socket.ioサーバへ接続しているユーザー間で情報を共有する仕組みを提供してくれます。

上図でいうとアルパカさんが発言した内容をSocket.ioがいったん受け取り、パンダさんとペンギンさんに送信してくれます。Socket.ioの良いところは情報が更新されたらサーバが勝手に教えてくれる点です。パンダさんやペンギンさんは情報が更新されているか定期的にサーバを覗きに行く必要がありません。非常に楽ちんですね。なおここでは例として3人が登場していますが、サーバや回線が許す限り接続するユーザー数は増やすことができます。

今回作成するチャットはこの仕組を利用し、誰かが発言をするといったんSocket.ioサーバへ情報を送信し、Socket.ioサーバが接続中の全ユーザーにお知らせして回る処理を行います。

制約や注意点

制約としては全ユーザーがネットワークに接続されたオンライン状態であり、Socket.ioサーバに接続しっぱなしになっている必要があります。また基本的にはほぼリアルタイムな通信が実現できますが、ユーザーの環境によっては遅延などが発生することもあります。サーバを建てて運用する必要が出てきますのでその分の料金も発生します。

P2Pじゃダメなの?

P2Pなどで直接クライアント同士で通信すればサーバ不要なのでは……?という疑問が出てくるのはもっともで、誰もが一度は思いを巡らすわけですが、以下のような制約があり中々簡単には行きません。

  1. NATやFirewallなどが間に存在し相手と直接通信が出来ない
    • STUNサーバなどを用いて回避するわけですが結局サーバが必要になる
  2. 意図せずクライアントが切断された際に再度同じ状態にすることが難しい
    • 特にスマホなどの移動が前提の環境だとプッツンプッツン切れることを念頭に置く必要あり
    • クライアントの1人がホスト役になっている場合、ホスト役が抜けると他の誰かにそれを割り当てるなどの処理が発生
  3. オンラインゲームの場合はチート(ズル)がしやすくなる
    • チートが蔓延するとサービス存続の危機に陥いる(誇張ではなく)
    • 一般的にサーバ内でチート防止のチェックなどを行っています。

このあたりはトレードオフですね。P2PにはP2Pの、サーバにはサーバの良さがありますので要件によって取捨選択します。

さて御託はここまでにして、ここからは実際に動かして見ることにします。

環境構築

今回はまずはローカルで動かすことにします。ある程度動くようになったら実際のサーバで公開しましょう。

インストール

Node.jsがまだ入っていない方はひとまず安定版をインストールしておいてください。

GitHubなどに適当なリポジトリを作成しローカルへclone。ディレクトリ内へ移動します。npm initでこのプロジェクトの情報をまとめたpackage.jsonを作成する際に色々聞かれますが、いずれも後で変更できるのでとりあえず動かしたい場合はEnterキーを連打でもOKです。

$ git clone git@github.com:katsube/socketio-chat.git
$ cd socketio-chat
$ npm init

最後にnpm installでSocket.ioと、依存関係にあるExpressを入れます。ExpressはWebサーバを簡単に作成できる便利フレームワークです。

$ npm install socket.io express

package.jsonのdependenciesをのぞいてインストールされたバージョンを確認しておきます。今回は以下が入りました。

$ cat package.json
 (中略)
  "dependencies": {
    "express": "^4.17.1",
    "socket.io": "^2.3.0"
  }

ついでに.gitignoreも準備し、git commit/pushしておきます。

$ cat .gitignore
node_modules/

$ git add .
$ git commit -m '1st commit'
$ git push

サンプルコード

まずは最低限のコードを実行してみます。Node.jsでWebサーバ兼Socket.ioサーバを同時に起動します。

なおソースはGitHub上からも参照できます。 github.com

serve.js

Node.jsで実行するためのJavaScriptです(Webブラウザ上で実行はしません)。ファイル名は何でも良いのですがここではserve.jsと付けました。

const app  = require("express")();
const http = require("http").createServer(app);
const io   = require("socket.io")(http);

/**
 * "/"にアクセスがあったらindex.htmlを返却
 */
app.get("/", (req, res)=>{
  res.sendFile(__dirname + "/index.html");
});

/**
 * [イベント] ユーザーが接続
 */
io.on("connection", (socket)=>{
  console.log("ユーザーが接続しました");

  socket.on("post", (msg)=>{
    io.emit("member-post", msg);
  });
});

/**
 * 3000番でサーバを起動する
 */
http.listen(3000, ()=>{
  console.log("listening on *:3000");
});

index.html

今回はクライアントをHTMLで用意しましたが、他の言語や環境でも作れたりします。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>SocketIOチャット</title>
</head>
<body>

<h1>SocketIOチャット</h1>

<!-- 発言フォーム -->
<form id="frm-post">
  <input type="text" id="msg">
  <button>送信</button>
</form>

<!-- 発言ログ -->
<ul id="msglist">
</ul>


<script src="/socket.io/socket.io.js"></script>
<script>
  //-------------------------------------
  // Socket.ioサーバへ接続
  //-------------------------------------
  const socket = io();

  /**
   * [イベント] フォームが送信された
   */
  document.querySelector("#frm-post").addEventListener("submit", (e)=>{
    // 規定の送信処理をキャンセル(画面遷移しないなど)
    e.preventDefault();

    // 入力内容を取得する
    const msg = document.querySelector("#msg");
    if( msg.value === "" ){
      return(false);
    }

    // Socket.ioサーバへ送信
    socket.emit("post", {text: msg.value});

    // 発言フォームを空にする
    msg.value = "";
  });

  /**
   * [イベント] 誰かが発言した
   */
  socket.on("member-post", (msg)=>{
    const list = document.querySelector("#msglist");
    const li = document.createElement("li");
    li.innerHTML = `${msg.text}`;
    list.insertBefore(li, list.firstChild);
  });

  /**
   * [イベント] ページの読込み完了
   */
  window.onload = ()=>{
    // テキストボックスを選択する
    document.querySelector("#msg").focus();
  }
</script>
</body>
</html>

実行する

サーバを起動

では早速動かしてみましょう。Terminalで先ほどのJavaScriptをnodeに渡すだけです。これでSocket.ioサーバが起動しました。

$ node serve.js
listening on *:3000

ブラウザから確認

あとはブラウザからhttp://localhost:3000へアクセスします。フォームが表示されたら実際に発言してみてください。この時、複数のタブまたはウィンドウを開いておくとわかりやすいですね。いずれかのウィンドウで発言すると他のウィンドウにもほぼ同時に反映されます。

サーバを終了

サーバを落としたいときはCtrl+cキーで強制的に終了させます。

$ node serve.js
listening on *:3000
^C

解説

大まかな原理

Socket.ioはサーバもクライアントもイベントが発生するとことで動作します。今回のチャットでは発言を行うとサーバに対して「post」イベントが発行するとともに発言を送信します。それを受け取ったサーバは全接続者に対して「member-post」イベントを発行するとともに発言を送信しているという流れになります。

これを踏まえてクライアントとサーバの実装を見て行きましょう。

  • 今回のコードでは「member-post」イベントはアルパカにも送られています

クライアント

ライブラリを取得

おや?っと思った方はするどいです。HTML側で読み込むSocket.io用のクライアントですがこれはサーバ側で何も準備をしなくても以下のパスにアクセスすれば取ってくることができます。

<script src="/socket.io/socket.io.js"></script>

ちなみにCDNから取ってくることもできます。サーバの負荷を少しでも下げたい場合などはこちらをどうぞ。サーバのSocket.ioのバージョンと必ず合うものを選んでくださいね。

<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>

サーバへ接続

最初に行うのはSocket.ioサーバへの接続です。以下の通りio()を実行したタイミングで接続が行われます。初見のときは単にオブジェクトでも取ってきてるのかと思ってましたよw 大胆やw

const socket = io();

デフォルトだと現在のドメインwindow.locationへ接続に行きますが、以下のように指定することも可能です。

const socket = io("http://example.com/");

Socket.ioサーバへ送信

Socket.ioサーバへの送信はsocket.emit()を実行するだけです。第1引数にイベント名、第2引数に送信する値を指定します。イベント名は文字列であれば自由に付けることができます。スペースなどが入ってもかまいません。送信する値もここではJSONを渡していますが単なる文字列や数値でもかまいません。

document.querySelector("#frm-post").addEventListener("submit", (e)=>{
  // 中略

  // Socket.ioサーバへ送信
  socket.emit("post", {text: msg.value});
});

Socket.ioサーバから受信

サーバからのデータを受信するには、サーバから送られてくるイベントに対する処理を事前に定義しておく必要があります。今回は「member-post」イベントがサーバから送られてきた場合の処理を定義しています。

socket.on("member-post", (msg)=>{
   // サーバから送られた値は msg を参照する
});

サーバ

クライアントの接続 - connection

ポイントは以下ですね。一番最初に発生するイベントは「connection」になります。サーバにクライアントが接続した際に発生するイベントです。Socket.ioにまつわる処理は基本的にこの中に書いていきます。

io.on("connection", (socket)=>{
  // ここに接続後の処理を記述する
});

クライアントへ一斉送信

接続後、前述の「post」イベントがクライアントから送られてくるとio.emit()を実行するという処理になっています。このメソッドが接続者全員に情報を送る機能になります。なんとたった1行で書けてしまうのです。第1引数がイベント名、第2引数がクライアントへ送信する値となります。

io.on("connection", (socket)=>{
 // postイベントが送られてきたら以下を実行
  socket.on("post", (msg)=>{
    io.emit("member-post", msg);
  });
});

クライアントから送られてきた情報はmsgを参照することで得られます。今回はクライアントから送られてきたデータをそのまま全員にemitしていますが、サーバ上で加工した上で送信することも可能です。

さらに環境を整える

開発中にサーバを自動的に再起動する

Socket.ioに限った話ではないのですが、Node.jsでサーバプログラムを動かしている最中に、サーバ側のコードを変更した際には、一度終了させてから再度起動しないと変更分が反映されません。めっちゃ面倒です。ハゲます。ハゲ不可避です。

そこで、ファイルが更新されると自動的にサーバを再起動してくれるnodemonを導入し頭皮を守護するのが良いでしょう。インストール時に-gオプションを付けるとグローバル領域に入ります。

$ npm install -g nodemon

インストールが完了したらnodeコマンドの代わりにnodemonで起動するだけです。

$ nodemon serve.js

基本的にnodemonは開発用ですので、本番運用時には利用しません。

Windowsの場合

Windowsで実行すると次のようなエラーメッセージが表示されてうまく動きません。

このシステムではスクリプトの実行が無効になっているため、ファイル C:\
Users\katsube\AppData\Roaming\npm\nodemon.ps1 を読み込むことができません。詳細
については、「about_Execution_Policies」(https://go.microsoft.com/fwlink/?Link
ID=135170) を参照してください。

発生場所 行:1 文字:1
+ nodemon
+ ~~~~~~~
    + CategoryInfo          : セキュリティ エラー: (: ) []、PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

Windowsで動かしたい場合は以下のようにnpxを先頭に付けることで実行できます。

PS C:\> npx nodemon serve.js

本番運用中にサーバを起動し続ける

Apacheなどの一般的なWebサーバの場合、プログラムに問題があり異常終了したとしても、複数ある子プロセスの1つが死ぬだけでAoache本体は稼働し続けてくれますが、Nodeでサーバプログラムを書いた場合は例外などが発生するとサーバそのものがダウンしてしまいます。開発時であれば良いのですが停止が許されない本番運用時には困り物。

そこで、Node製のサーバプログラムが異常終了した場合に自動的に再度起動してくれるforeverを導入します。

$ npm install -g forever

起動は以下の通り。あとはバックグラウンドで勝手に動いてくれますのでこのままTerminalを閉じても大丈夫です。

$ forever start serve.js

終了したい場合はオプションにstopを指定します。

$ forever stop serve.js

またforeverは複数のサーバを同時に扱えるのですが、どのサーバを起動しているのかわからなくなったり確認を行いたい場合はlistで一覧表示ができます。

$ forever list

続き

blog.katsubemakito.net

参考ページ