.NET Micro FrameworkでUDP Multicast通信を行う

ローカルネットワークで効率よくデータをデバイス間で通信しあう方法として、UDP SocketのMulticast通信があります。
ずっと昔、Windows Phoneとの連携ネタで書いたことがありますが、いろいろ不備があったので書き直して投稿です。

例えば、ローカルネットワークに接続されている家電機器などの稼働状況を、タブレットをアドホックにネットワークつないで情報を得たい場合や、ローカルネットワークにつながっている家電機器等が他にどんだけデバイスがつながっているか深く知らない状態でデータを共有してロードバランスする、などでUDP Multicastは活用できます。DPWSのDiscoveryでもこのプロトコルが使われています。このプロトコルは標準プロトコルなので、.NET Micro Framework以外のプラットフォームでも、このプロトコルに対応しているものであれば何でも通信可能です。

Multicast通信を簡単に図示するとこんな感じ。

.NET Micro FrameworkでMulticast通信をするには、System.Net.Sockets名前空間のSocketクラスを使います。双方向通信なので送信と受信があります。送信用と受信用で二つのSocketオブジェクトを使います。毎度同じコードを書くのは面倒くさいので、MulticastClientという名前でクラスライブラリ化した例を説明します。

    public class MulticastClient
    {
        private Socket mySocket;
        private IPAddress localAddr;
        private IPAddress multicastAddr;
        private int multicastPort;
        private IPEndPoint multicastEP;

        public MulticastClient(IPAddress localAddress )
        {
            localAddr = localAddress;
        
            mySocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        }

        public void JoinGroup(IPAddress groupAddress, int groupPort)
        {
            multicastAddr = groupAddress;
            multicastPort = groupPort;

            #region Setting for Send

            byte[] multicastAddrBytes = multicastAddr.GetAddressBytes();
            byte[] ipAddrBytes = IPAddress.Any.GetAddressBytes();
            byte[] multicastOpt = new byte[]
            {
                multicastAddrBytes[0],multicastAddrBytes[1],multicastAddrBytes[2],multicastAddrBytes[3],
                ipAddrBytes[0],ipAddrBytes[1],ipAddrBytes[2],ipAddrBytes[3]
            };
            mySocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, multicastOpt);
            multicastEP = new IPEndPoint(multicastAddr, multicastPort);

            #endregion

            var thread = new Thread(MCCommReceive);
            thread.Start();
        }

        public void SendMessage(string msg)
        {
            if (joinFlag)
            {
                byte[] msgBytes = System.Text.UTF8Encoding.UTF8.GetBytes(msg);
                int len = msgBytes.Length;
                if (len > RECEIVEBUFSIZE - 2)
                {
                    throw new ArgumentOutOfRangeException("Message length should be less than " + (RECEIVEBUFSIZE - 2) + " bytes");
                }
                byte[] dataBytes = new byte[2 + len];
                dataBytes[0] = (byte)(len >> 8);
                dataBytes[1] = (byte)(len & 0xff);
                msgBytes.CopyTo(dataBytes, 2);
                mySocket.SendTo(dataBytes, dataBytes.Length, SocketFlags.None, multicastEP);
            }
        }

        int RECEIVEBUFSIZE = 1024;

        bool joinFlag = false;
        void MCCommReceive()
        {
            var receiveSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            #region Setting for Receive

            receiveSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ExclusiveAddressUse, false);
            receiveSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
            IPEndPoint localEP = new IPEndPoint(IPAddress.Any, multicastPort);
            receiveSocket.Bind(localEP);
            byte[] multicastAddrBytes = multicastAddr.GetAddressBytes();
            byte[] ipAddrBytes = IPAddress.Any.GetAddressBytes();
            byte[] multicastOpt = new byte[]
            {
               multicastAddrBytes[0], multicastAddrBytes[1], multicastAddrBytes[2], multicastAddrBytes[3],    // WsDiscovery Multicast Address: 239.255.255.250
                 ipAddrBytes       [0], ipAddrBytes       [1], ipAddrBytes       [2], ipAddrBytes       [3]
            };
            receiveSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, multicastOpt);
            EndPoint senderEP = new IPEndPoint(multicastAddr, multicastPort);

            #endregion

            int len = 0;
            byte[] dataBytes = new byte[RECEIVEBUFSIZE];
            bool mcgJoining = false;
            lock (this)
            {
                joinFlag = true;
                mcgJoining = joinFlag;
            }
            while (mcgJoining)
            {
                try
                {
                    len = receiveSocket.Receive(dataBytes, dataBytes.Length, SocketFlags.None);
                    len = (int)(dataBytes[0] << 8) + dataBytes[1];
                    byte[] buf = new byte[len];
                    for (int i = 0; i < len; i++)
                    {
                        buf[i] = dataBytes[i + 2];
                    }
                    if (OnMulticastMessageReceived != null)
                    {
                        OnMulticastMessageReceived(buf, len, (IPEndPoint)senderEP);
                    }
                }
                catch (Exception ex)
                {
                    Debug.Print(ex.Message);
                }
                lock (this)
                {
                    mcgJoining = joinFlag;
                }
            }

            receiveSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.DropMembership, multicastOpt);
        }

        public event OnMulticastMessageReceivedDelegate OnMulticastMessageReceived;
        public delegate void OnMulticastMessageReceivedDelegate(byte[] msg, int len, IPEndPoint sender);
    }

このクラスには、コンストラクタ、Multicast Groupにジョインし、メッセージ受信を開始する、JoinGroupメソッド、JoinGroup後に、Multicast Groupにデータ送信する為のSendMessageメソッド、JoinGroupメソッドの中で別スレッドとして実行されてデータを受信し続けるMCCommReceiveメソッドがあります。

JoinGroupメソッドでは、送信用のSocketインスタンスを作成し、MulticastGroup送信用にSetSocketOptoinメソッドを使って設定します。更に、データ受信用のスレッドを起動しています。

SendMessageメソッドは、JoinGroupメソッドをコール後にMulticast Groupにメッセージを送信するメソッドです。

MCCommReceiveメソッドは、受信用のSocketインスタンスを作成し、MulticastGroup受信用にSetSocketOptoinメソッドを使って設定後、無限ループでMulticastGroupからのデータを受信し続け、OnMulticastMessageReceivedに登録されたイベントハンドラをコールします。

このクラスは以下のように使います。

var client = new MulticastClient(myLocalIpAddress);
client.OnMulticastMessageReceived += OnReceived;
client.JoinGroup(groupIPAddr, groupPort);
...
client.SendMessage("Hello");
...

Socketからのデータ受信は、Socketに対して送信されたデータの塊での受信です。データ長より短いバイト数でSocketに対してReceiveをコールすると、Exceptionが発生します。

Multicast Group通信は、Groupにジョインしているノード群が、独立にメッセージを送信しあうので、どれか特定のノードとハンドシェーク的に通信する場合には、データの送信元の特定や、データの交差などに注意が必要です。

ローカルネットワークにつながっている複数ノード間でアドホックにつながって通信しあう場合、UDP Multicast Group通信はとても便利です。.NET Micro Frameworkだけでなく、Windows 8、Windows Phoneや非Windowsのプラットフォームで利用できるのでいろいろ試してみてください。
client.JoinGroup(groupIPAddr, groupPort);