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 に変更されているようだ。
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ハンドラとして登録する。
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オプションは仮の値を指定する。