1. 程式人生 > >[C# 網絡編程系列]專題六:UDP編程

[C# 網絡編程系列]專題六:UDP編程

單播 using 功能 .get 掩碼 ati multi 內容 協議

紹了TCP編程的一些知識,UDP與TCP地位相當的另一個傳輸層協議,它也是當下流行的很多主流網絡應用(例如QQ、MSN和Skype等一些即時通信軟件傳輸層都是應用UDP協議的)底層的傳輸基礎,所以在本專題中就簡單介紹下UDP的工作原理和UDP編程的只是,希望可以對剛接觸網絡編程的朋友起到入門的作用。

一、UDP介紹

UDP和TCP都是構建在IP層之上傳輸層的協議,但UDP是一種簡單、面向數據報(Sock_Dgram)的無連接協議,提供的是不一定可靠的傳輸服務。

然而TCP是一種面向連接、可靠的,面向字節流(Sock_Stream)的傳輸協議,對於“無連接”是指在正式通信前不必與對方先建立連接,不管對方狀態如何都可以直接發送過去(就如QQ中通過QQ號查看好友後發送添加好友請求,此間不需要考慮對方的狀態如何,都照樣發送請求)。從UDP和TCP的定義中就可以看出它們兩者的區別了,(1)UDP的可靠性不如TCP,因為TCP傳輸前要首先建立連接,這樣就增加了TCP傳輸的可靠性,所以UDP也被稱為不可靠的傳輸協議,關於TCP的介紹可以看我上一篇博客的介紹。

TCP和UDP還有另外一個區別。(2)UDP不能保證有序傳輸。即UDP不能確保數據的發送和接收順序。

下面就來看看UDP協議的工作原理,對UDP的工作原理有一個好的理解,對後面介紹的UDP編程也是一個好的基礎。

1.1 UDP的工作原理

UDP將網絡數據流量壓縮成數據報的形式,每一個數據報用8個字節(8 X 8位=64位)描述報頭信息,剩余字節包含具體的傳輸數據。UDP報頭(只有8個字節)相當於TCP的報頭(至少20個字節)很短,UDP報頭由4個域組成,每個域各占2個字節,具體為源端口、目的端口、用戶數據報長度和校驗和,

具體結構見下圖(下面也貼出了TCP報文的結構圖,與UDP數據報做一個對比的作用):

技術分享技術分享

UDP協議和TCP協議都使用端口號為不同的應用保留其各自的數據傳輸通道這一機制,數據發送方將UDP數據報通過源端口發送出去,而數據接收方則通過目標端口接收數據。

1.2 UDP的優勢

前面介紹中說UDP相對於TCP是不可靠的,不能保證有序傳輸的傳輸協議,然而UDP協議相對於TCP協議的優勢在哪裏呢?,

UDP相對於TCP的優勢主要有三個方面的:

(1)UDP速度比TCP快。

由於UDP不需要先與對方建立連接,也不需要傳輸確認,因此其數據的傳輸速度比TCP快很多。對於一些著重傳輸性能而不是傳輸完整性的應用(網絡音頻播放、視頻點播和網絡會議等),使用UDP協議更加適合,因為它傳輸速度快,使通過網絡播放的視頻音質好、畫面清晰。

(2)UDP有消息邊界。

通過UDP協議進行傳輸的發送方對應用程序交下來的報文,在添加首部後就向下直接交付給IP層。既不拆分也不合並,而是保留這些報文的邊界,所以使用UDP協議不需要像TCP那樣考慮消息邊界的問題,這樣就使得UDP編程相對於TCP在接收到的數據處理方面要簡單的多。(對於TCP消息邊界的問題可以查看相關的文檔,在這裏我就不列出來了)

(3)UDP可以一對多傳輸

由於傳輸數據部建立連接,也就不需要維護連接狀態,因此一臺服務器可以同時向多個客戶端發送相同的信息。利用UDP可以使用廣播或者組播的方式同時向子網的所有客戶端進程發送信息,廣播和組播的介紹放到後面TCP編程中介紹。

上面介紹了UDP協議相對於TCP協議的優勢,其中速度快是UDP的最重要的優勢,也是像一些網絡會議、即時通信軟件傳輸層選擇UDP協議進行傳輸的原因所在。

二、.net平臺對UDP編程的支持

介紹完UDP相對於TCP的優勢後,當然很希望在.net平臺下開發一個基於UDP協議的一個應用了,然後.net平臺下對UDP編程也做了很好的支持,為我們開發基於UDP協議的網絡應用提供很多方便之處,下面就簡單介紹.net平臺下對UDP編程的支持(主要介紹提供的類來對UDP協議進行編程)。

.net類庫中的UdpClient類對基礎的Socket進行了封裝,這樣就在發送和接受數據時不需要考慮底層套接字的收發時處理的一些細節問題,這樣為UDP編程提供了方便,也可以提高開發效率(感覺net就是做這樣的事情的,對一些底層的實現進行封裝,方便我們的調用,這也體現了面向對象語言的封裝特性)對於這個的具體的使用我就不做過多的介紹的,在後面的UDP編程的實現部分將會對該類中主要方法的使用,大家可以查看MSDN來查看該類中其他成員的使用: http://msdn.microsoft.com/zh-cn/library/System.Net.Sockets.UdpClient.aspx

三、UDP編程的具體實現

由於UDP進程在通信之前是不需要建立連接,消息接收方可能並不知道是誰給它發的消息,因此UDP編程分為兩種模式:一種“實名發送”,即接收方可以由收到的消息得知發送方進程端口,另外一種則為“匿名發送”,即接收方並不知道發給它信息的遠程進程究竟來自哪個端口。下面通過一個winform 程序來演示下UDP的編程:

實現代碼:

[csharp] view plain copy print?
  1. using System;
  2. using System.Net;
  3. using System.Net.Sockets;
  4. using System.Text;
  5. using System.Threading;
  6. using System.Windows.Forms;
  7. namespace UDPClient
  8. {
  9. public partial class frmUdp : Form
  10. {
  11. private UdpClient sendUdpClient;
  12. private UdpClient receiveUpdClient;
  13. public frmUdp()
  14. {
  15. InitializeComponent();
  16. IPAddress[] ips = Dns.GetHostAddresses("");
  17. tbxlocalip.Text = ips[3].ToString();
  18. int port = 51883;
  19. tbxlocalPort.Text = port.ToString();
  20. tbxSendtoIp.Text = ips[3].ToString();
  21. tbxSendtoport.Text = port.ToString();
  22. }
  23. // 接受消息
  24. private void btnReceive_Click(object sender, EventArgs e)
  25. {
  26. // 創建接收套接字
  27. IPAddress localIp = IPAddress.Parse(tbxlocalip.Text);
  28. IPEndPoint localIpEndPoint = new IPEndPoint(localIp, int.Parse(tbxlocalPort.Text));
  29. receiveUpdClient = new UdpClient(localIpEndPoint);
  30. Thread receiveThread = new Thread(ReceiveMessage);
  31. receiveThread.Start();
  32. }
  33. // 接收消息方法
  34. private void ReceiveMessage()
  35. {
  36. IPEndPoint remoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0);
  37. while (true)
  38. {
  39. try
  40. {
  41. // 關閉receiveUdpClient時此時會產生異常
  42. byte[] receiveBytes = receiveUpdClient.Receive(ref remoteIpEndPoint);
  43. string message = Encoding.Unicode.GetString(receiveBytes);
  44. // 顯示消息內容
  45. ShowMessageforView(lstbxMessageView, string.Format("{0}[{1}]", remoteIpEndPoint, message));
  46. }
  47. catch
  48. {
  49. break;
  50. }
  51. }
  52. }
  53. // 利用委托回調機制實現界面上消息內容顯示
  54. delegate void ShowMessageforViewCallBack(ListBox listbox, string text);
  55. private void ShowMessageforView(ListBox listbox, string text)
  56. {
  57. if (listbox.InvokeRequired)
  58. {
  59. ShowMessageforViewCallBack showMessageforViewCallback = ShowMessageforView;
  60. listbox.Invoke(showMessageforViewCallback, new object[] { listbox, text });
  61. }
  62. else
  63. {
  64. lstbxMessageView.Items.Add(text);
  65. lstbxMessageView.SelectedIndex = lstbxMessageView.Items.Count - 1;
  66. lstbxMessageView.ClearSelected();
  67. }
  68. }
  69. private void btnSend_Click(object sender, EventArgs e)
  70. {
  71. if (tbxMessageSend.Text == string.Empty)
  72. {
  73. MessageBox.Show("發送內容不能為空","提示");
  74. return;
  75. }
  76. // 選擇發送模式
  77. if (chkbxAnonymous.Checked == true)
  78. {
  79. // 匿名模式(套接字綁定的端口由系統隨機分配)
  80. sendUdpClient = new UdpClient(0);
  81. }
  82. else
  83. {
  84. // 實名模式(套接字綁定到本地指定的端口)
  85. IPAddress localIp = IPAddress.Parse(tbxlocalip.Text);
  86. IPEndPoint localIpEndPoint = new IPEndPoint(localIp, int.Parse(tbxlocalPort.Text));
  87. sendUdpClient = new UdpClient(localIpEndPoint);
  88. }
  89. Thread sendThread = new Thread(SendMessage);
  90. sendThread.Start(tbxMessageSend.Text);
  91. }
  92. // 發送消息方法
  93. private void SendMessage(object obj)
  94. {
  95. string message = (string)obj;
  96. byte[] sendbytes = Encoding.Unicode.GetBytes(message);
  97. IPAddress remoteIp = IPAddress.Parse(tbxSendtoIp.Text);
  98. IPEndPoint remoteIpEndPoint = new IPEndPoint(remoteIp, int.Parse(tbxSendtoport.Text));
  99. sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIpEndPoint);
  100. sendUdpClient.Close();
  101. // 清空發送消息框
  102. ResetMessageText(tbxMessageSend);
  103. }
  104. // 采用了回調機制
  105. // 使用委托實現跨線程界面的操作方式
  106. delegate void ResetMessageCallback(TextBox textbox);
  107. private void ResetMessageText(TextBox textbox)
  108. {
  109. // Control.InvokeRequired屬性代表
  110. // 如果控件的處理與調用線程在不同線程上創建的,則為true,否則為false
  111. if (textbox.InvokeRequired)
  112. {
  113. ResetMessageCallback resetMessagecallback = ResetMessageText;
  114. textbox.Invoke(resetMessagecallback, new object[] { textbox });
  115. }
  116. else
  117. {
  118. textbox.Clear();
  119. textbox.Focus();
  120. }
  121. }
  122. // 停止接收
  123. private void btnStop_Click(object sender, EventArgs e)
  124. {
  125. receiveUpdClient.Close();
  126. }
  127. // 清空接受消息框
  128. private void btnClear_Click(object sender, EventArgs e)
  129. {
  130. this.lstbxMessageView.Items.Clear();
  131. }
  132. }
  133. }
