3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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 に変更されているようだ。

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_* ハンドラでは、クライアントにメッセージを送らない場合は、

{:ok, state}

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

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

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

{: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ハンドラとして登録する。

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オプションは仮の値を指定する。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?