C#網(wǎng)絡(luò)編程系列六:UDP編程
引用:前一個(gè)專題簡(jiǎn)單介紹了TCP編程的一些知識(shí),UDP與TCP地位相當(dāng)?shù)牧硪粋€(gè)傳輸層協(xié)議,它也是當(dāng)下流行的很多主流網(wǎng)絡(luò)應(yīng)用(例如QQ、MSN和Skype等一些即時(shí)通信軟件傳輸層都是應(yīng)用UDP協(xié)議的)底層的傳輸基礎(chǔ),所以在本專題中就簡(jiǎn)單介紹下UDP的工作原理和UDP編程的只是,希望可以對(duì)剛接觸網(wǎng)絡(luò)編程的朋友起到入門的作用。
一、UDP介紹
UDP和TCP都是構(gòu)建在IP層之上傳輸層的協(xié)議,但UDP是一種簡(jiǎn)單、面向數(shù)據(jù)報(bào)(Sock_Dgram)的無(wú)連接協(xié)議,提供的是不一定可靠的傳輸服務(wù)。
然而TCP是一種面向連接、可靠的,面向字節(jié)流(Sock_Stream)的傳輸協(xié)議,對(duì)于“無(wú)連接”是指在正式通信前不必與對(duì)方先建立連接,不管對(duì)方狀態(tài)如何都可以直接發(fā)送過(guò)去(就如QQ中通過(guò)QQ號(hào)查看好友后發(fā)送添加好友請(qǐng)求,此間不需要考慮對(duì)方的狀態(tài)如何,都照樣發(fā)送請(qǐng)求)。從UDP和TCP的定義中就可以看出它們兩者的區(qū)別了,(1)UDP的可靠性不如TCP,因?yàn)門CP傳輸前要首先建立連接,這樣就增加了TCP傳輸?shù)目煽啃?,所以UDP也被稱為不可靠的傳輸協(xié)議,關(guān)于TCP的介紹可以看我上一篇博客的介紹。
TCP和UDP還有另外一個(gè)區(qū)別。(2)UDP不能保證有序傳輸。即UDP不能確保數(shù)據(jù)的發(fā)送和接收順序。
下面就來(lái)看看UDP協(xié)議的工作原理,對(duì)UDP的工作原理有一個(gè)好的理解,對(duì)后面介紹的UDP編程也是一個(gè)好的基礎(chǔ)。
1.1 UDP的工作原理
UDP將網(wǎng)絡(luò)數(shù)據(jù)流量壓縮成數(shù)據(jù)報(bào)的形式,每一個(gè)數(shù)據(jù)報(bào)用8個(gè)字節(jié)(8 X 8位=64位)描述報(bào)頭信息,剩余字節(jié)包含具體的傳輸數(shù)據(jù)。UDP報(bào)頭(只有8個(gè)字節(jié))相當(dāng)于TCP的報(bào)頭(至少20個(gè)字節(jié))很短,UDP報(bào)頭由4個(gè)域組成,每個(gè)域各占2個(gè)字節(jié),具體為源端口、目的端口、用戶數(shù)據(jù)報(bào)長(zhǎng)度和校驗(yàn)和,
具體結(jié)構(gòu)見(jiàn)下圖(下面也貼出了TCP報(bào)文的結(jié)構(gòu)圖,與UDP數(shù)據(jù)報(bào)做一個(gè)對(duì)比的作用):
UDP協(xié)議和TCP協(xié)議都使用端口號(hào)為不同的應(yīng)用保留其各自的數(shù)據(jù)傳輸通道這一機(jī)制,數(shù)據(jù)發(fā)送方將UDP數(shù)據(jù)報(bào)通過(guò)源端口發(fā)送出去,而數(shù)據(jù)接收方則通過(guò)目標(biāo)端口接收數(shù)據(jù)。
1.2 UDP的優(yōu)勢(shì)
前面介紹中說(shuō)UDP相對(duì)于TCP是不可靠的,不能保證有序傳輸?shù)膫鬏攨f(xié)議,然而UDP協(xié)議相對(duì)于TCP協(xié)議的優(yōu)勢(shì)在哪里呢?,
UDP相對(duì)于TCP的優(yōu)勢(shì)主要有三個(gè)方面的:
(1)UDP速度比TCP快。
由于UDP不需要先與對(duì)方建立連接,也不需要傳輸確認(rèn),因此其數(shù)據(jù)的傳輸速度比TCP快很多。對(duì)于一些著重傳輸性能而不是傳輸完整性的應(yīng)用(網(wǎng)絡(luò)音頻播放、視頻點(diǎn)播和網(wǎng)絡(luò)會(huì)議等),使用UDP協(xié)議更加適合,因?yàn)樗鼈鬏斔俣瓤?,使通過(guò)網(wǎng)絡(luò)播放的視頻音質(zhì)好、畫面清晰。
(2)UDP有消息邊界。
通過(guò)UDP協(xié)議進(jìn)行傳輸?shù)陌l(fā)送方對(duì)應(yīng)用程序交下來(lái)的報(bào)文,在添加首部后就向下直接交付給IP層。既不拆分也不合并,而是保留這些報(bào)文的邊界,所以使用UDP協(xié)議不需要像TCP那樣考慮消息邊界的問(wèn)題,這樣就使得UDP編程相對(duì)于TCP在接收到的數(shù)據(jù)處理方面要簡(jiǎn)單的多。(對(duì)于TCP消息邊界的問(wèn)題可以查看相關(guān)的文檔,在這里我就不列出來(lái)了)
(3)UDP可以一對(duì)多傳輸
由于傳輸數(shù)據(jù)部建立連接,也就不需要維護(hù)連接狀態(tài),因此一臺(tái)服務(wù)器可以同時(shí)向多個(gè)客戶端發(fā)送相同的信息。利用UDP可以使用廣播或者組播的方式同時(shí)向子網(wǎng)的所有客戶端進(jìn)程發(fā)送信息,廣播和組播的介紹放到后面TCP編程中介紹。
上面介紹了UDP協(xié)議相對(duì)于TCP協(xié)議的優(yōu)勢(shì),其中速度快是UDP的最重要的優(yōu)勢(shì),也是像一些網(wǎng)絡(luò)會(huì)議、即時(shí)通信軟件傳輸層選擇UDP協(xié)議進(jìn)行傳輸?shù)脑蛩凇?/p>
二、.net平臺(tái)對(duì)UDP編程的支持
介紹完UDP相對(duì)于TCP的優(yōu)勢(shì)后,當(dāng)然很希望在.net平臺(tái)下開(kāi)發(fā)一個(gè)基于UDP協(xié)議的一個(gè)應(yīng)用了,然后.net平臺(tái)下對(duì)UDP編程也做了很好的支持,為我們開(kāi)發(fā)基于UDP協(xié)議的網(wǎng)絡(luò)應(yīng)用提供很多方便之處,下面就簡(jiǎn)單介紹.net平臺(tái)下對(duì)UDP編程的支持(主要介紹提供的類來(lái)對(duì)UDP協(xié)議進(jìn)行編程)。
.net類庫(kù)中的UdpClient類對(duì)基礎(chǔ)的Socket進(jìn)行了封裝,這樣就在發(fā)送和接受數(shù)據(jù)時(shí)不需要考慮底層套接字的收發(fā)時(shí)處理的一些細(xì)節(jié)問(wèn)題,這樣為UDP編程提供了方便,也可以提高開(kāi)發(fā)效率(感覺(jué)net就是做這樣的事情的,對(duì)一些底層的實(shí)現(xiàn)進(jìn)行封裝,方便我們的調(diào)用,這也體現(xiàn)了面向?qū)ο笳Z(yǔ)言的封裝特性)對(duì)于這個(gè)的具體的使用我就不做過(guò)多的介紹的,在后面的UDP編程的實(shí)現(xiàn)部分將會(huì)對(duì)該類中主要方法的使用,大家可以查看MSDN來(lái)查看該類中其他成員的使用: http://msdn.microsoft.com/zh-cn/library/System.Net.Sockets.UdpClient.aspx
三、UDP編程的具體實(shí)現(xiàn)
由于UDP進(jìn)程在通信之前是不需要建立連接,消息接收方可能并不知道是誰(shuí)給它發(fā)的消息,因此UDP編程分為兩種模式:一種“實(shí)名發(fā)送”,即接收方可以由收到的消息得知發(fā)送方進(jìn)程端口,另外一種則為“匿名發(fā)送”,即接收方并不知道發(fā)給它信息的遠(yuǎn)程進(jìn)程究竟來(lái)自哪個(gè)端口。下面通過(guò)一個(gè)winform 程序來(lái)演示下UDP的編程:
實(shí)現(xiàn)代碼:
- 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)
- {
- // 創(chuàng)建接收套接字
- 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
- {
- // 關(guān)閉receiveUdpClient時(shí)此時(shí)會(huì)產(chǎn)生異常
- byte[] receiveBytes = receiveUpdClient.Receive(ref remoteIpEndPoint);
- string message = Encoding.Unicode.GetString(receiveBytes);
- // 顯示消息內(nèi)容
- ShowMessageforView(lstbxMessageView, string.Format("{0}[{1}]", remoteIpEndPoint, message));
- }
- catch
- {
- break;
- }
- }
- }
- // 利用委托回調(diào)機(jī)制實(shí)現(xiàn)界面上消息內(nèi)容顯示
- 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("發(fā)送內(nèi)容不能為空","提示");
- return;
- }
- // 選擇發(fā)送模式
- if (chkbxAnonymous.Checked == true)
- {
- // 匿名模式(套接字綁定的端口由系統(tǒng)隨機(jī)分配)
- sendUdpClient = new UdpClient(0);
- }
- else
- {
- // 實(shí)名模式(套接字綁定到本地指定的端口)
- 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);
- }
- // 發(fā)送消息方法
- 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();
- // 清空發(fā)送消息框
- ResetMessageText(tbxMessageSend);
- }
- // 采用了回調(diào)機(jī)制
- // 使用委托實(shí)現(xiàn)跨線程界面的操作方式
- delegate void ResetMessageCallback(TextBox textbox);
- private void ResetMessageText(TextBox textbox)
- {
- // Control.InvokeRequired屬性代表
- // 如果控件的處理與調(diào)用線程在不同線程上創(chuàng)建的,則為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();
- }
- }
- }
運(yùn)行結(jié)果:
實(shí)名發(fā)送:
在本地運(yùn)行本程序的三個(gè)進(jìn)程(分別為A,B,C),把進(jìn)程C做為接受進(jìn)程,進(jìn)程A和進(jìn)程B都向進(jìn)程C發(fā)信息,進(jìn)程A和進(jìn)程分別綁定端口號(hào)為11883和21883,發(fā)送到端口都為51883,配置界面如下:
首先不勾選“匿名”復(fù)選框,在進(jìn)程C中點(diǎn)擊“接收”按鈕開(kāi)啟接受線程,在A進(jìn)程和B進(jìn)程中發(fā)送消息框里分別輸入你好,我是1和你好,我是2 ,然后點(diǎn)擊發(fā)送按鈕,此時(shí)在進(jìn)程中就可以看到進(jìn)程A和進(jìn)程B發(fā)來(lái)的消息,如下圖:
從圖中可以看出每條消息之前都顯示了消息的準(zhǔn)確來(lái)源(包括消息進(jìn)程鎖在的Ip地址和端口號(hào))
匿名發(fā)送:
下面把“匿名”復(fù)選框勾上后,再按照前面的步驟將得到下面的結(jié)果:
從圖中結(jié)果可以看出此時(shí)列表中顯示的消息來(lái)源的進(jìn)程端口號(hào)分別為49439和49440,而不是發(fā)送消息進(jìn)程的真實(shí)端口(11883和21883)
這種UDP只能辨別消息源主機(jī)的Ip地址,而無(wú)法知道發(fā)消息的進(jìn)程究竟是哪個(gè)端口稱為“匿名發(fā)送”。正如我們平時(shí)發(fā)手機(jī)短信一樣,如果我們把認(rèn)識(shí)的名字和電話號(hào)碼預(yù)先存在通訊錄里,當(dāng)一發(fā)來(lái)信息,接受方馬上就可以從來(lái)電顯示中看到是誰(shuí)發(fā)來(lái)的(實(shí)名模式);但是如果是陌生人發(fā)來(lái)信息或者廣告等信息時(shí),僅看來(lái)電顯示,根本不知道對(duì)方是誰(shuí)(匿名模式),QQ發(fā)消息也是一樣的道理。
四、UDP廣播和組播
前面UDP的實(shí)現(xiàn)中發(fā)送數(shù)據(jù)使用的都是一對(duì)一(單播)的通信方式,即只將數(shù)據(jù)發(fā)送到某一個(gè)進(jìn)程。前面提到UDP可以實(shí)現(xiàn)一對(duì)多的傳輸方式,即通過(guò)廣播和組播把數(shù)據(jù)發(fā)送給一組進(jìn)程。下面就介紹下UDP廣播和組播的相關(guān)知識(shí)。
4.1 廣播和組播的基本概念
雖然利用TCP協(xié)議可以保證數(shù)據(jù)的可靠、有序的傳輸,但是TCP僅支持一對(duì)以的傳輸,而且傳輸時(shí)需要在發(fā)送端和每一個(gè)接受端之間建立單獨(dú)的數(shù)據(jù)通信通道,如果需要實(shí)現(xiàn)網(wǎng)絡(luò)會(huì)議、網(wǎng)絡(luò)視頻的點(diǎn)播等功能時(shí)要向大量主機(jī)發(fā)送相同的數(shù)據(jù)包,如果采用單播方式逐個(gè)節(jié)點(diǎn)傳輸?shù)脑?,將?huì)給發(fā)送方帶來(lái)網(wǎng)絡(luò)堵塞等問(wèn)題,此時(shí)可以考慮實(shí)現(xiàn)UDP的多播方式——即廣播和組播來(lái)實(shí)現(xiàn)這樣的功能(一對(duì)多通信分為廣播和組播兩種形式)。
廣播是指同時(shí)向子網(wǎng)中的多臺(tái)計(jì)算機(jī)發(fā)送消息,并且所有子網(wǎng)中的計(jì)算機(jī)都可以接收到發(fā)送方發(fā)來(lái)的消息,每個(gè)廣播消息包含一個(gè)特殊的IP地址,這個(gè)IP的中子網(wǎng)內(nèi)主機(jī)標(biāo)志部分的二進(jìn)制都為1,例如,子網(wǎng)掩碼為255.255.255.0,對(duì)于子網(wǎng)192.168.0,則這個(gè)IP地址為192.168.0.255.
然后廣播消息又分為本地廣播和全球廣播兩種類型, 本地廣播是指向子網(wǎng)中的所有計(jì)算機(jī)發(fā)送廣播消息,其他網(wǎng)絡(luò)不會(huì)受到本地廣播的影響。
IP地址分為兩部分——網(wǎng)絡(luò)標(biāo)志部分和主機(jī)標(biāo)志部分,這兩部分是靠子網(wǎng)掩碼來(lái)區(qū)分的,主機(jī)標(biāo)記部分二進(jìn)制全部為1的地址成為本地廣播地址。例如:
A類網(wǎng)絡(luò)192.168.0.0,使用子網(wǎng)掩碼255.255.0.0,則本地廣播地址為:
對(duì)于IPv4來(lái)說(shuō),全球廣播使用所有位全為1的IP地址,即255.255.255.255,這個(gè)廣播地址代表數(shù)據(jù)報(bào)的目的地是網(wǎng)絡(luò)上所有設(shè)備,但是由于路由器會(huì)自動(dòng)過(guò)濾全球廣播,所以使用這個(gè)地址根本就沒(méi)有任何意義。
然后當(dāng)接收者分布于多個(gè)不同的子網(wǎng)時(shí),廣播將不再適用,此時(shí)可以通過(guò)組播的方式來(lái)實(shí)現(xiàn),組播也叫多路廣播,組播是將信息從一臺(tái)計(jì)算機(jī)發(fā)送到本網(wǎng)或全網(wǎng)內(nèi)指定的計(jì)算機(jī)上,即發(fā)送到那些加入了指定組播組的計(jì)算機(jī)上,每臺(tái)計(jì)算機(jī)都可以通過(guò)程序隨時(shí)加入某個(gè)組播組中,也可以隨時(shí)退出來(lái), 就像我們開(kāi)網(wǎng)了會(huì)議一樣,可以隨時(shí)加入會(huì)議室進(jìn)行開(kāi)會(huì),會(huì)議結(jié)束和會(huì)議進(jìn)行中都可以隨意的退出來(lái)。
4.2 加入和退出組播組
組播組又稱為多路廣播組,組播地址的范圍在224.0.0.0到239.255.255.255的D類IP地址(至于這個(gè)概念大家可以百度百科里面就查看)。任何發(fā)送到組播地址的消息都會(huì)被發(fā)送到組內(nèi)所有成員設(shè)備上,組可以使永久的也可以是臨時(shí),大多數(shù)我們使用的都是臨時(shí)的,僅在有成員的時(shí)候才存在。
使用組播時(shí),注意生命周期(TTL,Time to live)的設(shè),TTL值表示允許路由器轉(zhuǎn)發(fā)的最大次數(shù),當(dāng)達(dá)到這個(gè)最大值時(shí),數(shù)據(jù)包就會(huì)被丟棄,TTL的默認(rèn)值為1,設(shè)置為1時(shí)表明只能在子網(wǎng)中發(fā)送數(shù)據(jù)
加入組播組:UdpClient類提供了JoinMulticastGroup方法,用于將UdpClient加入到使用指定的IPAddress的組播組中,調(diào)用該方法后,基礎(chǔ)的Socket會(huì)自動(dòng)向路由器發(fā)送數(shù)據(jù)包,用于請(qǐng)求成為組播組的成員,如果成為組播組成員,就可以接收該組播組的數(shù)據(jù)報(bào)。至于具體方法的時(shí)候會(huì)在后面實(shí)現(xiàn)UDP廣播程序中會(huì)用到,另外大家也可以查看MSDN,所以這里我就不再列出來(lái)了,只是指出這個(gè)方法的作用,讓大家知道有這么個(gè)方法來(lái)調(diào)用。
退出組播組:同樣利用UdpClient的DropMulticastGroup方法,可以退出組播組,調(diào)用該方法后,基礎(chǔ)Socket會(huì)自動(dòng)向路由器發(fā)送數(shù)據(jù)包,用于請(qǐng)求從指定的組播組里退出,從組中回收UdpClient對(duì)象之后,將不再接受發(fā)送到該組播組的數(shù)據(jù)報(bào)。
五、總結(jié)
由于時(shí)間的關(guān)系,這篇文章就介紹到這里的,至于實(shí)現(xiàn)UDP廣播的程序放在后面一個(gè)專題里面的,前面也對(duì)廣播和組播的概念進(jìn)行了簡(jiǎn)單的介紹,相信大家也對(duì)廣播和組播有了個(gè)簡(jiǎn)單的認(rèn)識(shí)(廣播組和組播組說(shuō)白了就是一個(gè)IP地址的集合,其實(shí)實(shí)現(xiàn)UDP廣播的程序和前面實(shí)現(xiàn)單播的程序差不多,只是前面綁定了一個(gè)IP地址當(dāng)然也只能發(fā)送到一個(gè)IP地址了,也就是所謂的單播,多播和廣播就是發(fā)送的IP地址是一個(gè)組,當(dāng)然也就實(shí)現(xiàn)了一對(duì)多的傳輸了)。UDP廣播程序的實(shí)現(xiàn)就放在下一個(gè)專題和大家分享的,因?yàn)槲椰F(xiàn)在要去吃飯了,吃完飯?jiān)倮^續(xù)和大家介紹,希望大家如果覺(jué)得有幫助的話,也可以推薦下,這給我繼續(xù)寫下去的動(dòng)力,謝謝大家的支持。
原文鏈接:http://www.cnblogs.com/zhili/archive/2012/09/01/UDP_Multicast.html
【編輯推薦】
- C#網(wǎng)絡(luò)編程系列一:網(wǎng)絡(luò)協(xié)議簡(jiǎn)介
- C#網(wǎng)絡(luò)編程系列二:HTTP協(xié)議詳解
- C#網(wǎng)絡(luò)編程系列三:自定義Web服務(wù)器
- C#網(wǎng)絡(luò)編程系列四:自定義Web瀏覽器
- C#網(wǎng)絡(luò)編程系列五:TCP編程
- C#網(wǎng)絡(luò)編程系列七:UDP編程補(bǔ)充
- C#網(wǎng)絡(luò)編程系列八:P2P編程
- C#網(wǎng)絡(luò)編程系列九:類似QQ的即時(shí)通信程序
- C#網(wǎng)絡(luò)編程系列十:實(shí)現(xiàn)簡(jiǎn)單的郵件收發(fā)器