チャットアプリの作成:② WebSocket

チャットアプリで必要となるリアルタイムの双方向通信をおこなうためのWebSocketプロトコルの利用についてメモを残します。ここではまだチャットアプリは完成しません。

  • Socket.ioのインストール
  • Socket.ioの設定
  • クライアント側のSocket.io
  • 実験
  • index.html
  • クライアント側コード (chat.js)
  • サーバー側コード (index.js)
  • 解説

Socket.ioのインストール

Socket.io は、ブラウザとサーバー間のリアルタイム、双方向、イベントベースの通信を可能にするライブラリで、Node.jsサーバーとJavaScriptクライアントライブラリから構成されます。

以下のコマンドでインストールします。

> npm i socket.io@2.3.0

Socket.ioの設定

Socket.ioは単独でも使用できますが、Expressと一緒に使うこともできます。

前回は次のようなコードを index.js に記述していました。

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

ここでは、Socket.ioExpress を共に使う方式を採用するため、上記コードを次のように変更します。

const path = require('path')
const http = require('http')
const express = require('express')
const socketio = require('socket.io')

// Expressアプリケーションの生成
const app = express() 

// Expressアプリケーションを用いてのHTTPサーバーの生成
const server = http.createServer(app)

// HTTPサーバーに socket.io を接続
const io = socketio(server) 

クライアント側のSocket.io

Socket.ioは、クライアント側のコードを含む/socket.io/socket.io.js を自動的に提供するので、以下のスクリプトにより、例えば自分が作成した js/chat.js にロードできます。また、こちらのCDN(Content Delivery Network)から利用することもできます。

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

実験

チャットアプリを作る前に、ソケット通信の実験をします。

この実験では、どちらのブラウザの[いいね!]ボタンをクリックしても連動していいねの数が1づつカウントアップしていくようにしました。 ブラウザを複数立ち上げ、それぞれでlocalhost:3000 を表示させ、npm run dev を実行します。

チャットアプリに至る前のソケット通信の実験

このような動作となるコードを以下に示し、その後解説します。

index.html

<!DOCTYPE html>
<html lang="ja"">
<head>
    <meta charset="UTF-8">
    <title>チャットアプリ</title>
</head>
<body>
    <h1>チャットアプリ!</h1>
    <p id="num-likes"></p>
    <button id="like">いいね!</button>
    <script src="/socket.io/socket.io.js"></script>
    <script src="/js/chat.js"></script>
</body>
</html> 

クライアント側コード (chat.js)

const socket = io()

const numLikes = document.querySelector('#num-likes')

socket.on('countUpdated', (count) => {
    numLikes.textContent = `${count} いいね`
})

document.querySelector('#like').addEventListener('click', () => {
    socket.emit('like')
}) 

サーバー側コード (index.js)

const path = require('path')
const http = require('http')
const express = require('express')
const socketio = require('socket.io')

// Expressアプリケーションの生成
const app = express() 

// Expressアプリケーションを用いてのHTTPサーバーの生成
const server = http.createServer(app)

// HTTPサーバーに socket.io を接続
const io = socketio(server)

const port = process.env.port
const publicDirectoryPath = path.join(__dirname, '../public')

app.use(express.static(publicDirectoryPath))

let count = 0

io.on('connect', (socket) => {
    console.log('新しい WebSocket 接続')

    socket.emit('countUpdated', count) // 全てのブラウザに送られない

    socket.on('like', () => {
        count++
        io.emit('countUpdated', count)  // 全てのブラウザに送られる
    })
})

server.listen(port, () => {
    console.log(`Server is up on port ${port}`)
}) 

解説

今回の実験では、ブラウザからサーバーとサーバーから(複数の)ブラウザの2方向の合計2種類の通信をおこなっています。

通信では送信側がクライアント、受信側がサーバーと呼ばれる役割を持ち、ブラウザはサーバーにもクライアントにもなり、同様にNode.jsサーバーのコードも同じく通信のクライアントにもサーバーにもなりますが、以下ではブラウザのコードをブラウザ、 Node.jsサーバー のコードをサーバーと呼ぶことにします。

サーバーの準備

サーバーは Socket.io が提供する io.on によりイベントを受信する体制に入ります。

そして、クライアントがサーバーに接続した際に発生するイベント’connect’で次のコードが実行されます。

io.on('connect', (socket) => {
    :
}) 

ブラウザの準備

ブラウザのJavaScriptは、 ブラウザ側のSocket.ioライブラリが提供するioを呼び出すことでSocket.ioサーバーに接続できます。

ブラウザでは socket.on で名前で指定したイベントを待ち受けます。

const socket = io()
:
socket.on('countUpdated', (count) => {
    :
})

サーバーからブラウザへ送信(countUpdatedイベント)

特定のブラウザがサーバーに接続したことを io.onで受信したら、socket.emit を用いて、その特定のブラウザに対して指定したイベント(この場合 countUpdatedイベント)を送信します。

io.on('connect', (socket) => {
    console.log('新しい WebSocket 接続')
    socket.emit('countUpdated', count) // 全てのブラウザに送られない
    :

ブラウザでの受信(countUpdatedイベント)

ブラウザは下記コードにてcoutUpdatedイベントを受信し、渡されてきたいいね数を表示します。

 socket.on('countUpdated', (count) => {
    numLikes.textContent = `${count} いいね`
})

ブラウザからサーバーへ送信(likeイベント)

[いいね!]ボタンがクリックされたら、socket.emit を用いて、ここでは likeという名前のイベントを送信します。

サーバーでの受信(countUpdatedイベント)

サーバーでは likeイベントをsocket.on で受信し、count変数を+1したあと、全ブラウザに対してcountUpdatedイベントを送信します。こちらに用いる emit は全てのブラウザに対して送られるもので、上の「サーバーからブラウザへ送信(countUpdatedイベント)」で用いている socket.emit とは別のものであることに注意します。

socket.on('like', () => {
    count++
    io.emit('countUpdated', count)  // 全てのブラウザに送られる
})