本連載「Socket.IOで始めるWebSocket超入門」では、WebSocketを扱うことができるNode.jsのライブラリ「Socket.IO」を使って、サンプルアプリケーションを構築していきます。
具体的には、チャットを題材とし、送受信されるメッセージ内容が即時反映されるリアルタイムかつ双方向なWebアプリケーションの構築を目標とします。さらに構築の中で、Socket.IOの各種ライブラリの使い方について解説することで、Socket.IOを使ったWebSocketの実践方法を体系的に学びます。
前回の「いまさら聞けないWebSocketとSocket.IOの基礎知識&インストール」では、本連載で使用する環境の構築とチャットアプリケーションのひな型を作成しました。今回は、ひな型を使用してSocket.IOの基本ライブラリについて解説します。
以降は、前回の環境構築とひな型の作成が完了していることが前提です。もし、まだひな型の作成まで終わっていない方は、前回記事を参考にしてひな型の作成まで実施してみてください。
はじめに、前回作成したチャットアプリケーションの概要について簡単に説明します。チャットアプリケーションの構成は以下の通りです。
まず、あるクライアントがデータをサーバに送信します(クライアントAがサーバに「Hello!!」メッセージを送信)。サーバは接続している全てのクライアント(クライアントA、B、C)にそのデータを送信します。全てのクライアントは、サーバから送信されたデータを受信し、画面上に表示します。このような流れでクライアントとサーバ間で双方向かつリアルタイムな通信を行います。
ここからは、Socket.IOライブラリの使い方を見ていきましょう。前回のひな型作成時に実装済みのコードについて説明した後に、実際に追加機能を実装します。コードのコメント文や以下の文章では、クライアントサイドの説明項目と、サーバサイドの説明項目を分けるために、以下のような表記を使用します。
まずは、WebSocket通信を有効化するまでに必要な実装について、説明します。
WebSocket通信用のソケットを持つHTTPサーバの生成が必要となります。その処理をサーバサイドに記述します。
// S01. 必要なモジュールを読み込む var http = require('http'); var socketio = require('socket.io'); var fs = require('fs'); // S02. HTTPサーバを生成する var server = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type' : 'text/html'}); res.end(fs.readFileSync(__dirname + '/index.html', 'utf-8')); }).listen(3000); // ポート競合の場合は値を変更 // S03. HTTPサーバにソケットをひも付ける(WebSocket有効化) var io = socketio.listen(server);
必要なモジュールは、http、socket.io、fs(ファイルシステム)の3つです。Node.jsの「require(モジュール名) 」で読み込むことができます。
HTTPサーバを生成します。「http.createServer(リクエストリスナー)」を使用します。サーバ生成時に、リクエストリスナーが自動で登録されるため、クライアントからHTTPリクエストが送信されるたびに引数に指定した関数が実行されます。ひな型では初期処理として、ヘッダ出力とindex.htmlの出力を行っています。
また、「http.Server.listen(ポート番号)」を使用して、使用するポートを設定します。
WebSocket通信を有効化するためには、生成したHTTPサーバとソケットのひも付けを行う必要があります。そのために、「socketio.listen(HTTPサーバ)」を使用しています。
クライアントサイドでは、Socket.IOクライアントライブラリの読み込みとソケット接続処理を記述する必要があります。
<!DOCTYPE html> <html lang="ja"> <head> ……(中略) <!-- C01. Socket.IOクライアントライブラリの読み込み --> <script type="text/javascript" src="/socket.io/socket.io.js"></script> ……(中略) <script type="text/javascript"> var socket = io.connect(); // C02. ソケットへの接続 ……(中略)
クライアントライブラリは、srcに"/socket.io/socket.io.js"を指定したscriptタグを記述するだけで読み込めます。実際にjsファイルを配置する必要はありません。これは、Socket.IOがサーバ起動時に"/socket.io/socket.io.js"ライブラリを自動生成するためです。
ソケット接続には、「io.connect()」を使用します。
ここまでで、クライアント~サーバ間でWebSocket通信をする準備は完了です。
次に、双方向通信によるデータ送受信の実装についてです。ひな型の処理の流れに沿って説明します。ポイントは、emitは送信、onは受信ということです。
データの送信 | データの受信 | |
---|---|---|
emit(送信イベント名, 送信データ) 関数 | on(受信イベント名, 受信データ) 関数 | |
$("form").submit(function(e){ var message = $("#msgForm").val(); $("#msgForm").val(''); // C03. client_to_serverイベント・データを送信する socket.emit("client_to_server", {value : message}); e.preventDefault(); });
フォームがsubmitされたら入力値を取得し、その値をclient_to_serverイベントとしてサーバへ送信しています。
// S04. connectionイベント・データを受信する io.sockets.on('connection', function(socket) { // S05. client_to_serverイベント・データを受信する socket.on('client_to_server', function(data) { // S06. server_to_clientイベント・データを送信する io.sockets.emit('server_to_client', {value : data.value}); }); });
クライアント~サーバ間でWebSocket通信が確立すると、io.socketsオブジェクトに対してconnectionイベントが発火(データが送信)されます。このconnectionイベントはWebSocket通信が確立している限り有効です。そのため、connectionイベント受信時のコールバック関数として、必要なサーバサイドの処理を定義することになります。
io.sockets.on('connection', function(socket) { // この中に必要な処理を記述する });
クライアントから送信されたclient_to_serverイベントとデータを受信しています。
受信したデータをserver_to_clientイベントとして、接続している全クライアントへ送信しています。全クライアントにデータを送信するには、「io.sockets.emit(送信イベント名, 送信データ)」を使用します。
io.sockets.emit('server_to_client', {value : data.value});
// C04. server_to_clientイベント・データを受信する socket.on("server_to_client", function(data){appendMsg(data.value)}); function appendMsg(text) { $("#chatLogs").append("<div>" + text + "</div>"); }index.html
サーバから送信されたserver_to_clientイベントを受信し、データを取得します。コールバック関数(appendMsg(text))で、チャットログエリアにデータを表示します。
以上が、クライアント~サーバ間での双方向通信の実装です。emit/onを交互に使用してデータをやりとりしていることが確認できたと思います。
Socket.IOサーバライブラリには、全クライアントにデータを送信する「io.sockets.emit(送信イベント名, 送信データ)」だけではなく、他にもデータ送信用ライブラリが存在します。
まずは、「socket.broadcast.emit(送信イベント名, 送信データ)」です。このライブラリを使用すると、サーバは、データ・イベントを送信したクライアント以外の全てのクライアントにデータを送信します。
では、実際にコードを追加・変更しながら処理の違いを確認してみましょう。ユーザーが入室したら、他のクライアントにだけ入室メッセージ表示する処理を実装します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>websocket-chat</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <!-- C01. Socket.IOクライアントライブラリの読み込み --> <script type="text/javascript" src="/socket.io/socket.io.js"></script> </head> <body> <div class="container"> <h1>WebSocket-Chat</h1> <form class="form-inline"> <div class="form-group"> <label for="msgForm">名前:</label> <input type="text" class="form-control" id="msgForm"> </div> <button type="submit" class="btn btn-primary" id="sendButton">入室</button> </form> <div id="chatLogs"></div> </div> <script type="text/javascript"> var socket = io.connect(); // C02. ソケットへの接続 var isEnter = false; var name = ''; // C04. server_to_clientイベント・データを受信する socket.on("server_to_client", function(data){appendMsg(data.value)}); function appendMsg(text) { $("#chatLogs").append("<div>" + text + "</div>"); } $("form").submit(function(e){ var message = $("#msgForm").val(); $("#msgForm").val(''); if (isEnter) { message = "[" + name + "]: " + message; // C03. client_to_serverイベント・データを送信する socket.emit("client_to_server", {value : message}); } else { name = message; var entryMessage = name + "さんが入室しました。"; // C05. client_to_server_broadcastイベント・データを送信する socket.emit("client_to_server_broadcast", {value : entryMessage}); changeLabel(); } e.preventDefault(); }); function changeLabel() { $("label").text("メッセージ:"); $("button").text("送信"); isEnter = true; } </script> </body> </html>
“~さんが入室しました。”というメッセージをサーバに送信するclient_to_server_broadcastイベントを新しく追加しています。
// S01. 必要なモジュールを読み込む var http = require('http'); var socketio = require('socket.io'); var fs = require('fs'); // S02. HTTPサーバを生成する var server = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type' : 'text/html'}); res.end(fs.readFileSync(__dirname + '/index.html', 'utf-8')); }).listen(3000); // ポート競合の場合は値を変更 // S03. HTTPサーバにソケットをひも付ける(WebSocket有効化) var io = socketio.listen(server); // S04. connectionイベント・データを受信する io.sockets.on('connection', function(socket) { // S05. client_to_serverイベント・データを受信する socket.on('client_to_server', function(data) { // S06. server_to_clientイベント・データを送信する io.sockets.emit('server_to_client', {value : data.value}); }); // S07. client_to_server_broadcastイベント・データを受信し、送信元以外に送信する socket.on('client_to_server_broadcast', function(data) { socket.broadcast.emit('server_to_client', {value : data.value}); }); });
client_to_server_broadcastイベントより取得した入室メッセージをクライアントに送信する処理を追加しています。
コードの変更・保存が完了したら、アプリケーションの動作を確認します。ターミナルで、websocket-chatディレクトリに移動し、「node app.js」コマンドを実行してください(既に実行中の場合は、1度停止した後に再度実行してください)。その後、ブラウザウィンドウを複数開き、「http://localhost:3000/」にアクセスしてみましょう。
どれか1つのウィンドウで、名前欄に値を入力し、入室ボタンを押下します。
ボタン押下後、入力ウィンドウ以外に、「~さんが入室しました。」というメッセージが表示されていることが確認できたと思います。
このように、broadcastを利用すると、自分以外の全てのクライアントにメッセージを送信できます。
socket.broadcast.emit('server_to_client', {value : data.value});
次に、個別送信について説明します。Socket.IOでアクセスしたクライアントには必ず一意のIDが割り当てられます。このIDを利用することで、特定のクライアントに対してのみサーバからデータを送信できます。個別送信では、「io.to(ID).emit(送信イベント名, 送信データ)」を使用します。
今度は、入室時のユーザー名を、入力したユーザーだけに表示する処理を実装します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>websocket-chat</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <!-- C01. Socket.IOクライアントライブラリの読み込み --> <script type="text/javascript" src="/socket.io/socket.io.js"></script> </head> <body> <div class="container"> <h1>WebSocket-Chat</h1> <form class="form-inline"> <div class="form-group"> <label for="msgForm">名前:</label> <input type="text" class="form-control" id="msgForm"> </div> <button type="submit" class="btn btn-primary" id="sendButton">入室</button> </form> <div id="chatLogs"></div> </div> <script type="text/javascript"> var socket = io.connect(); // C02. ソケットへの接続 var isEnter = false; var name = ''; // C04. server_to_clientイベント・データを受信する socket.on("server_to_client", function(data){appendMsg(data.value)}); function appendMsg(text) { $("#chatLogs").append("<div>" + text + "</div>"); } $("form").submit(function(e){ var message = $("#msgForm").val(); $("#msgForm").val(''); if (isEnter) { message = "[" + name + "]: " + message; // C03. client_to_serverイベント・データを送信する socket.emit("client_to_server", {value : message}); } else { name = message; var entryMessage = name + "さんが入室しました。"; // C05. client_to_server_broadcastイベント・データを送信する socket.emit("client_to_server_broadcast", {value : entryMessage}); // C06. client_to_server_personalイベント・データを送信する socket.emit("client_to_server_personal", {value : name}); changeLabel(); } e.preventDefault(); }); function changeLabel() { $("label").text("メッセージ:"); $("button").text("送信"); isEnter = true; } </script> </body> </html>
名前欄に入力された値をサーバに送信する処理をclient_to_server_personalイベントとして追加しています。
// S01. 必要なモジュールを読み込む var http = require('http'); var socketio = require('socket.io'); var fs = require('fs'); // S02. HTTPサーバを生成する var server = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type' : 'text/html'}); res.end(fs.readFileSync(__dirname + '/index.html', 'utf-8')); }).listen(3000); // ポート競合の場合は値を変更 // S03. HTTPサーバにソケットをひも付ける(WebSocket有効化) var io = socketio.listen(server); // S04. connectionイベント・データを受信する io.sockets.on('connection', function(socket) { var name; // S05. client_to_serverイベント・データを受信する socket.on('client_to_server', function(data) { // S06. server_to_clientイベント・データを送信する io.sockets.emit('server_to_client', {value : data.value}); }); // S07. client_to_server_broadcastイベント・データを受信し、送信元以外に送信する socket.on('client_to_server_broadcast', function(data) { socket.broadcast.emit('server_to_client', {value : data.value}); }); // S08. client_to_server_personalイベント・データを受信し、送信元だけに送信する socket.on('client_to_server_personal', function(data) { var id = socket.id; name = data.value; var personalMessage = "あなたは、" + name + "さんとして入室しました。" io.to(id).emit('server_to_client', {value : personalMessage}) }); });
データを送信する前に、各クライアントに一意に割り当てられるIDを取得します。「socket.id」によって取得することが可能です。そのIDに対して、つまり特定のクライアントに対してのみデータを送信しています。
コードの変更・保存が完了したら、再度アプリケーションの動作を確認します。“node app.js”を実行し、複数のブラウザウィンドウでアプリケーションを開いてください。
先ほどと同様に、どれか1つのブラウザの名前欄に値を入力し、入室ボタンを押します。入力ウィンドウには、「あなたは、~さんとして入室しました。」というメッセージが表示され、それ以外のウィンドウには「~さんが入室しました。」というメッセージが表示されることが確認できると思います。
このように、socket.idを指定してデータを送信すると、特定のクライアントのみにデータを送信することができます。
io.to(id).emit('server_to_client', {value : personalMessage})
最後にクライアントとのコネクションが切断したときの処理を実装します。ブラウザウィンドウを閉じたユーザーがいたら、その他のユーザーに退出メッセージを表示する処理を追加します。
// S01. 必要なモジュールを読み込む var http = require('http'); var socketio = require('socket.io'); var fs = require('fs'); // S02. HTTPサーバを生成する var server = http.createServer(function(req, res) { res.writeHead(200, {'Content-Type' : 'text/html'}); res.end(fs.readFileSync(__dirname + '/index.html', 'utf-8')); }).listen(3000); // ポート競合の場合は値を変更 // S03. HTTPサーバにソケットをひも付ける(WebSocket有効化) var io = socketio.listen(server); // S04. connectionイベント・データを受信する io.sockets.on('connection', function(socket) { var name; // S05. client_to_serverイベント・データを受信する socket.on('client_to_server', function(data) { // S06. server_to_clientイベント・データを送信する io.sockets.emit('server_to_client', {value : data.value}); }); // S07. client_to_server_broadcastイベント・データを受信し、送信元以外に送信する socket.on('client_to_server_broadcast', function(data) { socket.broadcast.emit('server_to_client', {value : data.value}); }); // S08. client_to_server_personalイベント・データを受信し、送信元のみに送信する socket.on('client_to_server_personal', function(data) { var id = socket.id; name = data.value; var personalMessage = "あなたは、" + name + "さんとして入室しました。" io.to(id).emit('server_to_client', {value : personalMessage}); }); // S09. disconnectイベントを受信し、退出メッセージを送信する socket.on('disconnect', function() { if (name == 'undefined') { console.log("未入室のまま、どこかへ去っていきました。"); } else { var endMessage = name + "さんが退出しました。" io.sockets.emit('server_to_client', {value : endMessage}); } }); });
クライアントサイドのindex.htmlへのコードは不要です。Socket.IOはクライアントが切断すると、自動的にdisconnectイベントを発生させます。つまり、コネクション切断時の処理はサーバサイドにてdisconnectイベント受信後のコールバック関数を定義するだけで実装できます。
socket.on('disconnect', function() { // この中に切断時の処理を記述する });
再度、同じ要領で動作確認を行います。複数のブラウザウィンドウを開き、1つのウィンドウを閉じてみてください。
「~さんが退出しました。」というメッセージが残りの開かれているウィンドウに表示されたと思います。
また、入室しないままウィンドウを閉じると、以下のようにターミナル上にメッセージが表示されることも確認してみてください。
いかがでしたでしょうか。今回説明した内容を表形式でまとめます。
処理概要 | クライアントサイド | サーバサイド |
---|---|---|
WebSocket通信準備 | ・モジュール読み込み (/socket.io/socket.io.js) ・ソケット接続(io.connect) |
・モジュール読み込み(require) ・HTTPサーバ生成(createServer) ・ソケットのひも付け(listen) |
データ送信 | emit:一斉送信 | |
― | broadcast:自分以外 | |
― | to(id):自分のみ | |
データ受信 | on:イベント・データ受信 | |
切断時の処理 | ― | disconnectイベント定義 |
次回は、Socket.IOライブラリのうち、データ送信対象をグループ化できる「room」と、サーバサイドの機能を分割できる「namespace」に焦点を当てて解説します。
Copyright © ITmedia, Inc. All Rights Reserved.