[.NET] 應用程式內共用 UdpClient連線

摘要:[.NET] 應用程式內共用 UdpClient連線

原始碼下載: MutualUdpClientSample.rar



在開發與遠端設備通訊的系統時,為了提高資料傳輸的效率,常常會選擇UDP這個通訊協定來作為資料傳輸的媒介。而 .NET framework中所提供的UdpClient物件,可以幫助開發人員依照系統需求開啟UDP通訊端點,快速建立UDP連線來提供與遠端設備通訊的功能。



這個系統架構下當增加一個不同種類的遠端設備時,必須要提供一個不同的UDP通訊端點,才能用來提供與不同種類遠端設備通訊的功能,在遠端設備種類越來越多時,系統所需要的UDP通訊端點就會依照遠端設備種類而增加。



在遠端設備種類越來越多的情景中,為了網路管理考量會限制系統與遠端設備通訊時,必須統一使用同一個UDP通訊端點來與遠端設備通訊,再由封包內容、或是IP位址去判斷實際連接的遠端設備為何。



class Program
{
    static void Main(string[] args)
    {
        // Receiver
        UdpClient udpClientA = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));

        UdpClient udpClientB = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));
    }
}

依照系統需求開發人員可能寫出上列的程式碼,直接建立兩個UdpClient物件來開啟同一個UDP通訊端點。這段程式碼內容可以通過編譯器的檢查,但在按下執行之後,就會在Visual Studio之中看到SocketException的例外通知,用來告知開發人員同一個通訊端點只能被開啟一次,使用兩個UdpClient來開啟同一個通訊端點是無法執行的。



有涉略過Design pattern的開發人員,在遇到資源物件只能有一個實體的情景,會想到套用Singleton Pattern來提供資源物件共享的功能。系統中UdpClient物件所開啟的UDP通訊端點,就是屬於這種只能由一個物件所開啟的資源,這個情景中在UdpClient物件上套用Singleton Pattern看起來會是個不錯的選擇。



class Program
{
    // Singleton
    private static UdpClient _udpClientInstance = null;

    private static UdpClient UdpClientInstance
    {
        get
        {
            if (_udpClientInstance == null)
            {
                _udpClientInstance = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));
            }
            return _udpClientInstance;
        }
    }


    // Main
    static void Main(string[] args)
    {
        // Receiver
        UdpClient udpClientA = Program.UdpClientInstance;

        UdpClient udpClientB = Program.UdpClientInstance;

        // Transmiter
        UdpClient transmiter = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));


        // Send
        transmiter.Send(new byte[] { 55 }, 1, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));


        // Receive
        byte[] packet = null;
        IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort);

        packet = udpClientA.Receive(ref remoteEndPoint);
        Console.WriteLine(string.Format("UdpClientA Receive:{0}", packet[0]));

        packet = udpClientB.Receive(ref remoteEndPoint);
        Console.WriteLine(string.Format("UdpClientB Receive:{0}", packet[0]));

        // End
        Console.ReadLine();

        // Close
        transmiter.Close();
        udpClientB.Close();
        udpClientA.Close();
    }
}

將Singleton Pattern套用在系統內所使用的UdpClient物件上,可以寫出上列的程式碼,系統內所使用的UdpClient物件都是取用到系統內一個靜態存放的共享UdpClient物件。這段程式碼內容可以通過編譯器的檢查,並且在執行時也不會出現SocketException的例外通知,因為套用Singleton Pattern讓系統內只會開啟UDP通訊端點一次。


但進階一點去思考UdpClient物件的封包接收功能,UdpClient物件中提供Receive方法來等待、接收遠端設備傳送的資料封包,收到資料封包之後再次執行Receive方法會繼續等待、接收下一個資料封包。也就是說一個遠端設備傳送的資料封包,UdpClient只能透過Receive方法取得一次,在系統內共享同UdpClient物件,沒有辦法共享Receive方法所取得的資料封包。


觀察上列範例的執行結果,可以發現在範例中由transmiter所傳送的資料封包,在被UdpClientA透過Receive方法接收之後,UdpClientB無法接收到這個遠端傳送的資料封包,這也就驗證範例中將Singleton Pattern套用在系統內所使用UdpClient上的方式,會發生了無法共享資料封包的問題。



