ElixirでPlugからWebSocketをサービスする

ElixirのPlugベースのWebAPIサーバに、WebSocket対応を追加したメモです。

テストしたElixirやモジュールのバージョンは以下の通り。

| アプリ/パッケージ | バージョン |
|:------|:------|
| Elixir | 1.10.4 |
| Cowboy | 2.8.0 |
| Plug | 1.10.3 |
| Plug.Cowboy | 2.3.0 |

## WebSocketハンドラ
WebSocketの通信を行うプロセスは、Cowboyハンドラ・モジュールとして記述する。旧バージョンのCowboyでは、ビヘイビア名が cowboy_websocket_handler だったが、今は cowboy_websocket に変更されているようだ。

``` Elixir:websocket_handler.ex
defmodule WebApi.WebsocketHandler do
    @behaviour :cowboy_websocket

    def init(req, state) do
        # :cowboy_websocketを返すとWebsocketにUPGRADEする
        {:cowboy_websocket, req, state}
    end

    def websocket_init(state) do
        # クライアントが接続した直後に呼び出される
        Process.send_after(self(), :ping, 10_000)
        # クライアントにメッセージを送ってみる。
        {:reply, {:text, "start"}, state}
    end

    def websocket_handle(:ping, state) do
        # pingメッセージにpongメッセージを返す
        {:reply, :pong, state}
    end

    def websocket_handle({:ping, frame}, state) do
        {:reply, {:pong, frame}, state}
    end

    def websocket_handle(:pong, state) do
        # pongメッセージを受け流す
        {:ok, state}
    end

    def websocket_handle({:pong, _frame}, state) do
        {:ok, state}
    end

    def websocket_handle(frame, state) do
        # :ping = frame
        # :pong = frame
        # {:text, BINARY} = frame
        # {:binary, BINARY} = frame
        # {:ping, BINARY} = frame
        # {:pong, BINARY} = frame
        # ...
        {:reply, {:text, message}, state}
    end

    def websocket_info(:ping, state) do
        Process.send_after(self(), :ping, 10_000)
        {:reply, :ping, state}
    end

    def websocket_info(message, state) do
        #...
        {:reply, {:text, message}, state}
    end

    def terminate(reason, req, state) do
        #...
        :ok
    end

end
```
init/2 コールバックは、cowboyのdispatchルールから呼び出されるハンドラで、:cowboy_websocketを含むタプルを返すと、Websocketプロトコルへのアップグレードがおこる。

websocket_init/2 コールバックは、クライアントとの接続後にディスパッチされたプロセスで、最初に呼び出される。:replyを含むタプルを返すと、クライアントからの接続直後にメッセージを送ることができる。

websocket_* ハンドラでは、クライアントにメッセージを送らない場合は、

``` Elixir
{:ok, state}
```

を返す。メッセージを送る場合は、テキストメッセージであれば、

``` Elixir
{:reply, {:text, BINARY}, state}
```

を返す。コネクションを切断する場合は、

``` Elixir
{:stop, state}
```

websocket_handle/2 コールバックは、クライアントがメッセージを送ると呼び出される。パラメータには、クライアントからのメッセージの形式に応じて、:ping、:pong、{:text, BINARY}、{:binary, BINARY}、{:ping, BINARY}、{:pong, BINARY} が、与えられる。

websocket_info/2 コールバックは、プロセスに対するメッセージのハンドラで、クライアントに対してメッセージを送るために利用できる。メッセージは、Process.send/3 で送ることができる。

定期的にメッセージ交信をしないと、コネクションがタイムアウトするので、適宜、pingメッセージを送る必要がある。

## Supervisor

Supervisorの子に Plug.Cowboy を登録する際、Websocketのハンドラモジュールは、dispatchオプションでcowboyハンドラとして登録する。

``` Elixir:application.ex
defmodule PlugWebsocket.Application do
    use Application

    def start(_type, _args) do
        dispatch = [
            {:_,
             [
                 {"/ws", WebApi.WebsocketHandler, []},
                 {:_, Plug.Cowboy.Handler, {WebApi.PlugTop, []}},
             ]}
        ]

        children = [
            # Starts a worker by calling: PlugWebsocket.Worker.start_link(arg)
            # {PlugWebsocket.Worker, arg}
            Plug.Cowboy.child_spec(scheme: :http, plug: WebApi.PlugTop,
                                   options: [port: 4040, dispatch: dispatch]),
        ]
```

Plug.Cowboy.child_spec/1 の :plugオプションは、必須オプションなので指定しないとエラーになるが、dispatchオプションの指定で上書きされるので、:plugオプションは仮の値を指定する。