Old Sunset Days

Node.jsでSocket.ioによるリアルタイム双方向通信

日付 タグ node.js カテゴリ node.js

目次

Socket.ioによるリアルタイム双方向通信

通常HTTP通信であれば、クライアントからサーバーへリクストがあった際にレスポンスをサーバー側からクライアントへ返す。

しかし、チャット機能のようなものの場合、誰かが発言したことを知っているのは、その発言をしたクライアントと、サーバーのみ。

チャットに参加している他のクライアントは、サーバーからの通知がない限り、あるいはページを自身でリロードしない限り、他の人の発言内容がチャット上で更新されないのであれば、それはちょっと不便である。

そこでチャットの発言を知るサーバーから何かしら全てのクライアント側へ能動的に通知ができる仕組みが必要になってくる。

この必然性によって出てきた技術の1つがWebSocketというHTTPをベースにしたプロトコルである。クライアントとサーバーの間でコネクションを確立した後、そのコネクションを維持しつつけ、双方向に通信をできる状態にする。フレームというデータ単位でデータ通信を行うので、HTTPに比べてデータ量が抑えられる。

しかし、双方向通信を可能にするには他にもロングポーリング(Comet)といったクライアントからサーバーに更新が来たら通知するようにというリクエストを投げておくような方式などもあり、ブラウザによってサポートしている、していないなどがあって、複雑な状況だった。

その複雑な状況を解決するために出てきたのがSocket.io。プログラムを書く側がWebsocketが使われているのか、ポーリングで実現されているのか意識をせずにブラウザがサポートしてるプロトコルに勝手にスイッチして双方向通信を実現できるようにしてくれる仕組みだ。

Node.jsではSocket.ioが利用できるので、今回はこのSocket.ioを使って双方向通信の実現方法を見ていく。

http://socket.io/ - socket.io公式

なお、今回作ったサンプルのソースコードは参照用としてGithubの以下に置いてある。 https://github.com/hugodeblog/node-socketio

Socket.ioで今回実装するサンプル

今回はページに「いいね」ボタンがあり、ボタンをクリックするといいね数が増えるというサンプルにしてみた。 同じ人が何度もボタンを押せるし、そのカウント数をあえてリアルタイムで全クライアントへ更新通知する必要があるかというと、 その必然性はないのだが、あくまでも簡単な例のサンプルということで。

実際にはこのサンプルをもとにチャットアプリ等へ応用していくのがいいだろう。 まずはアプリの雛形として、ボタンが1個あるページをHTMLとして返すサーバーを作る。

最初に適当な作業ディレクトリを作成して、npm initでpackage.jsonを初期化しておく。

$ mkdir node-socketio
$ cd node-socketio
$ npm init --yes

最初にホームページとしてindex.html を準備する。

index.html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>カウンターページ</title>
  </head>
  <body>
    <h1>いいね数のカウント</h1>
    <p>このページのいいね数
    <div id="loveNum">0</div>
    </p>
    <input id="submitLove" type="button" value="いいね"/>
  </body>
</html>

そして、アクセスがあると、このindex.html を返すサーバー実装が必要だ。
今回もES6形式のimport文を利用していく。


server.mjs

import http from 'http';
import fs from 'fs';

const server = http.createServer();

server.on('request', (req, res) => {
  fs.readFile('index.html', 'utf-8', (error, data) => {
    res.writeHead(200, {'Content-Type' : 'text/html'});
    res.write(data);
    res.end();
  });
});

server.listen(3000);

この状態で一度サーバーを起動して様子を見てみる。

$ node server.mjs

では、http://localhost:3000 を開いてみよう。

この時点では、いいね、ボタンこそあるものの、ボタンをクリックしても何も起きない。

Socket.ioでいいねカウンターをインクリメント

Socket.ioを利用することで

  1. クライアント(ブラウザ)でいいねボタンが押される
  2. クライアント(ブラウザ)からサーバーへいいねボタン押した通知
  3. サーバー側でいいねカウンターをインクリメント
  4. サーバー側から全クライアント(全ブラウザ)に最新のいいね数を通知
  5. ブラウザ(クライアント)側で表示を更新

上記の流れで最新のいいね数をどのクライアント(ブラウザ)で見ていても表示が最新のものに更新されるようにしてみよう。

まずsocket.ioを使うためのパッケージをインストール。

$ npm install socket.io --save

いまだとsocket.ioの3.xがインストールされる。2.xを利用したい場合は、

$ npm install socket.io@2.x --save

とすればいいが、今回は3.xのバージョンを利用する。

package.json
{
  "name": "node-socketio",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "socket.io": "^3.0.3"
  }
}


index.html

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>カウンターページ</title>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
  </head>
  <body>
    <h1>いいね数のカウント</h1>
    <p>このページのいいね数
    <div id="loveNum">0</div>
    </p>
    <input id="submitLove" type="button" value="いいね"/>
    <script>
    $(document).ready(function(){
      var socket = io('/home');
      socket.on('connect', socket => {
        console.log('socket connection: client');
      });

      // サーバーから既存のいいね数が来た
      socket.on('fistlove', function(data, fn) {
        console.log('fistlove', data);
        $('#loveNum').empty();
        $('#loveNum').text(data.num);
        fn('最初のデータはクライアント側で受け取ったよ');
      });

      // サーバーからいいね数更新が来た
      socket.on('updatelove', function(data) {
        console.log('updatelove', data);
        $('#loveNum').empty();
        $('#loveNum').text(data.num);
      });

      // サーバーへいいね数インクリを通知
      $('#submitLove').on('click', function(event) {
        socket.emit('submitlove', {
          message: 'いいね'
        });
      });
    });
    </script>
  </body>