為了提供系統使用同一個UDP通訊端點來與遠端設備通訊,再由封包內容、或是IP位址去判斷實際連接的遠端設備為何的功能。筆者設計一個名為MutualUdpClient的解決方案,用來在系統內共用UDP通訊連線並且共享遠端設備傳送的資料封包。原始碼下載: MutualUdpClientSample.rar



在MutualUdpClient這個解決方案中,套用先前部落格中所發表的Singleton Pool模式,套用這個模式讓系統能夠共用UdpClient連線,並且在有系統物件使用UdpClient連線時就開啟共用UDP通訊連線,而在所有系統物件都不需要使用UdpClient連線才真正去關閉這個共用的UDP通訊連線。關於Singleton Pool模式的詳細資料可以參考:[Architecture Pattern] Singleton Pool



套用Singleton Pool模式解決了共享UdpClient連線的功能,接著在MutualUdpClient這個解決方案中,為了共享遠端設備傳送的資料封包,在UdpClient與MutualUdpClient之間加入了一個RouteUdpClient物件。


RouteUdpClient物件是一個主動式的物件,在被建立之後會開啟一條獨立的執行緒,不斷的接收UdpClient所接收到的資料封包,並且將接收到資料封包透過事件的方式通知每個MutualUdpClient,經由這樣的流程就可以將遠端設備所傳送的資料封包,在每個MutualUdpClient之間共享。


而MutualUdpClient物件在收到RouteUdpClient所提供的資料封包時,會先將資料封包暫存在一個佇列裡,並且在MutualUdpClient物件的Receive方法被呼叫時,再從佇列取出資料封包並且回傳給呼叫端,用以將遠端設備傳送的資料封包提供給呼叫端做後續的處理。經由這樣的方式,每個系統中所建立的MutualUdpClient物件就可以透過Receive方法取得,每個遠端設備傳送的資料封包。


*這邊要特別一提的是,MutualUdpClient物件不選擇事件方式來提供資料封包而採用Receive方法來提供,是為了讓使用MutualUdpClient物件的開發人員,在使用物件的時候,能夠得到與使用UdpClient一樣的開發體驗,用以減少開發時的學習時間。



處理完共享UdpClient連線、共享遠端設備傳送的資料封包之後,還要處理一下傳送資料封包到遠端設備的功能。在MutualUdpClient之中,對於傳送資料封包到遠端設備並沒有特殊需求,所以直接使用UdpClient的Send功能就可以完成將資料封包傳送到遠端設備的功能。



class Program
{
    static void Main(string[] args)
    {
        // Receiver
        MutualUdpClient udpClientA = new MutualUdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));

        MutualUdpClient udpClientB = new MutualUdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));

        // Transmiter
        UdpClient transmiter = new UdpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999));


        // Send
        transmiter.Send(new byte[] { 55 }, 1, new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234));


        // Receive
        byte[] packet = null;
        IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, IPEndPoint.MinPort);

        packet = udpClientA.Receive(ref remoteEndPoint);
        Console.WriteLine(string.Format("UdpClientA Receive:{0}", packet[0]));

        packet = udpClientB.Receive(ref remoteEndPoint);
        Console.WriteLine(string.Format("UdpClientB Receive:{0}", packet[0]));

        // End
        Console.ReadLine();

        // Close
        transmiter.Close();
        udpClientB.Close();
        udpClientA.Close();
    }
}

上列程式碼示範如何在系統中使用MutualUdpClient物件,在範例中可以看到程式碼中直接建立了兩個相同UDP端點的MutualUdpClient物件,並且可以正常的執行不會出現SocketException的例外通知。而遠端設備transmiter所傳送的資料封包,在被UdpClientA透過Receive方法接收之後,UdpClientB依然可以透過Receive方法接收同一個資料,這也就驗證了MutualUdpClient物件提供了共享通訊連線、共享資料封包的功能。



原始碼下載: MutualUdpClientSample.rar

期許自己
能以更簡潔的文字與程式碼,傳達出程式設計背後的精神。
真正做到「以形寫神」的境界。