【Node.js】WebSocketサーバ と WebSocket API を使ってチャットもどきを作ってみよう!【サンプルコードあり】

WebSocket API を 使ってみよう!

この記事で学べること

WebSocketを使ったリアルタイム通信の仕組みを利用して、ブラウザ間のデータ更新をする方法

皆さん、こんにちは。どんぶラッコです。

お仕事の関係で WebSocket の通信をフロント側に実装する機会がありました。その際、 JavaScript の標準APIである WebSocket を使って通信をテストしたので、使い方をまとめておこうと思います!

そして、WebSocketを叩くためにはWebSocketの配信サーバが必要なので、ついでにWebSocketサーバも実装してみました笑

どちらのコードもgithubにアップロードしてあるので、興味ある方は確認してみてください!

今回のハンズオンを実行すると↓↓のようなリアルタイムチャットもどきを作ることができます。

ということで、早速作成手順を確認していきましょう♪

Node.jsの設定をする

まずはNode.js を使い、HTMLファイルを表示できるようにします

WebSocketサーバを構築する

websocket モジュールを使い、 WebSocketサーバの設定を行い wscatコマンド で挙動を確認します

HTMLファイルからWebSocket通信をする

これがやっと本題! JavaScriptのWebScoket API を使って WebSocketの情報を拾ってみます

事前準備

Node.js がすでにインストールされていることを確認してください

node --version

Node.jsの設定をする

ディレクトリ作成 & npm init

まずは、ディレクトリを作成し、 npm init します。これで色んなパッケージがDLできるようになります。

ディレクトリ作成 & npm init
mkdir ws-handson   # 任意のディレクトリを作成
cd ws-handson
npm init  # package.json ファイルの作成

npm init をすると聞かれる内容は全てデフォルトのままで今回はOKです。Enter キーを連打してください。

http インタフェイスを使って nodeサーバ構築

次に、NodeのHTTPインタフェイスを使ってHTTPリクエストを受けられるようにしておきましょう。

server.js を作成し、以下の内容を記述します。

server.js
const http = require('http')

// ポート番号
const PORT = 3000

// リクエスト・レスポンスの対応内容を記述
const server = http.createServer((request, response) => {
  response.writeHead(200)
  response.write('Hello!')
  response.end()
})

// リスナーを起動
server.listen(PORT, () => {
  console.log(`${new Date()} サーバ起動 http://localhost:${PORT}`)
})

今ディレクトリはこの状態ですね。

.
 ├── package.json
 └── server.js

ここまでできたら、一度 node サーバを起動してみましょう。 node コマンドをターミナルから実行します。

node サーバの起動コマンド
node server.js
Thu Jun 24 2021 20:29:09 GMT+0900 (Japan Standard Time) サーバ起動 http://localhost:3000

こんな表示が出れば成功です。

http://localhost:3000 にアクセスして Hello! と表示されていればOKです。

サーバを停止するときは ctrl + c で停止します。

HTML ファイルを表示できるようにする

次に、HTMLファイルを表示できるようにします。今回は http://localhost:3000 にアクセスしたら HTMLページを表示、それ以外のエンドポイント ( http://localhost:3000/hoge など ) にアクセスがあったら 404ページを返すようにしましょう。

今回HTMLは public というフォルダを作り、その中に記述していくことにします。

public フォルダを作成し、その中に index.html を作成します。

ディレクトリ構造はこんな感じですね。

.
 ├── package.json
 ├── public
 │   └── index.html
 └── server.js

public/index.html
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>WebSocket ハンズオン</title>
</head>
<body>
  <h1>HTML表示テスト</h1>
</body>
</html>

そして server.js の表記内容も更新します。 http.createServer の中身を書き換えましょう。

server.js
const http = require('http')
const fs = require('fs') // 追加: ファイルを読み取るモジュール

// 中略