技術分享
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace UDPClient
{
    public partial class frmUdp : Form
    {
        private UdpClient sendUdpClient;
        private UdpClient receiveUpdClient;
        public frmUdp()
        {
            InitializeComponent();
            IPAddress[] ips = Dns.GetHostAddresses("");
            tbxlocalip.Text = ips[3].ToString();
            int port = 51883;
            tbxlocalPort.Text = port.ToString();
            tbxSendtoIp.Text = ips[3].ToString();
            tbxSendtoport.Text = port.ToString();
        }

        // 接受消息
        private void btnReceive_Click(object sender, EventArgs e)
        {
            // 創建接收套接字
            IPAddress localIp = IPAddress.Parse(tbxlocalip.Text);
            IPEndPoint localIpEndPoint = new IPEndPoint(localIp, int.Parse(tbxlocalPort.Text));
            receiveUpdClient = new UdpClient(localIpEndPoint);


            Thread receiveThread = new Thread(ReceiveMessage);
            receiveThread.Start();
        }

        // 接收消息方法
        private void ReceiveMessage()
        {
            IPEndPoint remoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0);
            while (true)
            {
                try
                {
                    // 關閉receiveUdpClient時此時會產生異常
                    byte[] receiveBytes = receiveUpdClient.Receive(ref remoteIpEndPoint);

                    string message = Encoding.Unicode.GetString(receiveBytes);

                    // 顯示消息內容
                    ShowMessageforView(lstbxMessageView, string.Format("{0}[{1}]", remoteIpEndPoint, message));
                }
                catch
                {
                    break;
                }
            }
        }

        // 利用委托回調機制實現界面上消息內容顯示
        delegate void ShowMessageforViewCallBack(ListBox listbox, string text);
        private void ShowMessageforView(ListBox listbox, string text)
        {
            if (listbox.InvokeRequired)
            {
                ShowMessageforViewCallBack showMessageforViewCallback = ShowMessageforView;
                listbox.Invoke(showMessageforViewCallback, new object[] { listbox, text });
            }
            else
            {
                lstbxMessageView.Items.Add(text);
                lstbxMessageView.SelectedIndex = lstbxMessageView.Items.Count - 1;
                lstbxMessageView.ClearSelected();
            }
        }
        private void btnSend_Click(object sender, EventArgs e)
        {
            if (tbxMessageSend.Text == string.Empty)
            {
                MessageBox.Show("發送內容不能為空","提示");
                return;
            }

            // 選擇發送模式
            if (chkbxAnonymous.Checked == true)
            {
                // 匿名模式(套接字綁定的端口由系統隨機分配)
                sendUdpClient = new UdpClient(0);
            }
            else
            {
                // 實名模式(套接字綁定到本地指定的端口)
                IPAddress localIp = IPAddress.Parse(tbxlocalip.Text);
                IPEndPoint localIpEndPoint = new IPEndPoint(localIp, int.Parse(tbxlocalPort.Text));
                sendUdpClient = new UdpClient(localIpEndPoint);
            }

            Thread sendThread = new Thread(SendMessage);
            sendThread.Start(tbxMessageSend.Text);
        }

        // 發送消息方法
        private void SendMessage(object obj)
        {
            string message = (string)obj;
            byte[] sendbytes = Encoding.Unicode.GetBytes(message);
            IPAddress remoteIp = IPAddress.Parse(tbxSendtoIp.Text);
            IPEndPoint remoteIpEndPoint = new IPEndPoint(remoteIp, int.Parse(tbxSendtoport.Text));
            sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIpEndPoint);
          
            sendUdpClient.Close();
           
            // 清空發送消息框
            ResetMessageText(tbxMessageSend);
        }

        // 采用了回調機制
        // 使用委托實現跨線程界面的操作方式
        delegate void ResetMessageCallback(TextBox textbox);
        private void ResetMessageText(TextBox textbox)
        {
            // Control.InvokeRequired屬性代表
            // 如果控件的處理與調用線程在不同線程上創建的,則為true,否則為false
            if (textbox.InvokeRequired)
            {
                ResetMessageCallback resetMessagecallback = ResetMessageText;
                textbox.Invoke(resetMessagecallback, new object[] { textbox });
            }
            else
            {
                textbox.Clear();
                textbox.Focus();
            }
        }

        // 停止接收
        private void btnStop_Click(object sender, EventArgs e)
        {
            receiveUpdClient.Close();
        }

        // 清空接受消息框
        private void btnClear_Click(object sender, EventArgs e)
        {
            this.lstbxMessageView.Items.Clear();
        }
    }
}

