ElixirでUDPを受信する(テキスト, XML)

#1.はじめに

Elixirでブロードキャストに投げられたUDPデータを受信するまでの手順をまとめました。

> 最終的に、下記リンク先のツール「UECS通信実用規約に基づいたロギング用ソフトウェア」のElixir版を作るのを目標にしています。
[ユビキタス環境制御システム(UECS)技術](https://www.naro.affrc.go.jp/laboratory/nivfs/contents/kenkyu_joho/uecs/index.html)
[ユビキタス環境制御システム「UECS」](https://uecs.jp/)
(20/5/25)完成版：[UecsListner](https://github.com/trihome/UecsListner)


#2.文字を受信して表示

まず最初に、UDPのブロードキャストで受信した文字列を、表示するだけの機能を作ります。

```sh:コマンドライン
$ cd ~/gitwork/elixir
#プロジェクトをつくる（スーパーバイザも有効に）
elixir$ mix new udpapp --sup
elixir$ cd udpapp/lib/udpapp/
#udpのソースファイル
elixir/udpapp/lib/udpapp$ touch udp.ex
```

###ソースファイル

UDPの処理を実装します。[^1] [^2]

[^1]: https://elixirforum.com/t/error-in-handle-info-2-when-implementing-a-udp-echo-server/17318
[^2]: https://daruiapprentice.blogspot.com/2016/04/using-gen-udp-module-in-elixir.html

```elixir:udpapp/lib/udpapp/udp.ex
defmodule Udpapp.Udp do
  @moduledoc """
  Documentation for `Udp`.
  """

  use GenServer
  require Logger

  def start_link(port, opts) do
    #application.exのworkerから起動したときに、この関数が実行されます
    Logger.info("* #{__MODULE__}: start_link call")
    #GenServerを起動します
    GenServer.start_link(__MODULE__, port, opts)
  end

  def init(port) do
    #GenServerが起動したときにこの初期化関数が実行されます
    Logger.debug("* #{__MODULE__}: UDP server binding #{port}")
    #UDPソケットを開きます
    :gen_udp.open(port, [:binary])
    {:ok, port}
  end

  def handle_info({:udp, socket, ip, port, data}, server_socket) do
    #UDPを受信したらこの関数が実行されます
    Logger.debug("* #{__MODULE__}: handle_info > #{inspect(socket)}, #{inspect(ip)}, #{inspect(port)}, #{inspect(data)}")
    {:noreply, server_socket}
  end
end
```

スーパーバイザを実装します。
このファイルは、プロジェクトを生成したときに自動的に作られるので、`#↓ここを追記`の所を書き加えます。

```elixir:udpapp/lib/udpapp/application.ex
defmodule Udpapp.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  #↓ここを追記
  import Supervisor.Spec

  def start(_type, _args) do
    children = [
      # Starts a worker by calling: Udpapp.Worker.start_link(arg)
      # {Udpapp.Worker, arg}

      #↓ここを追記。10000はポート番号
      worker(Udpapp.Udp, [10000, [name: :udpserver]]),
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Udpapp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

###サーバ側実行

iexを起動して待機状態にします。

```sh:サーバ側：コマンドライン
$ iex -S mix
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1]
23:47:00.223 [info]  * Elixir.Udpapp.Udp: start_link call
23:47:00.229 [debug] * Elixir.Udpapp.Udp: UDP server binding 10000
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

#ここから先は後述のncコマンド実行時に表示されます。
23:49:57.845 [debug] * Elixir.Udpapp.Udp: handle_info > #Port<0.6>, {192, 168, 0, 6}, 38576, "hoge-\n"
23:50:04.025 [debug] * Elixir.Udpapp.Udp: handle_info > #Port<0.6>, {192, 168, 0, 6}, 34197, "fuga\n"
#終了
[Ctrl-\]
$
```

###クライアント側実行

別のターミナルを開いて、`nc`コマンド[^3]を実行します。

[^3]: https://qiita.com/chenglin/items/70f06e146db19de5a659

```sh:クライアント側：コマンドライン
$ echo "hoge-" | nc -ub 192.168.0.255 10000
[Ctrl-c]
$ echo "fuga" | nc -ub 192.168.0.255 10000
[Ctrl-c]
```

|`nc`引数|意味|
|:---|:---|
|-ub|u: udp通信、b: ブロードキャスト|
|192.168.0.255|送信先アドレス|
|10000|ポート番号|

とりあえず、文字列の受信まで確認できました。

#3.XMLデータを受信したあと、パースして表示

[ユビキタス環境制御システム「UECS」](https://uecs.jp/)では、下記のXML形式のデータをUDPで送信しています。

```xml:（例）uecs_rawdata.xml
<?xml version="1.0"?>
<UECS ver="1.00-E10">
  <DATA type="InAirCO2" room="1" region="101" order="0" priority="29">447</DATA>
  <IP>192.168.5.191</IP>
</UECS>
```

このXMLデータをパースして、表示してみます。
***※上記のデータを、`uecs_rawdata.xml`として保存しておいてください。（あとでncコマンドでこの内容を送ります）***

###ソースファイル

UDPの処理を実装したモジュールに追記します。

```elixir:udpapp/lib/udpapp/udp.ex
defmodule Udpapp.Udp do

（･･･省略･･･）

  use GenServer
  require Logger
  #↓ここを追記
  import SweetXml

（･･･省略･･･）

  def handle_info({:udp, socket, ip, port, data}, server_socket) do
    #UDPを受信したらこの関数が実行されます
    Logger.debug("* #{__MODULE__}: handle_info > #{inspect(socket)}, #{inspect(ip)}, #{inspect(port)}, #{inspect(data)}")
    {:noreply, server_socket}

    #↓ここから追記
    uecs_data =
      data
      # 内容をパース
      |> perse_uecs()

    # XMLの内容を表示
    Logger.debug(" ip: #{inspect(uecs_data[:ip])}")
    Logger.debug(" data: #{inspect(uecs_data[:data])}")
    Logger.debug(" type: #{inspect(uecs_data[:type])}")
    Logger.debug(" room: #{inspect(uecs_data[:room])}")
    Logger.debug(" region: #{inspect(uecs_data[:region])}")
    Logger.debug(" order: #{inspect(uecs_data[:order])}")
    Logger.debug(" priority: #{inspect(uecs_data[:priority])}")
  end

  #↓この関数を追記
  def perse_uecs(xmldoc) do
    # XMLデータを読み込んでパースして、
    # マップに変換して返り値にする
    %{
      :ip => xmldoc |> xpath(~x"/UECS/IP/text()"l),
      :data => xmldoc |> xpath(~x"/UECS/DATA/text()"l),
      :type => xmldoc |> xpath(~x"/UECS/DATA/@type"l),
      :room => xmldoc |> xpath(~x"/UECS/DATA/@room"l),
      :region => xmldoc |> xpath(~x"/UECS/DATA/@region"l),
      :order => xmldoc |> xpath(~x"/UECS/DATA/@order"l),
      :priority => xmldoc |> xpath(~x"/UECS/DATA/@priority"l)
    }
  end
end
```

mixファイルに、xmlパーサ「[sweet_xml](https://hexdocs.pm/sweet_xml/SweetXml.html)」のライブラリを追記します。

```elixir:udpapp/mix.exs
defmodule Udpapp.MixProject do
  use Mix.Project

（･･･省略･･･）

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      #↓ここを追記
      {:sweet_xml, "~> 0.6.5"}
    ]
  end
end
```

###サーバ側実行

```sh:サーバ側：コマンドライン
#ライブラリの依存関係の処理
$ mix deps.get
Resolving Hex dependencies...
Dependency resolution completed:
New:
  sweet_xml 0.6.6
* Getting sweet_xml (Hex package)

#コンパイル・実行
$ iex -S mix
Erlang/OTP 22 [erts-10.6.4] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1]
00:30:25.217 [info]  * Elixir.Udpapp.Udp: start_link call
00:30:25.224 [debug] * Elixir.Udpapp.Udp: UDP server binding 10000
Interactive Elixir (1.9.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

#ここから先は後述のncコマンド実行時に表示されます。
00:30:26.667 [debug] * Elixir.Udpapp.Udp: handle_info > #Port<0.6>, {192, 168, 0, 6}, 35867, "<?xml version=\"1.0\"?><UECS ver=\"1.00-E10\"><DATA type=\"InAirCO2\" room=\"1\" region=\"101\" order=\"0\" priority=\"29\">447</DATA><IP>192.168.5.191</IP></UECS>\n\n"
 
00:30:26.754 [debug]  ip: ['192.168.5.191']
00:30:26.755 [debug]  data: ['447']
00:30:26.755 [debug]  type: ['InAirCO2']
00:30:26.755 [debug]  room: ['1']
00:30:26.755 [debug]  region: ['101']
00:30:26.755 [debug]  order: ['0']
00:30:26.755 [debug]  priority: ['29']
```

###クライアント側実行

クライアント側から、先ほど作ったxmlファイルを送信します。

```sh:クライアント側：コマンドライン
$  nc -ub 192.168.0.255 10000 < uecs_rawdata.xml 
[Ctrl-c]
```

ここまで、UDPデータの受信と、XMLのパーサまで試してみました。


[次回はデータベースへのINSERTとSELECT](https://qiita.com/myasu/items/d763e248d5d5df428d21)を試してみます。


#Appendix.UECSのデータ形式

[UECSの通信規約](https://uecs.jp/uecs/uecs-5.html)として定義されています。

UECSでのノード間の通信には、XMLで記述された通信パケットを使用。
共用通信子(CCM: Common Correspondence Message）には、計測値、状態変数、動作指示情報、設定値、目標値などを記述。

|属性|意味|型|範囲|
|:--|:--|:--|:--|
|ip|発信ノードのIPアドレス|string|-|
|data|データ|float||
|type|CCM識別子|string|3文字以上19文字以下の英数字。内容はUECS通信基本規約の中で定義済。|
|room|部屋番号|int|0～127、0は全部屋に対するブロードキャスト|
|region|系統番号|int|0～127、0は同一部屋・全系統番号に対するブロードキャスト|
|order|通し番号|int|0～30000、0は同一系統・全通し番号に対するブロードキャスト|
|priority|優先順位|int|0～30、優先順位の管理を行わない場合は30に固定|


#参考資料