const server = http.createServer((request, response) => {

  // !!---ここから書き換え---!!
  const url = request.url
  switch (url) {
    case '/':
      fs.readFile('./public/index.html', 'utf-8', (error, data) => {
        response.writeHead(200, { 'Content-Type': 'text/html' })
        response.write(data)
        response.end()
      })
      break
    default:
      response.writeHead(404)
      response.end()
  }
  // !!---ここまで---!!

})

// 以下略

ここまで変更が完了したら node server.js コマンドを叩いて確認してみましょう。

こんな表示が出ていれば成功です。

ついでに、 / 以外のエンドポイントを叩くと 404になっていることも確認します。

いい感じですね!

HTMLファイルが表示できるところまで確認できたので、次は Websokcet サーバの構築に移りましょう。

Websocket サーバを構築する

websocket を使ってサーバ構築

今回、 websocket-node モジュールを利用して作成します。

まずはモジュールをインストールします。

npm パッケージのインストール
npm i websocket

次に server.js に websokcetに関する記述を追記していきます。

まずは Websocket の開始と終了ができるように記述を追加していきます。

server.js
const http = require('http')
const fs = require('fs')
const WebSocketServer = require('websocket').server  // 追記: websocketモジュールの読み込み

// 中略

// リスナーを起動
server.listen(PORT, () => {
  console.log(`${new Date()} サーバ起動 http://localhost:${PORT}`)
})

// !!--- 以下追記部分 ---!!

// WebSocketサーバの設定
const wsServer = new WebSocketServer({
  httpServer: server,
  // autoAcceptConnections は本番環境で使っちゃだめ
  autoAcceptConnections: false
})

const originIsAllowed = (origin) => {
  // アクセス元が信頼できるかを検証する用の関数。今回はlocal環境なので常にtrue
  return true
}

wsServer.on('request', (request) => {
  if (!originIsAllowed(request.origin)) {
    request.reject()
    console.log(`${new Date()} ${request.origin} からのアクセスが拒否されました`)
  }

  const connection = request.accept('ws-sample', request.origin)
  console.log(`${new Date()} 接続が許可されました`)

  connection.on('close', (reasonCode, description) => {
    console.log(`${new Date()} ${connection.remoteAddress} が切断されました`)
  })
})

wscat コマンドを使って開通確認

ここまで追記できたら node server.js を叩いて再びサーバを起動して、きちんとwebsocketが接続確立しているかを確認してみましょう。

まだHTML側の実装ができていないので、 wscat コマンドを叩いて確認をしてみます。

インストールしていない方は npmコマンドを使ってグローバルにインストールしましょう。

wscat コマンドインストール
npm i -g wscat

wscat コマンドが使えるようになったら server.js を起動させたまま、下記コマンドを入力します。その後、ctrl + c で手動で接続を切断してみましょう。

コマンド
wscat -c ws://localhost:3000 -s ws-sample

このように、接続・切断のログが node server.js で立ち上げた側のスレッドに表示されれば成功です!

オプションの意味はそれぞれ -c が connect、 -s が subprotocol です。

ws-sampleserver.js の方で指定している文字列ですね。

const connection = request.accept('ws-sample', request.origin) // この部分

ws-sample 以外の サブプロトコルを指定すると、エラーハンドリングをしていないので、エラーが発生して nodeサーバ自体も止まってしまいます。

500番エラー (サーバエラー) が返ってきてるのが分かりますね。

ちなみに、 autoAcceptConnections: true にすると、サブプロトコルを指定しなくても websokcet通信が確立します。本番環境で使っちゃいけない理由がこれです。

メッセージ処理を実装する

さて、websocketが使えることがわかったのでwebsocketでメッセージを受け取り、送信する処理を書きましょう。

公式サンプルを参考に実装していきます。

server.js
  const connection = request.accept('ws-sample', request.origin)
  console.log(`${new Date()} 接続が許可されました`)

  // !!--- ここから追記 ---!!
  connection.on('message', message => {
    switch (message.type) {
      case 'utf8':
        console.log(`メッセージ: ${message.utf8Data}`)
        connection.sendUTF(message.utf8Data)
        break
      case 'binary':
        console.log(`バイナリデータ: ${message.binaryData.length}byte`)
        connection.sendBytes(message.binaryData)
        break
    }
  })
  // !!--- 追記終了 ---!!

