コンテンツにスキップ

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通信を有効化
// エラーが出る場合は,var io = socketio(server); として試してください.
// socket.io のバージョンが変わったため socketio.listen が使えなくなったようです.(追記Jun 28, 2024)

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/ にアクセスしてみる.

WebSocket_chat_1

片方のウィンドウに何か入力して送信ボタンを押すと,両方のウィンドウに(リアルタイムで)入力メッセージが反映されたことが確認できる(はず).

ワンポイント

サーバを動かした端末のIPアドレス(ifconfigコマンドで確認できる)を使って http://192.168.0.XXX:6010 としてもアクセスできる.この場合は,離れた端末間でも通信可能なので各自,試してみよ.

なお,F12キーで開発ツールを開き,その中の「Network」タブを見てみると,WebSocket通信が開始されていることが確認できる.

WebSocket_chat_2

プログラム解説

サーバサイド(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
var io = socketio.listen(server);

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時のイベント名称はサーバで決めたものと同じ