</html>


server.mjs

import http from 'http';
import fs from 'fs';
import { Server } from 'socket.io';

const server = http.createServer();

var counter = 0;

server.on('request', (req, res) => {
  fs.readFile('index.html', 'utf-8', (error, data) => {
    res.writeHead(200, {'Content-Type' : 'text/html'});
    res.write(data);
    res.end();
  });
});

// socket.io通信のため
const io = new Server(server);

io.of('/home').on('connect', socket => {
  console.log('socket connection: server');
  console.log('connected by:', socket.id);
  socket.emit('fistlove', {num: counter}, function (data) {
    console.log(`response by client: ${data}`);
  });

  socket.on('submitlove', data => {
    console.log('submitlove called', data);
    console.log(`${data.message} by ${socket.id}`);
    counter++;
    io.of('/home').emit('updatelove', {num: counter});
  });
});

server.listen(3000);

クライアント(ブラウザ)側のコード

ここでクライアント(ブラウザ)側のコードindex.html の作りについてちょっと見ておく。

Socket.ioに加えて、HTMLのDOM操作をしてカウンターの数を書き換えるのでjQueryも使用する。

<script src="//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>

サーバーとSocket.ioのための接続は以下。
/home というnamespace付きで接続を作成している。

var socket = io('/home');
socket.on('connect', socket => {
  console.log('socket connection: client');
});

そして「いいね」ボタンを押した時にブラウザからサーバーへ通知する部分。

submitLoveボタンが押されると、ボタンクリックイベントが検知されて、socket.emit でsubmitloveイベントがサーバーへ通知される。

引数としてはmessage に「いいね」文字列を設定してサーバーへ送っている。
ここはあくまでもテスト的に文字列を入れただけなのでメッセージは本サンプルでは何でもいい。

$('#submitLove').on('click', function(event) {
  socket.emit('submitlove', {
    message: 'いいね'
  });
});

そして、サーバーから「いいね」数カウンターの更新があったことを受け取る部分。

socket.on('updatelove', function(data) {
  console.log('updatelove', data);
  $('#loveNum').empty();
  $('#loveNum').text(data.num);
});

<div id="loveNum">0</div> の部分をサーバーから通知が来たカウンター数で書き換え更新。

追加で、最初にSocket.ioで接続あった時にすでに既存の「いいね」数を即更新できるように、

socket.on('fistlove', function(data, fn) {
  console.log('fistlove', data);
  $('#loveNum').empty();
  $('#loveNum').text(data.num);
  fn('最初のデータはクライアント側で受け取ったよ');
});

初回接続のみに発行されるイベントを受け取るコードも用意していおく。これはupdateloveとほぼ同じ内容。

なお、ここでfn はブラウザ側ではなく、サーバー側で実行させるためのコードであり、そこに「最初のデータはクライアント側で受け取ったよ」を引数として送信している。

サーバー側のコード

次にサーバー側のコードserver.mjs の作りを見ていく。

var counter = 0;

いいね数のカウンターを保持。

socket.io 3.xを使う場合のimportはこんな風になる。

import { Server } from 'socket.io';

そして、Socket.ioのためのSeverオブジェクトを作成して、接続イベントconnect を待つ。 コンストラクタの引数には、createServerで作成されたserver を引数としている。 ここでもクライアント(ブラウザ)側と合わせて/home のnamespace付きで接続待ちにしている。

const io = new Server(server);

io.of('/home').on('connect', socket => {
  console.log('socket connection: server');
  console.log('connected by:', socket.id);
  socket.emit('fistlove', {num: counter}, function (data) {
    console.log(`response by client: ${data}`);
  });

function (data) は先ほどブラウザ側のコードで見たfn('最初のデータはクライアント側で受け取ったよ') と対応している。

初回のブラウザからのSocket.io接続時にsocket.emit('fistlove') で既存のカウンターを即時通知するうにしている。

socket.on('submitlove', data => {
  console.log('submitlove called', data);
  console.log(`${data.message} by ${socket.id}`);
  counter++;
  io.of('/home').emit('updatelove', {num: counter});
});

こちらは「いいね」ボタンが押された通知をブラウザから受け取った時の挙動。 io.of('/home').emit('updatelove') で全クライアント(ブラウザ)に対してupdateloveイベントの通知をしている。

実際の動作挙動チェック

それではサーバーを起動して動作チェックしてみよう。

右のブラウザのみ、「いいね」ボタンを押しているが、サーバーで保持するいいねカウンターの値が左のブラウザにもSocket.ioで反映されているのが確認できる。

$ node server.mjs
socket connection: server
connected by: oojipIilnOV4ueaiAAAB
response by client: 最初のデータはクライアント側で受け取ったよ
socket connection: server
connected by: 7-Uv_VUTScpR6gCPAAAD
response by client: 最初のデータはクライアント側で受け取ったよ
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD
submitlove called { message: 'いいね' }
いいね by 7-Uv_VUTScpR6gCPAAAD

以上でSocket.ioを使った簡単なサンプルの動作検証が確認できた。