WebSocketを用いた通信
WebSocketとは
- リアルタイムWeb技術の一種
- リアルタイムかつ双方向な通信を実現するためのプロトコル
- WebSocket通信の場合,コネクション確立時にプロトコルが(HTTPから)WebSocketに切り替わる
- 一旦,コネクションが確立されると,クライアントサーバ間のデータのやりとりは「
ws:
」または「wss:
」から始まるURIスキームで行われる - HTTPと比べて,通信のたびに新たにコネクションを確立する必要がない
Socket.io
- WebSocketを含むリアルタイムWeb技術を簡単に扱うことができるライブラリ
- 本家.
【注意】
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)
<!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でできることを,より簡単な記法で実現できようになる.ブラウザの違いを気にせずにプログラミングできる.
- メッセージを入力するタグの id は msgForm にしている.
- 今までに入力されたメッセージを表示する部分の id は chatLogs にしている.
- ボタンのtype属性をsubmitにしている.formからメッセージをsubmitする形にするため.
wschat.js:クライアントサイドその2(HTMLに埋め込むJavaScript)
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(); // テキストボックスのメッセージをmessageに保存
$("#msgForm").val(''); // テキストボックスを初期化
socket.emit("client_to_server", {value : message}); // メッセージを送信
ev.preventDefault(); // イベントの終了と思っておけばOK
});
app.js:サーバサイド(メッセージ送受信等,サーバサイドの処理を実装する)
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) { // HTTPサーバの生成
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)
必要モジュールの読み込み
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つを使っている.
HTTPサーバの生成
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);
- 最初のうちは,こう書いておけばnode.jsで作成したhttpサーバでCSSやJavaScriptが読み込める でOK.
http.createServer()
を使用してHTTPサーバを新たに作成する.- サーバ生成時に
リクエストリスナー
(クライアントからの要求を待つもの)が自動登録されるので,クライアントからHTTPリクエストが送信されるたびに,引数に指定した無名関数function(request, response)
が実行される. - この例では,function内で(呼び出し元からリクエストされてきた)URLを解析し,ヘッダ及びMIME(マイム)タイプに応じた応答(index.htmlやwschat.jsの出力)が行われている.
- このサンプルのように
http.createServer(・・・).listen(ポート番号)
とすると,待ち受けポート番号も指定できる.
29行目 ソケットをHTTPサーバに紐付ける
29 |
|
WebSocket通信を有効化するために,生成したHTTPサーバ(server
)とソケットオブジェクト(io
)の紐付けを行う.この例のようにsocketio.listen(HTTPサーバ)
と書けばOK.
クライアントサイド(index.htmlおよびwschat.js)
index.html socket.ioライブラリ読み込み
<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 ソケットへの接続
var socket = io.connect();
io.connect()
とするだけでソケットに接続できる.
以降,クライアント側では,このsocket
オブジェクトを使ってWebSocket通信を実現できる.
スクリプト解説その2
socket.ioによる双方向通信の実装
socket.ioを使った(双方向通信による)データ送受信では,emit
が送信,on
が受信を意味する.
データ送信の書き方 | データ受信の書き方 |
---|---|
.emit(送信イベント名称, function(送信データ){関数定義}) | .on(受信イベント名称, function(受信データ){関数定義}) |
クライアント(wschat.js)からサーバ(app.js)へのデータ送信
client_to_serverイベント,データ送信
$("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)へのデータ送信
connectionイベント,データ受信
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
イベント受信時のコールバック関数として,必要なサーバサイドの処理を定義する必要がある.
client_to_serverイベントとしてサーバがデータを受信, そして, 同名のイベントとしてサーバがデータを送信
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)を受信する. - 受信したデータをJSONで書き直し,新たに
server_to_client
という名称のイベントとして,接続済みの全クライアントへ送信する.このために,io.sockets.emit(送信イベント名称, 送信データ)
を使う.
クライアント側でのデータ受信
server_to_clientイベント,データ受信
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
<!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
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()
)
サーバサイドの拡張
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)の拡張
name = message;
var entryMessage = name + "さんが入室しました!";
socket.emit("client_to_server_broadcast", {value : entryMessage});
socket.emit("client_to_server_personal", {value : name});
changeLabels();
- 名前欄に入力された値をサーバに送信する処理を
client_to_server_personal
イベントとして追加
サーバサイドの拡張
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);
client_to_server_personal
イベントを受けたら,そのクライアントに(一意に)割り当てられたID(socket.id
から取得可)を使って,そのIDのみに個別にデータ送信する処理を追加
プログラムの拡張その3
クライアントが切断された時の処理を追加で実装してみる.ブラウザを閉じたユーザがいたら,それ以外のユーザに退室メッセージを送る.
サーバサイドの拡張
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});
}
});
});
- 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時のイベント名称はサーバで決めたものと同じ