@ledsun blog

無味の味は佳境に入らざればすなわち知れず

ruby.wasmからのWebSocket

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リクエストをおくったログが表示されたスクリーンショット

動作説明

このWebSocketサーバーは送られてきたメッセージをエコーバックしてきます。 ws.send("Hello, world! from Ruby")で送ったメッセージをエコーバックします。

  ws[:onmessage] = ->(event) {
    p event[:data]
  }

でサーバーから送り返されたメッセージをコンソールに出力しています。

ソースコード解説

ruby.wasmでWebSocketリクエストを送るときに興味深い点があります。

  class JS::Object
    undef_method :send
  end

です。 これがないと次のようなエラーがおきます。

JS::Objectのsendメソッドが定義されているときに起きるエラーのスクリーンショット

Error: /bundle/gems/js-2.6.2/lib/js.rb:184:in method_missing': undefined methodHello, 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::ObjectJavaScriptメソッドの呼び出しは、 BasicObject#method_missing (Ruby 3.3 リファレンスマニュアル) で実装されています。 JS::Objectsendメソッドが定義されていたらRubysendメソッドが呼び出されます。 全ての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脳でも、まったく気がつかない現象なので、面白かったです。