運行結果:

實名發送:

在本地運行本程序的三個進程(分別為A,B,C),把進程C做為接受進程,進程A和進程B都向進程C發信息,進程A和進程分別綁定端口號為11883和21883,發送到端口都為51883,配置界面如下:

技術分享

首先不勾選“匿名”復選框,在進程C中點擊“接收”按鈕開啟接受線程,在A進程和B進程中發送消息框裏分別輸入你好,我是1和你好,我是2 ,然後點擊發送按鈕,此時在進程中就可以看到進程A和進程B發來的消息,如下圖:

技術分享

從圖中可以看出每條消息之前都顯示了消息的準確來源(包括消息進程鎖在的Ip地址和端口號)

匿名發送:

下面把“匿名”復選框勾上後,再按照前面的步驟將得到下面的結果:

技術分享

從圖中結果可以看出此時列表中顯示的消息來源的進程端口號分別為49439和49440,而不是發送消息進程的真實端口(11883和21883)

這種UDP只能辨別消息源主機的Ip地址,而無法知道發消息的進程究竟是哪個端口稱為“匿名發送”。正如我們平時發手機短信一樣,如果我們把認識的名字和電話號碼預先存在通訊錄裏,當一發來信息,接受方馬上就可以從來電顯示中看到是誰發來的(實名模式);但是如果是陌生人發來信息或者廣告等信息時,僅看來電顯示,根本不知道對方是誰(匿名模式),QQ發消息也是一樣的道理。

