SinatraでWebsocket通信

  • 28
    いいね
  • 0
    コメント

Websocket通信って?

Webでの情報のやり取りはHTTP通信が基本。
しかしHTTP通信では、サーバーに対してリクエスト(例えばページの更新)を送らない限りクライアントが持っている情報が更新されることはありません。
いちいちユーザーが「ページを更新するのが面倒だけど自動で情報は更新してほしいな」ってことで使われ始めたのがAjax通信です。しかしAjax通信の場合、裏でリクエストを送ってJavaScriptでDOMを書き換えています。そのためリアルタイムなやり取りをするには常に裏でリクエストを送ってレスポンスを元にDOMをゴリゴリといったやり取りをしなければなりません。
そんなわけで使われ始めたのがWebsocketです。これは必要に応じてサーバーからクライアントに対して情報を送ってくれる通信になります!
情報を得るためにクライアントがリクエストを送る必要がないってところがミソ。

とりあえずやってみる

ネタとしてはベタだけどチャットを作ってみます。

使うGem

  • sinatra
  • sinatra-contrib
  • sinatra-websocket (今回の主役)

とりあえずこれらのgemをGemfileに書いて置いてください

Gemfile
source "https://rubygems.org"
gem 'sinatra'
gem 'sinatra-contrib'
gem 'sinatra-websocket'

書き終わったらbundleの実行を忘れずに・・・

STEP1 サーバー側の準備

app.rb
require 'bundler/setup'
Bundler.require
require 'sinatra/reloader' if development?
require 'sinatra-websocket'

set :server, 'thin'
set :sockets, []

get '/' do
  erb :index
end

get '/websocket' do
  if request.websocket? then
    request.websocket do |ws|
      ws.onopen do
        settings.sockets << ws
      end
      ws.onmessage do |msg|
        settings.sockets.each do |s|
          s.send(msg)
        end
      end
      ws.onclose do
        settings.sockets.delete(ws)
      end
    end
  end
end

とりあえずルーティングの整理から

  • / ・・・ビューの表示用
  • /websocket ・・・ WebSocket通信用。

ここでコードも少し解説しておきます。っと、その前にメソッドやら変数やらの解説

  • set :server, 'thin' ・・・ サーバとしてthinを使うという宣言
  • set :sockets, [] ・・・ Websocket通信で情報が更新された時にレスポンスを送る先を入れる配列
  • request.websocket? ・・・ リクエストがWebSocket通信であるかを返す
  • ws ・・・ サーバーの情報やクライアントの情報が詰まった変数(SinatraWebsocket::Connectionクラス)
  • settings.sockets ・・・ set :sockets, []で宣言した変数を呼び出している
  • request.websocket do ... end ・・・ Websocket通信が来た時の処理をまとめておくブロック(websocket通信以外が来るとエラー吐くから注意)
  • ws.onopen do ... end ・・・ Websocket通信のための接続がされようとしている時の処理をまとめるブロック(例えば、ページにアクセスした時やページを更新した時)
  • ws.onmessage do ... end ・・・ Websocket通信によってメッセージが来た時の処理をまとめるブロック
  • ws.onclose do ... end ・・・Websocket通信用の接続を外された時の処理をまとめるブロック(例えば、ページを閉じた時やページを更新した時)
  • s.send() ・・・ クライアントに向かって情報をWebSocket通信で送るメソッド。sendメソッドでは1クライアントにしか送れないから注意。あとこのメソッドはrequest.websocket do ... end外でも使えたりする。

すごく長くなってしまったけど、ここから解説本番です。
get '/' do ... endの解説は飛ばすとしてget '/websocket' do ... endの解説をすると・・・
1. Websocket通信でのコネクションを要求されたget '/websocket' do ... end内のws.onopen do ... endが呼び出される。その中でsettings.socketsの中にアクセスしたクライアントの情報などなどを追加する。(<<を使っているので要素の追加)
2. クライアントからWebsocket通信によってメッセージが送られてきた時、ws.onmessage do ... endが呼び出される。sendメソッドは、1クライアントに対してしかメッセージを返せないからeachで1クライアントずつ呼び出してsendしていく。
3. なんらかの原因で接続が切れた時、ws.onclose do ... endが呼び出される。settings.sockets.delete(ws)でクライアントの情報を削除する。

ブラウザ側の準備

views/index.erb
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <script src="/application.js" type="text/javascript"></script>
  <title>チャット</title>
</head>
<body>
  <form id="form">
   <input type="text" id="send-msg">
   <input type="submit">
 </form>
 <ul id="msgs"></ul>
</body>
</html>
public/application.js
  window.addEventListener('load', () => {
    let msgbox = document.getElementById('msgs');
    let form = document.getElementById('form');
    let sendMsg = document.getElementById('send-msg');
    let ws = new WebSocket('ws://' + window.location.host + '/websocket');

    ws.onopen = () => console.log('connection opened');
    ws.onclose = () => console.log('connection closed');
    ws.onmessage = m => {
      let li = document.createElement('li');
      li.textContent = m.data;
      msgbox.insertBefore(li, msgbox.firstChild);
    }

    send_msg.addEventListener('click', () => sendMsg.value = '');

    form.addEventListener('submit', e => {
      ws.send(sendMsg.value);
      sendMsg.value = '';
      e.preventDefault();
    });
  });

htmlの方はそれといった解説がないから省略。
jsの方を軽く解説しておくと・・・
1. var ws = new WebSocket('wss://' + window.location.host + "/websocket");で宣言される変数wsがWebsocket通信用の主役オブジェクト。このWebSocketオブジェクトはGemによって追加されたものじゃなくてjsが元々持っているオブジェクトなので注意。
2. あとは、だいたいサーバーと同じ作りになっています。ws.onopenは、接続を開始した時に実行する関数を代入し、ws.onmessageはサーバからsendされた時に実行される関数、ws.oncloseは接続閉じる時に呼ばれる関数を代入する変数です。

おまけ

このsinatra-websocketでチャンネルを使えるかわからなかったので、独自に実装する方法を軽く紹介して見ます。(これが正しい方法なのかはわからない・・・。)
Websocketでサーバからメッセージが送られるのは、app.rbでsendメソッドが呼び出されたクライアントだけでした。settings.sockets.eachで全てのクライアントを呼び出さなければチャンネルが作れるのではないでしょうか?
settings.socketsはただの配列なので、この中に配列を入れることはもちろんできます。何らかの方法でその配列を呼び出してeachでまわしたら、(擬似)チャンネルが完成するはずです。