無線通信エンジニアの備忘録

無線通信だったり、ITだったり、仕事で覚えた専門知識の備忘録

【C#】UDPのソケット通信を同期/非同期で行う

約1年振りの投稿となってしまいました(;^_^A


仕事で時々ソケット通信を行う試験用のプログラムを作ることがあるのですが、これまでずっと同期方式でプログラムを作成してきました。


なぜかって?


非同期方式が難しくてよく理解できなかったからです・・・(´;ω;`)ウッ…


今回、非同期方式をあれこれ試行錯誤しながら勉強し、単純な仕組みのプログラムであれば扱えるようになったので、備忘録として残そうと思います。


●この記事の目次

1.今回作成するプログラム

今回は、Visual Studioを使用し、図1-1に示すようなシンプルなUDP通信のチャットアプリを例に、同期受信/非同期受信それぞれのケースでプログラムを作成します。送信側は、非同期方式にするメリットを今のところ感じじられないので、同期方式にしています。


f:id:taekwongineer:20210326211025p:plain
図1-1 今回作成するチャットアプリ


2.同期受信方式

まず同期受信方式から説明します。

図1-1で示したGUIの各パーツと、プログラムのソース上の変数名の対応を図2-1に示します。


f:id:taekwongineer:20210326213347p:plain
図2-1 GUI部品と変数名の対応

そして、以下にVusialStudioで自動生成されるForm1.csに、今回の同期受信方式のチャットアプリの処理を追記したコードを示します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// 追加定義分
using System.Net;
using System.Net.Sockets;
using System.Threading;


namespace UdpSocketTest
{
    public partial class Form1 : Form
    {
        //////// 変数の定義 ////////
        // 送信処理系
        public UdpClient txClient;          // 送信用ソケット
        public byte[] txData;               // 送信データ格納用配列

        // 受信処理系
        public UdpClient rxClient;          // 受信用ソケット
        public IPEndPoint remoteEP;         // RxClientのReceiveメソッドの引き数
        public byte[] rxData;               // 受信データ格納用配列
        public Thread thread;               // 受信処理用メソッド実行用
        public Boolean flag = false;        // 受信ループ制御用フラグ
        public string strRx = "受信";       // ボタン表示用
        public string strStop = "停止";     // ボタン表示用


        // その他
        public delegate void SetTextDelegate(string text);
        
        public Form1()
        {
            InitializeComponent();

            // 送信用のソケットを初期化
            txClient = new UdpClient();

        }

        // 「送信」ボタンクリック時の処理
        private void Button_TxCont_Click(object sender, EventArgs e)
        {
            // テキストボックスから文字列を取得し、バイト配列に変換
            txData = Encoding.UTF8.GetBytes(textBox_TxMsg.Text);

            // データを送信
            txClient.Send(txData, txData.Length, textBox_TxAddr.Text, int.Parse(textBox_TxPort.Text));

            // テキストボックスに表示
            richTextBox.AppendText("[送信]" + textBox_TxMsg.Text + "\n");
        }

        //「受信/停止」ボタンクリック時の処理
        private void Button_RxCont_Click(object sender, EventArgs e)
        {
            if(button_RxCont.Text == strRx)
            {
                // 受信処理メソッドをマルチスレッドで実行
                flag = true;
                thread = new Thread(new ThreadStart(Receive));
                thread.Start();


                // ボタン表示を変更
                button_RxCont.Text = strStop;

            }else if(button_RxCont.Text == strStop)
            {
                // ソケットを切断
                rxClient.Close();

                // 受信ループフラグを無効化
                flag = false;              

                // ボタン表示を変更
                button_RxCont.Text = strRx;
            }
        }

        // 受信処理用メソッド
        public void Receive()
        {
            // 受信用ソケットを初期化
            rxClient = new UdpClient(int.Parse(textBox_RxPort.Text));

            // 受信データの送信元情報が格納されるremoteEPはnullにしておく。
            remoteEP = null;

            while (flag)
            {
                try
                {
                    // 受信用ソケットからデータを読込
                    rxData = rxClient.Receive(ref remoteEP);
                    Console.WriteLine(remoteEP.Address + "," + remoteEP.Port);
                    // 受信データをテキストボックスに表示
                    Invoke(new SetTextDelegate(SetText), "[受信]" + Encoding.UTF8.GetString(rxData) + "\n");

                }catch(SocketException se)
                {
                    // Receiveによるブロック中にcloseを実行すると、
                    // SocketExceptionがスローされる。
                    // 何もしない
                    
                }catch(ObjectDisposedException obe)
                {
                    // close実行後にReceiveを実行した場合にスローされる。
                    break;
                }
            }

        }

        // サブスレッドからのテキストボックスへの書込み処理用メソッド
        public void SetText(string text)
        {
            // テキストボックスに引数の文字列を追記
            richTextBox.AppendText(text);

            // カーソルを最後尾に移動
            richTextBox.Select(richTextBox.TextLength, 0);
            richTextBox.ScrollToCaret();
        }

        // 「×」ボタンクリック時の処理
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // ソケットを切断
            if(rxClient != null)
            {
                rxClient.Close();
            }
        }
    }
}

UDPデータの受信処理は、89行目からのReceive()メソッドの中で実施しています。
そして102行目の「rxData = rxClient.Receive(ref remoteEP);」を実行すると、92行目でバインドしたポート番号のUDPデータを受信するまで、プログラムの動作がブロックされた状態となります。

Receiveメソッドをメインスレッドで実行すると、ブロック時に何も操作ができなくなってしまうため、68~69行目に示すように、Threadクラスのオブジェクトを作成し、サブスレッドでReceive()メソッドを実行するようにします。

「rxData = rxClient.Receive(ref remoteEP);」によるデータ受信待ちのブロック状態を途中で解除するためには、78行目、139行目に示すように、UdpClientのオブジェクト(rxClient)に対してClose()メソッドを実行します。

すると、「rxData = rxClient.Receive(ref remoteEP);」はSocketExceptionの例外をスローするので、プログラムが異常終了しないように、try~catchで例外をキャッチできるようにしておきます。

また、タイミングによっては、Close()メソッドが実行されたrxClientに対してReceive()メソッドが実行され、ObjectDisposedExceptionの例外がスローされる可能性もあるので、SocketExeptionと併せてキャッチできるようにしておきます。

3.非同期受信方式

続いて非同期受信方式です。

GUIの各パーツの変数名は、同期方式と同じなので、ここではソースコードのみ記載します。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// 追加定義分
using System.Net;
using System.Net.Sockets;


namespace UdpSocketTest
{
    public partial class Form1 : Form
    {
        //////// 変数の定義 ////////
        // 送信処理系
        public UdpClient txClient;          // 送信用ソケット
        public byte[] txData;               // 送信データ格納用配列

        // 受信処理系
        public UdpClient rxClient;          // 受信用ソケット
        public IPEndPoint remoteEP;         // RxClientのReceiveメソッドの引き数
        public byte[] rxData;               // 受信データ格納用配列
        public string strRx = "受信";       // ボタン表示用
        public string strStop = "停止";     // ボタン表示用


        // その他
        public delegate void SetTextDelegate(string text);
        
        public Form1()
        {
            InitializeComponent();

            // 送信用ソケットを初期化
            txClient = new UdpClient();
        }

        // 「送信」ボタンクリック時の処理
        private void Button_TxCont_Click(object sender, EventArgs e)
        {
            // テキストボックスから文字列を取得し、バイト配列に変換
            txData = Encoding.UTF8.GetBytes(textBox_TxMsg.Text);

            // データを送信
            txClient.Send(txData, txData.Length, textBox_TxAddr.Text, int.Parse(textBox_TxPort.Text));

            // テキストボックスに表示
            richTextBox.AppendText("[送信]" + textBox_TxMsg.Text + "\n");
        }

        //「受信/停止」ボタンクリック時の処理
        private void Button_RxCont_Click(object sender, EventArgs e)
        {
            if(button_RxCont.Text == strRx)
            {

                // 受信用ソケットを初期化
                rxClient = new UdpClient(int.Parse(textBox_RxPort.Text));
                rxClient.BeginReceive(ReceiveCallback, null);

                // ボタン表示を変更
                button_RxCont.Text = strStop;

            }else if(button_RxCont.Text == strStop)
            {

                // ソケットを切断
                rxClient.Close();

                // ボタン表示を変更
                button_RxCont.Text = strRx;
            }
        }

        // 受信処理用メソッド
        void ReceiveCallback(IAsyncResult ar)
        {
            // 受信データの送信元情報が格納されるremoteEPはnullにしておく。
            remoteEP = null;

            try
            {
                // 受信したデータを読込
                rxData = rxClient.EndReceive(ar, ref remoteEP);

                // 受信データをテキストボックスに表示
                Invoke(new SetTextDelegate(SetText), "[受信]" + Encoding.UTF8.GetString(rxData) + "\n");

                // 再び受信開始
                rxClient.BeginReceive(ReceiveCallback, null);
            }
            catch (ObjectDisposedException ode)
            {
                // BeginReceiveの実施中にソケットをCloseするとスローされる例外
                // 何もしない
            }

        }

        // サブスレッドからのテキストボックスへの書込み処理用メソッド
        public void SetText(string text)
        {
            // テキストボックスに引数の文字列を追記
            richTextBox.AppendText(text);

            // カーソルを最後尾に移動
            richTextBox.Select(richTextBox.TextLength, 0);
            richTextBox.ScrollToCaret();
        }


        // 「×」ボタンクリック時の処理
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            // ソケットを切断
            if(rxClient != null)
            {
                rxClient.Close();
            }
        }
    }
}

同期方式との差分を簡単に説明します。

まず非同期方式では、UDPデータの受信を実行する際に、rxClientに対し、Receive()メソッドではなく、BeginReceive()というメソッドを使用します。(65行目)

BeginReceive()は実行しても以降の処理のブロックが発生しません。

代わりに非同期方式では、BeginReceive()実行後に、バインドしたポート番号のUDPデータを受信すると、BeginReceive()の第1引数に指定したコールバックメソッド(ReceiveCallback)が呼び出されます。(82行目)

第2引数は、任意の型のデータを指定することができ、ReceiveCallback()の引数(IAsyncResult ar)の中に渡されるようですが、特に今回は何も渡すものがないので、nullとしています。

コールバックメソッドの引数はIAsyncResult arとするのがお約束のようです。


ReceiveCallback()の中では、rxClientに対し、EndReceive()を実行することで、受信したUDPデータを読み込みます。

BeginReceive()とEndReceive()はセットで使うと覚えておくとよさそうです(;^_^A

その後、ReceiveCallback()の中で、再びrxClientに対して、BeginReceive()を実行することで、次のデータの受信を待受けます。(96行目)


BeginReceive()を実行後に、UDPデータの待受けを中断する場合は、同期方式と同様、rxClientに対し、Close()を実行します。

すると、非同期方式の場合は、ObjectDisposedExceptionの例外がスローされるので、try~catchでプログラムが異常終了しないように例外をキャッチするようにします。

4.まとめ

UDPのソケット通信におけるデータの受信方法について、同期方式と非同期方式の場合のコード例をそれぞれ作成しました。

非同期方式のコードは初めて書きましたが、同期方式と比べると、マルチスレッドにしなくて良いくらいで、大した差はないかなという印象です。
(今回の例に挙げたチャットアプリが単純なためかもしれませんが・・・)

とは言っても多少は非同期方式の方がコードがシンプルになるので、今後は非同期方式でコードを書けるようにしておきたいと思います(;^_^A

なお、今回はUDPのソケット通信を取り上げましたが、TCPのソケット通信でも考え方は全く同じようです。