メッセージの中身がテキストかバイナリデータかによって処理を分岐させています。

では、ここまで作ることができたら wscat コマンドを使って確かめてみましょう。

先ほど入力した

wscat -c ws://localhost:3000 -s ws-sample

を入力した後、好きなテキストを入力してみてください。

メッセージがサーバで認識されて、かつサーバからも送ったテキストと同じ内容が returnされてきていることがわかると思います。

これでサーバ側の準備は(やっと)整いました!あとはクライアント側に WebSocketの仕組みを構築していきます。

HTMLファイルからWebSocket通信をする

tailwindを使ってレイアウトを整える

今回は tailwind を使ってHTMLファイルを整えていきます。 STEP1で作ったHTMLファイルを書き換えます。これは長いので何も考えずにコピペでいいです。笑

index.html
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
  <title>WebSocket受け渡しテスト</title>
</head>
<body>
<main class="py-8 px-12 w-96 mx-auto bg-gray-100">
  <section class="my-4">
    <div>
      <span class="text-sm font-bold">WS Status</span>
      <div class="bg-indigo-200 p-2 mb-2">
        <span class="websocket-status">null</span>
      </div>
    </div>
  </section>
  <section class="my-4">
    <div class="bg-gray-300 p-2 rounded shadow">
      <div>
        <input
            class="appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline mb-2"
            id="send-message"
            type="text"
            placeholder="メッセージを入力"
        >
      </div>
      <div class="text-right">
        <button id="send-button" class="py-2 px-4 rounded bg-indigo-600 text-white">
          メッセージ送信
        </button>
      </div>
    </div>
  </section>
  <section class="my-4">
    <div class="text-sm font-bold">メッセージ一覧</div>
    <div id="messages">
    </div>
  </section>
</main>
</body>
</html>

こんな見た目のボックスが作成されるはずです。

WebSocketへの接続処理を書いてみる

そして、いよいよ JavaScriptを書いていきます。ファイルを分けてしまうと別途 server.js に読み込みの処理を書く必要があるので今回は index.html 内に書いてしまいます。 </body> の直前に書いていきましょう。

index.html
<script>
// 設定
const URL = 'ws://localhost:8888/'
const PROTOCOL = 'ws-sample'

// WebSocket 通信を開始する
const socket = new WebSocket(URL, PROTOCOL)

// ------------------------------
// WebSocket イベント
// ------------------------------

// WebSocket が開通したら発火する
// socket.onopen = () => {} でも可
socket.addEventListener('open', (event) => {
  console.log('open')
  socket.send('メッセージ') // メッセージの送信
})

// WebSocketサーバ からメッセージを受け取ったら発火する
// socket.onmessage = () => {} でも可
socket.addEventListener('message', ({ data }) => {
  console.log('message: ' + data)
  socket.close()
})

// WebSocketサーバ からエラーメッセージを受け取ったら発火する
// socket.onerror = () => {} でも可
socket.addEventListener('error', (event) => {
  console.log('error')
})

// WebSocket がクローズしたら発火する
// socket.onclose = () => {} でも可
socket.addEventListener('close', (event) => {
  console.log('close')
})

</script>
</body>

ここまでかけたら debug ツールを開いてメッセージを確認してみてください。

解説をしておくと、

// 設定
const URL = 'ws://localhost:8888/'
const PROTOCOL = 'ws-sample'

// WebSocket 通信を開始する
const socket = new WebSocket(URL, PROTOCOL)

この部分が先ほどの wscat コマンドと同じことをしている部分ですね!

その後、 addEventListner を使って、イベントが発火した時の処理を書いています。

  • open … WebSocket通信が開いたら発火するイベント
  • message … WebSocketサーバからメッセージが送られた時に発火するイベント
  • error … エラー発生時に発火するイベント
  • close … WebSocket通信が閉じたら発火するイベント

をそれぞれ聞くようにしています。

