WebSocket API を 使ってみよう!
WebSocketを使ったリアルタイム通信の仕組みを利用して、ブラウザ間のデータ更新をする方法
皆さん、こんにちは。どんぶラッコです。
お仕事の関係で WebSocket の通信をフロント側に実装する機会がありました。その際、 JavaScript の標準APIである WebSocket を使って通信をテストしたので、使い方をまとめておこうと思います!
そして、WebSocketを叩くためにはWebSocketの配信サーバが必要なので、ついでにWebSocketサーバも実装してみました笑
どちらのコードもgithubにアップロードしてあるので、興味ある方は確認してみてください!
今回のハンズオンを実行すると↓↓のようなリアルタイムチャットもどきを作ることができます。
ということで、早速作成手順を確認していきましょう♪
まずはNode.js を使い、HTMLファイルを表示できるようにします
websocket
モジュールを使い、 WebSocketサーバの設定を行い wscatコマンド で挙動を確認します
これがやっと本題! JavaScriptのWebScoket API を使って WebSocketの情報を拾ってみます
Node.jsの設定をする
ディレクトリ作成 & npm init
まずは、ディレクトリを作成し、 npm init
します。これで色んなパッケージがDLできるようになります。
mkdir ws-handson # 任意のディレクトリを作成
cd ws-handson
npm init # package.json ファイルの作成
npm init
をすると聞かれる内容は全てデフォルトのままで今回はOKです。Enter
キーを連打してください。
http インタフェイスを使って nodeサーバ構築
次に、NodeのHTTPインタフェイスを使ってHTTPリクエストを受けられるようにしておきましょう。
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 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
<!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
の中身を書き換えましょう。
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 i websocket
次に server.js
に websokcetに関する記述を追記していきます。
まずは Websocket の開始と終了ができるように記述を追加していきます。
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コマンドを使ってグローバルにインストールしましょう。
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-sample
は server.js
の方で指定している文字列ですね。
const connection = request.accept('ws-sample', request.origin) // この部分
ws-sample
以外の サブプロトコルを指定すると、エラーハンドリングをしていないので、エラーが発生して nodeサーバ自体も止まってしまいます。
500番エラー (サーバエラー) が返ってきてるのが分かりますね。
ちなみに、 autoAcceptConnections: true
にすると、サブプロトコルを指定しなくても websokcet通信が確立します。本番環境で使っちゃいけない理由がこれです。
メッセージ処理を実装する
さて、websocketが使えることがわかったのでwebsocketでメッセージを受け取り、送信する処理を書きましょう。
公式サンプルを参考に実装していきます。
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ファイルを書き換えます。これは長いので何も考えずにコピペでいいです。笑
<!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>
の直前に書いていきましょう。
<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>
タグの中身を書き換えてください。
<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
の 以下の部分を書き換えてください
// 前略
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の技術は至るところで使われています。
今のうちから慣れておきましょう!!