WebSocketを用いた通信
WebSocketとは
- リアルタイムWeb技術の一種
- リアルタイムかつ双方向な通信を実現するためのプロトコル
- WebSocket通信の場合,コネクション確立時にプロトコルが(HTTPから)WebSocketに切り替わる
- 一旦,コネクションが確立されると,クライアントサーバ間のデータのやりとりは「
ws:
」または「wss:
」から始まるURIスキームで行われる - HTTPと比べて,通信のたびに新たにコネクションを確立する必要がない
Socket.io
- WebSocketを含むリアルタイムWeb技術を簡単に扱うことができるライブラリ
- 本家.2018/3現在のバージョンは2.0.4
【注意】
Webに転がっているサンプルは,バージョンが古く,そのままでは動かないものが多い! サンプルを見つけた時はそれがどのバージョンで書かれたものかを意識し,コードの利用に当たっては最新版の文法で扱えるような修正が必須であることがほとんどなので,常に頭に入れておくこと!
socket.ioを利用できるようにする
先にNode.js環境設定を済ませておくこと.
開発ディレクトリを決めて,(作成した上で)移動.
$ mkdir -p ~/csd/socket
$ cd ~/csd/socket
npmを使ってsocket.ioを入れる.
$ npm init -y $ npm install socket.io
実行後,~/csd/socket以下に node_modules/socket.io
ディレクトリが配置されていればOK.
チャットプログラムのサンプル
~/csd/socket以下に,次の3ファイルを作成する.
- index.html:クライアントサイドその1(ユーザーが使用するHTML)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!DOCTYPE html> <html lang="ja"> <head> <title>WebSocket-chat</title> <meta charset="utf-8" /> <script type="text/javascript" src="/socket.io/socket.io.js"></script> <!-- JQueryも使うので以下も追加 --> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> </head> <body> <h1>WebSocket-Chat</h1> <form> <div> <label for="msgForm">メッセージ:</label> <input type="text" id="msgForm"> </div> <button type="submit">送信</button> </form> <div id="chatLogs"></div> <script type="text/javascript" src="wschat.js"></script> </body> </html> |
補足: jQueryは,JavaScript のライブラリ.JavaScriptでできることを,より簡単な記法で実現できようになる.ブラウザの違いを気にせずにプログラミングできるそうです.
- wschat.js:クライアントサイドその2(HTMLに埋め込むJavaScript)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var socket = io.connect(); // ソケットに接続 socket.on("server_to_client", function(data){ appendMsg(data.value); }); function appendMsg(text) { $("#chatLogs").append("<div>" + text + "</div>"); // $("id") は document.getElementById("id") と同じ } $("form").submit(function(ev){ var message = $("#msgForm").val(); // テキストボックスのメッセージを取得 $("#msgForm").val(''); // テキストボックスを初期化 socket.emit("client_to_server", {value : message}); ev.preventDefault(); // 今のところ,イベントの終了と思っておけばOK }); |
- app.js:サーバサイド(メッセージ送受信等,サーバサイドの処理を実装する)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | var PORT = 6010; // ポートが競合する場合は値を変更すればOK var http = require('http'); var fs = require('fs'); var path = require('path'); var socketio = require('socket.io'); var mime = { // 必要に応じてMIMEタイプを追加する ".html": "text/html", ".css": "text/css", ".js": "text/javascript" }; var server = http.createServer(function(request, response) { var filePath = (request.url == '/')? '/index.html' : request.url; // 「条件? 値1:値2」 条件が真なら値1を選択,そうでなければ値2を選択 var fullPath = __dirname + filePath; // __dirname から,プログラムを実行しているディレクトリのパスが得る response.writeHead(200, {'Content-Type' : mime[path.extname(fullPath)] || "text/plain"}); // 「expr1 || expr2」は expr1 が真とみなせる場合は expr1 を返し,そうでなければ expr2 を返す fs.readFile(fullPath, function(err, data) { if(!err) { response.end(data, 'UTF-8'); } }) }).listen(PORT); var io = socketio.listen(server); // HTTPサーバとソケットオブジェクトを紐付けして,WebSocket通信を有効化 io.sockets.on('connection', function(socket) { socket.on('client_to_server', function(data) { io.sockets.emit('server_to_client', {value : data.value}); }); }); console.log("Server started at port: " + PORT); |
動作テスト
まずはサーバを起動する.
$ node app.js
Server started at port: 6010
無事起動したら,chromium-browser,あるいはFirefoxなどのブラウザを起動し,2枚のウィンドウからそれぞれ http://localhost:6010/
にアクセスしてみる.
片方のウィンドウに何か入力して送信ボタンを押すと,両方のウィンドウに(リアルタイムで)入力メッセージが反映されたことが確認できる(はず).
ワンポイント
サーバを動かした端末のIPアドレス(ifconfig
コマンドで確認できる)を使って http://192.168.0.XXX:6010
としてもアクセスできる.この場合は,離れた端末間でも通信可能なので各自,試してみよ.
なお,F12キーで開発ツールを開き,その中の「Network」タブを見てみると,WebSocket通信が開始されていることが確認できる.
スプリプト解説
サーバサイド(app.js)
3〜6行目 必要モジュールの読み込み
3 4 5 6 | var http = require('http'); var fs = require('fs'); var path = require('path'); var socketio = require('socket.io'); |
Node.jsを使っている場合,require モジュール名
で読み込める.ここではhttp,socket.io,path, fs(ファイルシステム),の4つを使っている.
15〜27行目 HTTPサーバの生成
15 16 17 18 19 20 21 22 23 24 25 26 27 | var server = http.createServer(function(request, response) { var filePath = (request.url == '/')? '/index.html' : request.url; var fullPath = __dirname + filePath; response.writeHead(200, {'Content-Type' : mime[path.extname(fullPath)] || "text/plain"}); fs.readFile(fullPath, function(err, data) { if(!err) { response.end(data, 'UTF-8'); } }) }).listen(PORT); |
http.createServer()
を使用してHTTPサーバを新たに作成する.サーバ生成時にリクエストリスナー
(クライアントからの要求を待つもの)が自動登録されるので,クライアントからHTTPリクエストが送信されるたびに,引数に指定した無名関数function(request, response)
が実行される.この例では,function内で(呼び出し元からリクエストされてきた)URLを解析し,ヘッダ及びMIME(マイム)タイプに応じた応答(index.htmlやwschat.jsの出力)が行われている.今のところはよく分からないかもしれないが,当面はこう書いておけばnode.jsで作成したhttpサーバでCSSやJavaScriptが読み込める,と覚えておきましょう.ちなみにMIMEタイプは,8〜13行目に随時,追加できる.
なお,このサンプルのようにhttp.createServer(・・・).listen(ポート番号)
とすると,待ち受けポート番号も指定できる.
29行目 ソケットをHTTPサーバに紐付ける
29 | var io = socketio.listen(server);
|
WebSocket通信を有効化するために,生成したHTTPサーバ(server
)とソケットオブジェクト(io
)の紐付けを行う.この例のようにsocketio.listen(HTTPサーバ)
と書けばOK.
クライアントサイド(index.htmlおよびwschat.js)
index.html 6行目 socket.ioライブラリ読み込み
6 | <script type="text/javascript" src="/socket.io/socket.io.js"></script>
|
クライアント側で使うsocket.ioライブラリは,サーバ起動時に(socket.ioが)自動で /socket.io/socket.io.js
を生成・配置してくれるので,これを src
に指定したscriptタグ
を記述するだけで良い.(実際にjsファイルを配置する必要はない)
wschat.js 1行目 ソケットへの接続
1 | var socket = io.connect();
|
io.connect()
とするだけでソケットに接続できる.
以降,クライアント側では,このsocket
オブジェクトを使ってWebSocket通信を実現できる.
スクリプト解説その2
socket.ioによる双方向通信の実装
socket.ioを使った(双方向通信による)データ送受信では,emit
が送信,on
が受信を意味する.
データ送信の書き方 | データ受信の書き方 |
---|---|
.emit(送信イベント名称, function(送信データ){関数定義}) | .on(受信イベント名称, function(受信データ){関数定義}) |
クライアント(wschat.js)からサーバ(app.js)へのデータ送信
34行目 client_to_serverイベント,データ送信
10 11 12 13 14 15 | $("form").submit(function(ev){ var message = $("#msgForm").val(); $("#msgForm").val(''); socket.emit("client_to_server", {value : message}); ev.preventDefault(); }); |
フォーム(#msgForm
)からsubmitされた入力値をmessage
として取得し,その値を client_to_server
という名称をつけてイベントとしてサーバに送信する.送信データは,JSON形式で記述し直す(ここでは{value : message}
としている).
- JSON: JavaScript Object Notationの略.テキストベースのデータフォーマット.連想配列のような形式.例えば,以下のような形式.
{ "Name" : "Katsuhisa Yamanaka", "position" : "teacher", "age" : 39 }
サーバ側(app.js)でのデータ受信と,クライアント(wschat.js)へのデータ送信
31行目 connectionイベント,データ受信
31 32 33 34 35 | io.sockets.on('connection', function(socket) { socket.on('client_to_server', function(data) { io.sockets.emit('server_to_client', {value : data.value}); }); }); |
クライアントサーバ間にWebSocket通信が確立すると,io.socketsオブジェクトに対してconnection
イベントが発火(クライアントからサーバにデータ送信)する.
このconnection
イベントはWebSocket通信が確立している間は有効なため,connection
イベント受信時のコールバック関数として,必要なサーバサイドの処理を定義する必要がある.
32行目 client_to_serverイベント,データ受信
31 32 33 34 35 | io.sockets.on('connection', function(socket) { socket.on('client_to_server', function(data) { // 'client_to_server'イベントでデータを受け取ったら... io.sockets.emit('server_to_client', {value : data.value}); // 'server_to_client'イベントでそのデータを送る }); }); |
クライアントから送信されてきた client_to_server
イベント,及びそのデータ(この例ではdata
という名前で受け取ったJSON)を受信する.
33行目 server_to_clientイベント,データを送信する
31 32 33 34 35 | io.sockets.on('connection', function(socket) { socket.on('client_to_server', function(data) { io.sockets.emit('server_to_client', {value : data.value}); }); }); |
受信したデータをJSONで書き直し,新たにserver_to_client
という名称のイベントとして,接続済みの全クライアントへ送信する.このために,io.sockets.emit(送信イベント名称, 送信データ)
を使う.
クライアント側でのデータ受信
3行目 server_to_clientイベント,データ受信
3 4 5 6 7 8 | socket.on("server_to_client", function(data){ appendMsg(data.value); }); function appendMsg(text) { $("#chatLogs").append("<div>" + text + "</div>"); } |
サーバから送信されたserver_to_client
イベント(及びデータdata
)を受信した時のコールバック関数を定義している.もしクライアント側がこのイベントを受け取ったら,関数内のappendMsg()
によって,ログエリア(#chatLogs
)に受信したデータが表示される.
以上,emitとonを交互に使用するだけで,双方向通信が簡単に実装できる.
プログラムの拡張その1
ユーザが入室したら,(そのユーザ以外の)他のクライアントにだけ入室メッセージを表示する処理を追加実装してみる.
クライアントサイドの拡張
- index.html(変更点はマーカー部)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <!DOCTYPE html> <html lang="ja"> <head> <title>WebSocket-chat</title> <meta charset="utf-8" /> <script type="text/javascript" src="/socket.io/socket.io.js"></script> <!-- JQueryも使うので以下も追加 --> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> </head> <body> <h1>WebSocket-Chat</h1> <form> <div> <label for="msgForm">名前:</label> <input type="text" id="msgForm"> </div> <button type="submit"">入室</button> </form> <div id="chatLogs"></div> <script type="text/javascript" src="wschat.js"></script> </body> </html> |
- wschat.js(変更点はマーカー部)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | var socket = io.connect(); var isEnter = false; var name = ''; socket.on("server_to_client", function(data){ appendMsg(data.value); }); function appendMsg(text) { $("#chatLogs").append("<div>" + text + "</div>"); } $("form").submit(function(ev){ var message = $("#msgForm").val(); $("#msgForm").val(''); if (isEnter) { message = '[' + name + ']: ' + message; socket.emit("client_to_server", {value : message}); } else { name = message; var entryMessage = name + "さんが入室しました!"; socket.emit("client_to_server_broadcast", {value : entryMessage}); changeLabels(); } ev.preventDefault(); }); function changeLabels() { $("label").text("メッセージ:"); $("button").text("送信"); isEnter = true; } |
19〜22行目
- 初めて入室する際に「〜さんが入室しました!」というメッセージをサーバに送信するための
client_to_server_broadcast
イベントを新たに追加 - いったん入室を済ませたら,ボタン等のラベルを変更(27〜31行目の
changeLabels()
)
サーバサイドの拡張(変更点はマーカー部)
29 30 31 32 33 34 35 36 37 38 39 40 | var io = socketio.listen(server); io.sockets.on('connection', function(socket) { socket.on('client_to_server', function(data) { io.sockets.emit('server_to_client', {value : data.value}); }); socket.on('client_to_server_broadcast', function(data) { socket.broadcast.emit('server_to_client', {value: data.value}); }); }); console.log("Server started at port: " + PORT); |
7〜9行目
client_to_server_broadcast
イベントで取得した入室メッセージを,他のクライアントに送信する処理を追加- この際,
socket.broadcast.emit()
を使うと,イベントを送信してきた以外のクライアント全部に対してデータ送信が行える
プログラムの拡張その2
クライアントが複数の場合,個別に通信を行いたい場合もある.socket.ioを利用してアクセスしたクライアントには必ず一意のID
が割り当てられるので,このIDを使えば個別通信が実装できる.
クライアントサイド(wschat.js)の拡張
19 20 21 22 23 | name = message; var entryMessage = name + "さんが入室しました!"; socket.emit("client_to_server_broadcast", {value : entryMessage}); socket.emit("client_to_server_personal", {value : name}); changeLabels(); |
20行目付近
- 名前欄に入力された値をサーバに送信する処理を
client_to_server_personal
イベントとして追加
サーバサイドの拡張
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | var io = socketio.listen(server); io.sockets.on('connection', function(socket) { socket.on('client_to_server', function(data) { io.sockets.emit('server_to_client', {value : data.value}); }); socket.on('client_to_server_broadcast', function(data) { socket.broadcast.emit('server_to_client', {value: data.value}); }); var name; socket.on('client_to_server_personal', function(data) { name = data.value; var personalMessage = "あなたは" + name + "さんとして入室しました"; io.to(socket.id).emit('server_to_client', {value: personalMessage}); }); }); console.log("Server started at port: " + PORT); |
29行目付近
client_to_server_personal
イベントを受けたら,そのクライアントに(一意に)割り当てられたID(socket.id
から取得可)を使って,そのIDのみに個別にデータ送信する処理を追加
プログラムの拡張その3
クライアントが切断された時の処理を追加で実装してみる.ブラウザを閉じたユーザがいたら,それ以外のユーザに退室メッセージを送る.
サーバサイドの拡張
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | var name; socket.on('client_to_server_personal', function(data) { name = data.value; var personalMessage = "あなたは" + name + "さんとして入室しました"; io.to(socket.id).emit('server_to_client', {value: personalMessage}); }); socket.on('disconnect', function() { if (name == 'undefined') { console.log("未入室のまま去ったようです"); } else { var endMessage = name + "さんが退室しました" io.sockets.emit('server_to_client', {value : endMessage}); } }); }); |
35〜42行目
- socket.ioは,クライアントが切断すると自動的に
disconnect
イベントを発生させる - よって,コネクション切断時の処理は,サーバサイドで
disconnect
イベントを受けた場合のコールバック関数を定義するだけで実装可能- クライアントサイドは変更不要
とりあえずの完成版
gitlabからクローンして試してみよ.もちろん,先にNode.js環境設定が必要なので注意すること.
$ cd ~/csd $ git clone https://gitlab.cis.iwate-u.ac.jp/kimura/wschat.git $ cd wschat $ npm install $ node app.js
まとめ
Socket.ioのインストール
$ npm install socket.io
サーバへの接続
サーバ側
// クライアントとの接続に成功するとconnectionイベントが発火 io.sockets.on('connection',function(socket) { console.log('connection'); // クライアントとの接続が切れるとdisconnectedイベントが発火 socket.on('disconnected',function() { console.log('disconnected'); } });
クライアント側
<!-- どこか(headタグ内とか)に以下追加 --> <script src="/socket.io/socket.io.js"></script> // ソケット接続を試みる var socket = io.connect(); // 接続に成功すると'connect'イベントが発火 socket.on('connect',function() { document.write('サーバーと接続しました'); });
- socket.io.jsの(実際の)配置は不要
データ送受信
クライアントから送信
// データ送信 socket.emit('send_to_server', data)
- 1番目引数はイベント名称(何でもOK)
- 2番目引数に,送るデータ(型は何でも.JSONでも可)
サーバ側で受信,送信
// データ受信 socket.on('send_to_server', fuction(data) { // 送信したクライアント本人に送り返す socket.emit('send_to_client', msg); // 送信クライアントを除いた他のクライアント全てに送信 socket.broadcast.emit('send_to_others', msg); // 送信クライアントも含む全てのクライアントに送信 io.sockets.emit('send_to_all', msg); });
- on時のイベント名称はクライアントで決めたものと同じ
- emit時のイベント名称は自由に決めて良い
クライアントでの受信
// データ受信 socket.on('send_to_client', function(msg) { document.write(msg); });
- on時のイベント名称はサーバで決めたものと同じ