Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away