四、UDP廣播和組播

前面UDP的實現中發送數據使用的都是一對一(單播)的通信方式,即只將數據發送到某一個進程。前面提到UDP可以實現一對多的傳輸方式,即通過廣播和組播把數據發送給一組進程。下面就介紹下UDP廣播和組播的相關知識。

4.1 廣播和組播的基本概念

雖然利用TCP協議可以保證數據的可靠、有序的傳輸,但是TCP僅支持一對以的傳輸,而且傳輸時需要在發送端和每一個接受端之間建立單獨的數據通信通道,如果需要實現網絡會議、網絡視頻的點播等功能時要向大量主機發送相同的數據包,如果采用單播方式逐個節點傳輸的話,將會給發送方帶來網絡堵塞等問題,此時可以考慮實現UDP的多播方式——即廣播和組播來實現這樣的功能(一對多通信分為廣播和組播兩種形式)。

廣播是指同時向子網中的多臺計算機發送消息,並且所有子網中的計算機都可以接收到發送方發來的消息,每個廣播消息包含一個特殊的IP地址,這個IP的中子網內主機標誌部分的二進制都為1,例如,子網掩碼為255.255.255.0,對於子網192.168.0,則這個IP地址為192.168.0.255.

然後廣播消息又分為本地廣播和全球廣播兩種類型, 本地廣播是指向子網中的所有計算機發送廣播消息,其他網絡不會受到本地廣播的影響。

