Socket.io + Express を試してみる
WebSocketとは?
双方向通信を可能にするプロトコルの一つです。リアルタイムで通信を行いたいチャットなどに利用されます。
普通のhttp/https
通信では、クライエントからリクエストが送られる度にコネクションが張られ、サーバーからレスポンスが返ると、コネクションは切断されます。 つまり、通信の度にコネクションを張り直す必要があり、クライエントからのリクエストなしにサーバーからレスポンスを送信することができません。
これを解決するために、従来はロングポーリング(comet)
という技術が使われていました。
一定の間隔でブラウザからリクエストを送り、サーバーは新着情報がなければレスポンスを一定時間保留にします。その間に新着があればそれを返し、なければ何もなかったというレスポンスを返します。サーバー側の保留なしの単純なポーリング
よりリアルタイム性が高いのが特徴です。これによって擬似的に、リアルタイム通信が可能になりますが、以下の欠点があります。
- ヘッダーが冗長でオーバーヘッドが大きい
- タイムアウトがある
- httpコネクションを占有してしまう
一方でWebsocketは、最小2byte, 最大でも14byteと軽量かつ、HTTPをアップグレーデして、TCP上で双方向通信を実現します。 クライエントからの最初のリクエストのヘッダーは次のようになります。
GET / HTTP/1.1 Host: localhost:8080 Upgrade: websocket Connection: upgrade Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: xxxxxxxxxxx== Sec-WebSocket-Version: 13
サーバーからのレスポンスは次のようになります。
HTTP/1.1 101 OK Upgrade: websocket Connection: upgrade Sec-WebSocket-Accept: yyyyyyyyyy=
なんとなく直感で理解できるのではないでしょうか。
Sec-Websocket-Key
とSec-Websocket-Accept
はそれぞれ対応しており、クライエントが自分のリクエストに対するレスポンスだと認識できるようになっています。
ちなみに、101
は、Upgrade
リクエストヘッダーが送られてきた場合に返却するSwitchinig Protocols
レスポンスです。
この時点で、Websocketコネクションが確立されました。
Socket.ioとは?
手軽にwebsocketを実現するjavascriptライブラリで、フロント側と、node.jsで実装されたサーバー側から作られています。
WebSocketが繋がらなくなった時に、ロングポーリング
やポーリング
などの代替手段を提供してくれる他、到達確認や認可、namespaceなどを提供してくれます。
チャットを作ってみる
まずexpress
とsocket.io
をインストールします。
yarn add express socket.io
次のようにapp.js
を作成します。
// app.js const express = require('express'); const app = express(); const http = require('http').createServer(app); const PORT = process.env.PORT || 3000; app.get('/', (req, res) => { res.send('<h1>Hello world</h1>'); }); http.listen(PORT, () => { console.log(`Server listening. Port: ${PORT}`); });
express
に関する記述のみですが、動作確認をしておきましょう。
node app.js
ブラウザで、localhost:3000
にアクセスします。Hello world
と表示されるはずです。
次に表示部分をhtml
ファイルに分けていきます。
// app.js app.use(express.static(`${__dirname}/public`)); app.get('/', (req, res) => { res.send(`${__dirname}/index.html`); });
まず、app.js
でファイルを読み込みます。静的ファイルを分けたいので、static
を使っています。
<!-- public/index.html --> <!doctype html> <html> <head> <meta charset="UTF-8"> <title>Socket.io</title> <link rel="stylesheet" href="index.css"> </head> <body> <div class="chat-field"> <div class="chat-area"> <ul id="messages"></ul> </div> <form class="chat-form" action=""> <input id="m" /> <button class="disabled">Send</button> </form> </div> <script src="/socket.io/socket.io.js"></script> <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script> <script type="text/javascript" src="index.js"></script> </body> </html>
/* public/index.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } .chat-field { margin: 10em auto; width: 40%; height: 50em; border-radius: 5px; box-shadow: 5px 5px 25px 0 rgba(0, 0, 0, .5); text-align: center; } .chat-area { margin: 5em auto; width: 90%; height: 40em; overflow: scroll; } .chat-area li { margin: 0.5em; padding: 0.7em; background-color: rgb(223, 223, 223); border-radius: 8px; list-style-type: none; } .chat-form input { margin-right: 0.3em; padding: 0.6em; width: 70%; border: 2px solid blueviolet; border-radius: 5px; } .chat-form button { padding: 0.6em; border: 2px solid blueviolet; border-radius: 5px; background-color: blueviolet; color: white; }
次にindex.js
を書いていきます。アクセス時に、websocket
を張り、送信ボタンを押すとそれを通じて通信を行い、レスポンスを表示する仕様です。
// public/index.js $(() => { $('input').change(function() { let text = $(this).val(); if (text === null) { $('button').prop('disabled', true); } else { $('button').prop('disabled', false); }; }); const socket = io(); $('form').submit(e => { e.preventDefault(); socket.emit('post', $('#m').val()); $('#m').val(''); $('button').prop('disabled', true); return false; }); socket.on('msg', res => { $('#messages').append($('<li>').text(res)); }); });
socket.io
のインスタンスに対して、emit
で送信、on
で受信を行っています。第一引数は、送信側と受信側で同じキーを指定します。
ここまでで一度動作確認をしておきましょう。次のような画面になっていれば成功です。
続いてサーバー側にsocket.io
部分を書いていきます。
// app.js const io = require('socket.io')(http); io.on('connection', socket => { console.log('user connected'); socket.on('post', post => { io.emit('msg', post); }); });
on
で受信し、post
をキーに持つメッセージを受け取ります。そして、受け取ったものをそのまま返却していますが、これはconnectionが張られている全てのユーザーに送信されます。動作確認してみてください。
これで簡単なチャットは作成できましたが、自分と相手のメッセージくらい区別できるようにしておきましょう。 具体的には、接続時にユーザー一意のトークンを発行し、ユーザーがリクエストの際にそのトークンを含めることで実装します。
トークン生成にはcrypto
を使用し、socket.id
でユーザーごとに割り振られたidを取得できるのでこれを元に作成します。
// app.js const crypto = require('crypto'); io.on('connection', socket => { console.log('user connected'); (() => { const token = makeToken(socket.id); io.to(socket.id).emit('token', { token: token }); })(); socket.on('post', post => { io.emit('msg', post); }); }); const makeToken = id => { return crypto.createHash('sha1').update(id).digest('hex'); };
全てのユーザーに送信者のトークンを返却してるのが相当イケてないですが今回はよしとしてください(笑)
最後にクライエント側で識別を行って、ラインのように左右に出し分けたいと思います。
// index.js const IAM = { token: null }; socket.on('token', data => { IAM.token = data.token; }); $('form').submit(e => { e.preventDefault(); socket.emit('post', { msg: $('#m').val(), token: IAM.token, }); $('#m').val(''); $('button').prop('disabled', true); return false; }); socket.on('msg', res => { if (res.token === IAM.token) { $('#messages').append($('<li class="mine">').text(res.msg)); } else { $('#messages').append($('<li class="others">').text(res.msg)); } });
まず、token
を受け取り、IAM
にセットしておきます。(token
はconnectionが張られた際に、サーバーから送信されるものです。)
メッセージの送信時に、トークンを含めるように変更し、受け取り時はIAM
と比較して自分が送信したものかどうか判定します。class
属性を変えることで表示を分けます。
最後にindex.css
を変更します。
/* index.css */ .chat-area li { padding: 0.7em; background-color: rgb(223, 223, 223); border-radius: 8px; list-style-type: none; } .mine { margin: 0.5em; width: 50%; } .others { margin: 0.5em 0.5em 0.5em auto; width: 50%; }
これで完成です。ブラウザで複数タブを開いて、チャットをやりとりしてみてください。 最終コードはgithubにあげてます。
参考にさせて頂きましたm
WebSocketについて調べてみた。 - Qiita
socket.io が提供してくれているものは何か - from scratch
はじめてのSocket.io #2 チャット編「自分がemitした通信内容か判定する」