この記事は 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にあげてるのでご自由にどうぞ。
本当はデモサーバを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が送られて欠損したときにどのようなデータが受信されるのかとかあまり理解してないので、気が向いたときにこのサーバを使って実験しようと思います。