WebSocketを使いたい理由
ruby.wasmでdRubyをやりたいです。 dRubyではサーバーとクライアントで双方からリクエストをおくります。 HTTPはクライアントからサーバーへの一方向のリクエストを想定しているので不十分です。
Server Sent Eventsを使う手もあります。 WebSocketの方がServer Sent Eventsより新しいので、WebSocketを使います。 なんとなくWebSocketの方が効率が良さそうとか、APIがこなれてそうとか思っていますが、Server Sent Eventsを使った経験がないので憶測です。
faye-websocket-rubyを使ってWebSocketサーバーを用意する
まずWebSocketのサーバーを用意します。 RubyでWebSocketを使うには GitHub - faye/faye-websocket-ruby: Standards-compliant WebSocket client and server が使えます。 GitHub - socketry/async-websocket: Asynchronous WebSocket client and server, supporting HTTP/1 and HTTP/2 for Ruby.もあります。 なんとなく新しい方がいいなら、async-websocketの方が良い気もします。 今回は、過去に使ったことがあるfaye-websocket-rubyを使います。
faye-websocket-rubyをWebSocketサーバーとして使う #Ruby - Qiita を読むとWebSocketサーバーの起動方法が書いてあります。 サーバーはこのサンプルをそのまま使います。
thin start -R config.ru -p 9292
で起動します。
const ws = new WebSocket('ws://localhost:9292') ws.onmessage = console.log ws.send('hello')
でブラウザからサーバーにリクエストを送ります。 今回はこれをruby.wasmで書き換えます。
WebSocketサーバーからindex.htmlを返す
ruby.wasmの動作確認をしやすくするために、index.hmtlを返すように改造します。
# Normal HTTP request [200, {'Content-Type' => 'text/html'}, [File.read('index.html')]]
index.html
次のようなHTMLを用意すると、ruby.wasmでWebSocketリクエストを送信できることが確認できます。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <script src="https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.6.2/dist/browser.script.iife.js"></script> <script type="text/ruby"> require "js" class JS::Object undef_method :send end ws = JS.global[:WebSocket].new("ws://localhost:9292") ws[:onopen] = -> (event) { ws.send("Hello, world! from Ruby") } ws[:onmessage] = ->(event) { p event[:data] } </script> </html>
WebSocketサーバーを起動して http://localhost:9292 を開くと開発コンソールに次のような表示がされます。
動作説明
このWebSocketサーバーは送られてきたメッセージをエコーバックしてきます。
ws.send("Hello, world! from Ruby")
で送ったメッセージをエコーバックします。
ws[:onmessage] = ->(event) { p event[:data] }
でサーバーから送り返されたメッセージをコンソールに出力しています。
ソースコード解説
ruby.wasmでWebSocketリクエストを送るときに興味深い点があります。
class JS::Object undef_method :send end
です。 これがないと次のようなエラーがおきます。
Error: /bundle/gems/js-2.6.2/lib/js.rb:184:in
method_missing': undefined method
Hello, world! from Ruby' for an instance of JS::Object (NoMethodError)
Hello, world! from Ruby
メソッドが無いので怒られています。
そんなメソッドあるわけないので、それはそうです。
ではなぜ、Hello, world! from Ruby
という変わった名前のメソッドが実行されるのでしょうか?
WebSocket: send() メソッド - Web API | MDN の代わりに
Object#send (Ruby 3.3 リファレンスマニュアル) が呼び出されています。
JS::Object
のJavaScriptメソッドの呼び出しは、
BasicObject#method_missing (Ruby 3.3 リファレンスマニュアル) で実装されています。
JS::Object
にsend
メソッドが定義されていたらRubyのsend
メソッドが呼び出されます。
全てのRubyオブジェクトにはsend
メソッドが定義されています。
Object#send (Ruby 3.3 リファレンスマニュアル) です。
当然、このsend
メソッドが呼び出されます。
というわけでWebSocketのsend
メソッドを呼び出すには
class JS::Object undef_method :send end
が、必要なのでした。
感想
JavaScriptでWebSocketを書いたときはsend
メソッドで違和感がありませんでした。
Rubyistは、特にライブラリーの時は、本能的にsend
メソッドを避けるのです。
それでRuby脳でもsend
に違和感を感じませんでした。
ruby.wasmにすると名前が衝突します。 JavaScript脳でもRuby脳でも、まったく気がつかない現象なので、面白かったです。