また、 open イベントリスナ のコールバック関数内で socket.send() を使っています。これが クライアント側からWebSocketサーバにメッセージやバイナリを送信するために利用するメソッドです。

// WebSocket が開通したら発火する
// socket.onopen = () => {} でも可
socket.addEventListener('open', (event) => {
  console.log('open')
  socket.send('メッセージ') // メッセージの送信
})

この部分で 「メッセージ」という文章を送っているので、WebSocketサーバからも「メッセージ」という文章が送信されてきているわけです。

inputに入力をして送信ボタンが押されたらwsサーバにデータを送信する

では仕組みが理解できたところで、DOMと処理を一致させましょう。 <script></script> タグの中身を書き換えてください。

index.html
<script>

const URL = 'ws://localhost:8888/'
const PROTOCOL = 'ws-sample'

const socket = new WebSocket(URL, PROTOCOL)

// ------------------------------
// WebSocket イベント
// ------------------------------

// WebSocket が開通したら発火する
// socket.onopen = () => {} でも可
socket.addEventListener('open', (event) => {
  setWsStatus('Websocket Connection 開始')
})

// WebSocketサーバ からメッセージを受け取ったら発火する
// socket.onmessage = () => {} でも可
socket.addEventListener('message', ({ data }) => {
  setWsStatus('message: ' + data)
  appendMessage(data)
})

// WebSocketサーバ からエラーメッセージを受け取ったら発火する
// socket.onerror = () => {} でも可
socket.addEventListener('error', (event) => {
  setWsStatus('Websocket Connection エラー')
  console.log('error')
})

// WebSocket がクローズしたら発火する
// socket.onclose = () => {} でも可
socket.addEventListener('close', (event) => {
  setWsStatus('Websocket Connection 終了')
  console.log('close')
})

// ------------------------------
// WebSocket メソッド
// ------------------------------
const sendMessage = (message) => {
  socket.send(message)
}

// ------------------------------
// DOM 操作
// ------------------------------

const sendMessageEl = document.querySelector('#send-message')
const sendButtonEl = document.querySelector('#send-button')
sendButtonEl.addEventListener('click', () => {
  const message = sendMessageEl.value
  sendMessage(message)
  sendMessageEl.value = ''
})

const setWsStatus = (text) => {
  const statusEl = document.querySelector('.websocket-status')
  statusEl.innerHTML = text
}

const createMessageEl = (text) => {
  return `<div class="rounded bg-white p-2 mb-2 text text-gray-600">${text}</div>`
}
const appendMessage = (text) => {
  const el = createMessageEl(text)
  document.querySelector('#messages').insertAdjacentHTML('afterend', el)
}

</script>

ちゃんとWebSocketサーバから帰ってきた内容がDOMに反映されるようになりました!

複数ブラウザに対して WebSocketイベントを発火させる

最後に、せっかくなのでメッセージが投稿されたら接続中の全ユーザの画面を更新するようにしましょう。

server.js の 以下の部分を書き換えてください

server.js
// 前略

wsServer.on('request', (request) => {
  // 中略
  connection.on('message', message => {
    switch (message.type) {
      case 'utf8':
        console.log(`メッセージ: ${message.utf8Data}`)
        // connection.sendUTF(message.utf8Data) コメントアウト
        wsServer.broadcast(message.utf8Data) // 追記
        break
      case 'binary':
        // 中略
    }
  })

// 以下略

wsServer.broadcast を使うと、接続中の全ユーザにイベントを送ることができます。

ここを書き換えたら再度 node server.js でサーバを立ち上げ直し、複数ブラウザを開いて挙動を確認してみましょう。

結果の共有ができました!!

この記事のまとめ

かなり骨太な内容になってしまいました笑

これを一つずつ順を追っていけば、WebSocketが何をしているのか、理解が深まると思います。

繰り返しの案内となりますが、サンプルデータはgithubから確認してみてください♪

チャットや音声データのやり取りなど、WebSocketの技術は至るところで使われています。

今のうちから慣れておきましょう!!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA