6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2020-05-11

1.はじめに

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

最終的に、下記リンク先のツール「UECS通信実用規約に基づいたロギング用ソフトウェア」のElixir版を作るのを目標にしています。
ユビキタス環境制御システム(UECS)技術
ユビキタス環境制御システム「UECS」
(20/5/25)完成版:UecsListner

2.文字を受信して表示

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

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

ソースファイル

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

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

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

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を起動して待機状態にします。

サーバ側:コマンドライン
$ 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を実行します。

クライアント側:コマンドライン
$ 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」では、下記のXML形式のデータをUDPで送信しています。

(例)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の処理を実装したモジュールに追記します。

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」のライブラリを追記します。

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

サーバ側実行

サーバ側:コマンドライン
# ライブラリの依存関係の処理
$ 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ファイルを送信します。

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

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

次回はデータベースへのINSERTとSELECTを試してみます。

Appendix.UECSのデータ形式

UECSの通信規約として定義されています。

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に固定

参考資料

  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

  3. https://qiita.com/chenglin/items/70f06e146db19de5a659

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?