@ledsun blog

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

Async::WebSocket gemの素振り

ruby.wasmからのWebSocket - @ledsun blog では、faye-websocket-rubyを使いました。 このWebSocketサーバーを GitHub - socketry/async-websocket: Asynchronous WebSocket client and server, supporting HTTP/1 and HTTP/2 for Ruby. に変えてみましょう。

環境構築

bundle init
bundle add falcon async-websocket

WebSocketサーバーの作成

https://github.com/socketry/async-websocket/blob/534cad73595292b82b88de3b66d10773106c254d/examples/rack/config.ru にあるサンプルを参考にします。

#!/usr/bin/env -S falcon serve --bind http://127.0.0.1:7070 --count 1 -c
# frozen_string_literal: true

require 'async/websocket/adapters/rack'

app = lambda do |env|
    response = Async::WebSocket::Adapters::Rack.open(env) do |connection|
        while message = connection.read
            connection.write message
        end
    end or [404, {}, []]
end

run app

ruby.wasmのWebSocketクライアントを含んだindex.htmlを返せるようにします。

require 'async/websocket/adapters/rack'

app = lambda do |env|
  response = Async::WebSocket::Adapters::Rack.open(env) do |connection|
    while message = connection.read
       connection.write message
     end
  end or [200, {'Content-Type' => 'text/html'}, [File.read('index.html')]]
end

run app

index.htmlは前回使った物をそのまま使います。

<!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/[email protected]/dist/browser.script.iife.js"></script>
<script type="text/ruby">
  require "js"

  ws = JS.global[:WebSocket].new("ws://localhost:9292")
  ws[:onopen] = -> (event) {
    ws.call(:send, ["Hello, world! from Ruby"])
  }
  ws[:onmessage] = ->(event) {
    p event[:data]
  }
</script>

</html>

動作確認

bundle exec falcon serve --bind "http://localhost:9292"コマンドで、WebSocketサーバーを起動します。

ledsun@MSI:~/d/async_websocket►bundle exec falcon serve --bind "http://localhost:9292"
  0.0s     info: Falcon::Command::Serve [oid=0xf50] [ec=0xf78] [pid=135904] [2024-08-28 16:22:36 +0900]
               | Falcon v0.47.10 taking flight! Using Async::Container::Forked {:count=>8, :restart=>true}.
               | - Binding to: #<Falcon::Endpoint http://localhost:9292/ {}>
               | - To terminate: Ctrl-C or kill 135904
               | - To reload configuration: kill -HUP 135904
 0.03s     info: Falcon::Service::Server [oid=0xfc8] [ec=0xf78] [pid=135904] [2024-08-28 16:22:36 +0900]
               | Starting server on #<Falcon::Endpoint http://localhost:9292/ {}>

ブラウザで http://localhost:9292/ を開きます。 開発コンソールでWebSocketの通信を確認します。

WebSocketでメッセージを送受信している内容を確認してるスクリーンショット

成功しています。 async-websocketの素振りをした上に、Falconデビューを果たしました。

おまけ

ブラウザを閉じるとWebSocketサーバーで次のようなエラーが表示されます。

    4m     warn: Async::Task: Reading HTTP/1.1 requests for Async::HTTP::Protocol::HTTP1::Server. [oid=0x1180] [ec=0x11a8] [pid=136177] [2024-08-28 16:50:08 +0900]
               | Task may have ended with unhandled exception.
               |   Protocol::WebSocket::ClosedError: Protocol::WebSocket::ClosedError
               |   → /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/protocol-websocket-0.15.0/lib/protocol/websocket/connection.rb:165 in 'Protocol::WebSocket::Connection#receive_close'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/protocol-websocket-0.15.0/lib/protocol/websocket/close_frame.rb:64 in 'Protocol::WebSocket::CloseFrame#apply'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/protocol-websocket-0.15.0/lib/protocol/websocket/connection.rb:111 in 'Protocol::WebSocket::Connection#read_frame'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/protocol-websocket-0.15.0/lib/protocol/websocket/connection.rb:272 in 'Protocol::WebSocket::Connection#read'
               |     config.ru:8 in 'block (4 levels) in <top (required)>'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-websocket-0.28.0/lib/async/websocket/adapters/http.rb:41 in 'block in Async::WebSocket::Adapters::HTTP.open'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-http-0.71.0/lib/async/http/body/hijack.rb:39 in 'Async::HTTP::Body::Hijack#call'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/protocol-rack-0.6.0/lib/protocol/rack/body/streaming.rb:56 in 'Method#call'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/protocol-rack-0.6.0/lib/protocol/rack/body/streaming.rb:56 in 'Protocol::Rack::Body::Streaming#call'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-http-0.71.0/lib/async/http/protocol/http1/server.rb:72 in 'block in Async::HTTP::Protocol::HTTP1::Server#each'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-2.16.1/lib/async/task.rb:318 in 'Async::Task#defer_stop'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-http-0.71.0/lib/async/http/protocol/http1/server.rb:57 in 'Async::HTTP::Protocol::HTTP1::Server#each'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-http-0.71.0/lib/async/http/server.rb:50 in 'Async::HTTP::Server#accept'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/io-endpoint-0.13.1/lib/io/endpoint/wrapper.rb:182 in 'block (2 levels) in IO::Endpoint::Wrapper#accept'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-2.16.1/lib/async/task.rb:197 in 'block in Async::Task#run'
               |     /home/ledsun/.rbenv/versions/3.4-dev/lib/ruby/gems/3.4.0+0/gems/async-2.16.1/lib/async/task.rb:422 in 'block in Async::Task#schedule'

WebSocketコネクションのcloseイベントをハンドリングしないと行けなさそうです。