首页 > 代码库 > [C# 网络编程系列]专题六:UDP编程
[C# 网络编程系列]专题六:UDP编程
绍了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的编程:
实现代码:
- 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();
- }
- }
- }
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编程