Edited at

ElixirのPhoenixでUDPサーバを実装する

この記事は mixiグループ Advent Calendar 2018 の20日目の記事です。

去年はネットワークプロトコルのポエムを書いたので、今年は実際にUDPサーバをelixirで実装してみました。去年はめっちゃQUIC推しだったのにQUIC実装じゃないんかいって思うかもですが、QUICについてはvさんに期待してます。まぁQUICを実装するためにはそもそもUDPをいじれる土台を作らないとだめなので、これをベースに遊べればいいなと思ってます。

そういえば、WebSocket2(WebSocket over http/2)がdraftからPROPOSED STANDARDになりましたね。素晴らしい。0RTTでWebSocketがリコネクトできる時代が早く来てほしいです。


UdpPhx

早速本題ですが, udp_phx というPhoenixの上にUDPのサンプルサーバを作ってみました。

Phoenixのsupervisorに作ったUDPサーバをぶら下げて、phx.serverを立ち上げるとPhoenixアプリケーションのいち部としてUDPサーバが立ち上がります。

Dockerfileを付属したのでDocker環境がある人は簡単に立ち上げられると思います。

git clone github.com:oppai/udp_phx

cd udp_phx
docker-compose build
docker-compose up -d

# Webサーバに接続
curl -i 0.0.0.0:4000/hello
# UDPで接続
nc -u 0.0.0.0 8080
< aaaa
> {:udp, #Port<0.6206>, {172, 26, 0, 1}, 52430, "aaaa\n"}>

ローカルの8080ポートにマッピングしてるので、ncコマンドで接続することができます。入力しデータに対してinspectした結果をそのまま返しています。タプルの先頭から、パケットの種類、Socketオブジェクト、送信側ホスト、送信側ポート、データ領域です。

コアになるアプリケーションは UdpPhxUdp.Application モジュールで erlangの :gen_udp をElixirの GenServer でWrapしたものです。構成はPhoenixに則って、 Webサーバと同じ階層にUDPサーバを作りました。

defmodule UdpPhxUdp.Application do

@moduledoc """
深く考えず作ったためにudpという文言が2つもついてしまった気持ち悪いアプリケーション
"""

use GenServer
require Logger

def start_link(port \\ 12345) do
Logger.info("UDP server binding #{port}")
GenServer.start_link(__MODULE__, port)
end

def init(port) do
:gen_udp.open(port, [:binary])
end

def handle_info({:udp, socket, host, port, _} = data, server_socket) do
:gen_udp.send(socket, host, port, "#{inspect(data)}>\n")
{:noreply, server_socket}
end
end

今回作成したソースコードはGitHubにあげてるのでご自由にどうぞ。

https://github.com/oppai/udp_phx

本当はデモサーバをHerokuに上げたかったのですが、Herokuはランダム設定されたポートを $HOST に保存して80/443をフォワーディングする仕組みになっており、任意のポートを利用することができませんでした。 またポートのLISTENがTCPにしか許可されない様で、指定されたランダムポートをUDPでLISTENしたところ、PermissionErrorが返ってきました、残念😢


Elixirのパタンマッチを使ってデータをハンドリング

データをパターンマッチでハンドリングできることがElixirでUDPを扱うメリットかなと思います。

先程の handle_info/2 をデータ拡張してみます。

+  def handle_info({:udp, socket, host, port, "ping\n"}, server_socket) do

+ :gen_udp.send(socket, host, port, "pong\n")
+ {:noreply, server_socket}
+ end

def handle_info({:udp, socket, host, port, data}, server_socket) do
:gen_udp.send(socket, host, port, "#{inspect(data)}>\n")
{:noreply, server_socket}
end

"ping"という文字列が来たときに"pong"と返す事ができるようになりました。またElixirはバイナリデータのパタンマッチもすることができるので、文字列だけじゃなくてバイナリデータに対しても簡単にハンドリングすることができます。便利。


感想

折角UDPサーバ作ったので次は簡単なUDPアプリケーションを作ってみたいなと思います。通信速度が早い今の時代UDPのサーバなんて作る必要ないと思いますが、PhoenixだけでRESTAPI/WebSocket/UDPというマルチサーバを作ることができます。何よりSupervisorの上に乗っかってるのが安心できますよね。

実はTCPのパケットがどの様に再送するのかとか、巨大なUDPが送られて欠損したときにどのようなデータが受信されるのかとかあまり理解してないので、気が向いたときにこのサーバを使って実験しようと思います。