IP地址分為兩部分——網絡標誌部分和主機標誌部分,這兩部分是靠子網掩碼來區分的,主機標記部分二進制全部為1的地址成為本地廣播地址。例如:

A類網絡192.168.0.0,使用子網掩碼255.255.0.0,則本地廣播地址為:技術分享

對於IPv4來說,全球廣播使用所有位全為1的IP地址,即255.255.255.255,這個廣播地址代表數據報的目的地是網絡上所有設備,但是由於路由器會自動過濾全球廣播,所以使用這個地址根本就沒有任何意義。

然後當接收者分布於多個不同的子網時,廣播將不再適用,此時可以通過組播的方式來實現,組播也叫多路廣播,組播是將信息從一臺計算機發送到本網或全網內指定的計算機上,即發送到那些加入了指定組播組的計算機上,每臺計算機都可以通過程序隨時加入某個組播組中,也可以隨時退出來, 就像我們開網了會議一樣,可以隨時加入會議室進行開會,會議結束和會議進行中都可以隨意的退出來。

4.2 加入和退出組播組

組播組又稱為多路廣播組,組播地址的範圍在224.0.0.0到239.255.255.255的D類IP地址(至於這個概念大家可以百度百科裏面就查看)。任何發送到組播地址的消息都會被發送到組內所有成員設備上,組可以使永久的也可以是臨時,大多數我們使用的都是臨時的,僅在有成員的時候才存在。

使用組播時,註意生命周期(TTL,Time to live)的設,TTL值表示允許路由器轉發的最大次數,當達到這個最大值時,數據包就會被丟棄,TTL的默認值為1,設置為1時表明只能在子網中發送數據

加入組播組:

UdpClient類提供了JoinMulticastGroup方法,用於將UdpClient加入到使用指定的IPAddress的組播組中,調用該方法後,基礎的Socket會自動向路由器發送數據包,用於請求成為組播組的成員,如果成為組播組成員,就可以接收該組播組的數據報。至於具體方法的時候會在後面實現UDP廣播程序中會用到,另外大家也可以查看MSDN,所以這裏我就不再列出來了,只是指出這個方法的作用,讓大家知道有這麽個方法來調用。

退出組播組:

同樣利用UdpClient的DropMulticastGroup方法,可以退出組播組,調用該方法後,基礎Socket會自動向路由器發送數據包,用於請求從指定的組播組裏退出,從組中回收UdpClient對象之後,將不再接受發送到該組播組的數據報。

五、總結

由於時間的關系,這篇文章就介紹到這裏的,至於實現UDP廣播的程序放在後面一個專題裏面的,前面也對廣播和組播的概念進行了簡單的介紹,相信大家也對廣播和組播有了個簡單的認識(廣播組和組播組說白了就是一個IP地址的集合,其實實現UDP廣播的程序和前面實現單播的程序差不多,只是前面綁定了一個IP地址當然也只能發送到一個IP地址了,也就是所謂的單播,多播和廣播就是發送的IP地址是一個組,當然也就實現了一對多的傳輸了)。UDP廣播程序的實現就放在下一個專題和大家分享的。

本專題源碼: UDP編程

[C# 網絡編程系列]專題六:UDP編程