faye-websocket-rubyをWebSocketサーバーとして使う

# これは何か？

[faye-websocket-ruby](https://github.com/faye/faye-websocket-ruby)を使ってHello Worldします。
READMEと同レベルの情報しか書きません。

# faye-websocket-rubyとは？

[Faye](https://faye.jcoglan.com/)というPubSubライブラリのRuby実装です。実質的にはRubyでWebSocketを扱う際に使われるライブラリです。Rails5のActionCableにも使われているとか、使われていないとか。[Fayeの発音](https://www.youtube.com/watch?v=RaLb5rGBuXk)はフェイのようです。

Server-sent eventsにも対応しており、サーバからブラウザへの一方向のpush通知にも使えます。今回は扱いません。

# 準備
前提条件として、Ruby 3.3.4 がインストールされているものとします。

## Gemfileを作る

```sh
bundle init
```

Gemfileというファイルができます。
`faye-websocket`をインストールします。

```sh
bundle add faye-websocket
```

を実行すると、指定したライブラリと、そのライブラリが使うライブラリをまとめてインストールします。

# 使い方
## app.rb の作成
READMEに記載の`app.rb`を使います。

```rb
require 'faye/websocket'

App = lambda do |env|
  if Faye::WebSocket.websocket?(env)
    ws = Faye::WebSocket.new(env)

    ws.on :message do |event|
      ws.send(event.data)
    end

    ws.on :close do |event|
      p [:close, event.code, event.reason]
      ws = nil
    end

    # Return async Rack response
    ws.rack_response

  else
    # Normal HTTP request
    [200, {'Content-Type' => 'text/plain'}, ['Hello']]
  end
end
```

## app.rbの説明
### Rackへの対応
faye-websocket-rubyは、Rack仕様に対応しています。
Rackの`env`変数を使って、WebSocketコネクションを作ることができます。

`Faye::WebSocket.websocket?(env)`でWebSocketのリクエストであるか判定できます。
`Faye::WebSocket.new(env)`でWebSocketのコネクションインスタンスを取得できます。

### WebSocketのイベントを扱う

WebSocketのイベントハンドラーは

```rb
ws.on :message do |event|
  ws.send(event.data)
end
```

こんな素直なスタイルで書けます。

## スレッドベースのWebアプリケーションサーバを使う
faye-websocket-rubyは、非同期に結果を返すために内部で[EventMachine](https://github.com/eventmachine/eventmachine)を使っています。EventMachineは、IO以外の非同期な処理を非同期に実行するためにスレッドを使います。

Rack仕様に対応した、スレッドベースのWebアプリケーションサーバが使えます。
代表的なものに

- [Thin](http://code.macournoyer.com/thin/)
- [Puma](http://puma.io/)

があります。WEBrickでは動きません。



## Thinでfaye-websocketを動かす

### Gem のインストール

```rb
bundle add thin
```


### config.ru の作成

```rb
require 'thin'
require_relative './app'

Faye::WebSocket.load_adapter('thin')

run App
```

#### 注意点

`config.ru`に`Faye::WebSocket.load_adapter('thin')`を書かないと、ブラウザからWebSocketで接続する際に、次のエラーが出ます。

```
VM293:1 WebSocket connection to 'ws://localhost:9292/' failed: Error during WebSocket handshake: 'Upgrade' header is missing
```

なぜ、必要なのかは知りません。

### 起動

```sh
thin start -R config.ru -p 9292
```

### 動作確認（ブラウザの開発コンソールから）

ブラウザで http://localhost:9292 を開くと`Hello`が表示されます。
開発コンソールを開いて

```js
const ws = new WebSocket('ws://localhost:9292')
ws.onmessage = console.log
ws.send('hello')
```
を実行すると、コンソールに`MessageEvent`が表示されます。

## Pumaでfaye-websocketを動かす

### Gemfile

```rb
# frozen_string_literal: true
source "https://rubygems.org"

gem "rack"
gem "puma"
gem "faye-websocket"
```



### config.ru
```rb
require 'puma'
require './app'

run App
```

### 起動
```sh
puma config.ru -p 9292
```

#### rackupコマンドを使う場合

```sh
rackup -s puma -E deployment
```

`-E deployment`が重要です。
これをつけずにrackupコマンドを実行すると、Rack::Lintモジュールがマウントされます。[^1]

[^1]: https://github.com/rack/rack/blob/5c36acac4d298dcf04c58a10014c849a478da53f/lib/rack/server.rb#L242]

```
ws.rack_response
```
は固定で、ダミーのトリプル`[-1, {}, []]`を返します。[^2]
これがRack::Lintに引っかかり、次のエラーが出ます。

[^2]: https://github.com/faye/faye-websocket-ruby/blob/bbef55a2cd7fd52d145cdc73c0d162573b2a83a8/lib/faye/websocket.rb#L90

```
#<Thread:0x00007fa46801ad10@/usr/local/lib/ruby/gems/2.5.0/gems/faye-websocket-0.10.7/lib/faye/websocket.rb:40 run> terminated with exception (report_on_exception is true):
Traceback (most recent call last):
	10: from /usr/local/lib/ruby/gems/2.5.0/gems/faye-websocket-0.10.7/lib/faye/websocket.rb:40:in `block in ensure_reactor_running'
	 9: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:194:in `run'
	 8: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:194:in `run_machine'
	 7: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:974:in `run_deferred_callbacks'
	 6: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:974:in `times'
	 5: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:977:in `block in run_deferred_callbacks'
	 4: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:237:in `block in schedule'
	 3: from /usr/local/lib/ruby/gems/2.5.0/gems/faye-websocket-0.10.7/lib/faye/rack_stream.rb:39:in `block in hijack_rack_socket'
	 2: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:741:in `attach'
	 1: from /usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:763:in `attach_io'
/usr/local/lib/ruby/gems/2.5.0/gems/eventmachine-1.2.5/lib/eventmachine.rb:763:in `attach_fd': no implicit conversion of Rack::Lint::HijackWrapper into Integer (TypeError)
Rack::Lint::LintError: Status must be >=100 seen as integer
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/lint.rb:20:in `assert'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/lint.rb:620:in `check_status'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/lint.rb:51:in `_call'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/lint.rb:37:in `call'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/show_exceptions.rb:23:in `call'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/common_logger.rb:33:in `call'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/chunked.rb:54:in `call'
	/usr/local/lib/ruby/gems/2.5.0/gems/rack-2.0.4/lib/rack/content_length.rb:15:in `call'
	/usr/local/lib/ruby/gems/2.5.0/gems/puma-3.11.0/lib/puma/configuration.rb:225:in `call'
	/usr/local/lib/ruby/gems/2.5.0/gems/puma-3.11.0/lib/puma/server.rb:624:in `handle_request'
	/usr/local/lib/ruby/gems/2.5.0/gems/puma-3.11.0/lib/puma/server.rb:438:in `process_client'
	/usr/local/lib/ruby/gems/2.5.0/gems/puma-3.11.0/lib/puma/server.rb:302:in `block in run'
	/usr/local/lib/ruby/gems/2.5.0/gems/puma-3.11.0/lib/puma/thread_pool.rb:120:in `block in spawn_thread'
::1 - - [13/Feb/2018:18:08:58 +0900] "GET / HTTP/1.1" 500 1195 0.0109
```


### 動作確認

Thinと一緒です。