全面掌握VisualC#实现UDP协议(一)

一.走进UDP协议

UDP(User Datagram Protocol)协议就是“用户数据报协议”,它是一种无连接的协议,无连接主要是和TCP协议相比较的。我们知道当利用TCP协议传送数据的时候,首先必须先建立连接(也就是所谓的握手)才可以传输数据。而当计算机利用UDP协议进行数据传输的时候,发送方只需要知道对方的IP地址和端口号就可以发送数据,而并不需要进行连接。当然如果你非要进行连接,通过Visual C#也是可以实现的,但前提是要确定连接的远程主机的端口号处于监听状态,否则程序会出现不必要的错误,但这是种画蛇添足的做法,不仅丢失了UDP协议的无连接传送数据的特点和优点,而且还给程序运行带来了不安定的因素。所以这种方法并不值得提倡。

由于UDP协议并不需要进行确定的连接,所以编写基于UDP协议的应用程序比起编写基于TCP协议的应用程序要简单些(程序中可以不需要考虑连接和一些异常的捕获工作)。但同时也给基于UDP协议编写的程序带来了一个致命的缺点,UDP由于不提供可靠数据的传输,当计算机之间利用UDP协议传送数据的时候,发送方只管发送数据,而并不确认数据是否被对方接收。这样就会导致某些UDP协议数据包在传送的过程中丢失,尤其网络质量不令人满意的情况下,丢失数据包的现象会更严重。这就是为什么在网络上传输重要数据不采用UDP协议的原因。

但是我们也不能因为这一个缺点就全面否定UDP协议,这是因为虽然利用UDP协议来传送安全性要求高的数据是不适合的,但对于那些不重要的数据,或者即使丢失若干数据包也不影响整体性的数据,如音频数据,视频数据等,采用UDP协议就是一个非常不错的选择。如目前网络流行的很多即时聊天程序,如OICQ和ICQ等,采用的就是UDP协议。同时虽然UDP协议无法保证数据可靠性,但具有对网络资源开销较小,数据处理速度快的优点,所以在有些对数据安全性要求不是很高的情况下,采用UDP协议也是一个非常不错的选择。

总结一下上面的内容,可见UDP是一种不面向连接的网络协议,既有其优点,也有其不足,具体如下:

  1. 基于UDP协议的网络应用程序,实现起来比较简单,并且基于UDP协议的网络应用程序在运行时,由于受到环境影响较小,所以不容易出错。

  2. UDP协议占用网络资源较少,数据处理较快,所以在网络中传送对安全性要求不是十分高数据时,其优点比较明显。所谓对安全性要求不高的数据,是指那些不重要的数据,或者是即使丢失若干数据,也不影响其整体的数据,如音频数据等。目前很多流行的网络应用程序都是基于UDP协议的,如OICQ、ICQ等。

  3. 由于其不是面向连接的网络协议,其缺点也是非常明显的,有些时候甚至是致命的。因为使用UDP协议来传送数据,在数据发送后,在发送方并不确认对方是否接收到。这样就可能导致传送的数据在网络中丢失,尤其在网络条件并不很好的情况下,丢失数据包的现象就更多。所以传送重要数据一般不采用UDP协议。

二.简介Visual C#发送、接收UDP数据包使用的主要类及其用法

用Visual C#实现UDP协议,最为常用,也是最为关键的类就是UdpClient,UdpClient位于命名空间System.Net.Sockets中,Visual C#发送、接收UDP数据包都是通过UdpClient类的。表01和表02是UdpClient类中常用方法和属性及其简要说明。

方法说明
Close关闭 UDP 连接
Connect建立与远程主机的连接
DropMulticastGroup退出多路广播组
JoinMulticastGroup将 UdpClient 添加到多路广播组
Receive返回已由远程主机发送的 UDP 数据文报
Send将 UDP 数据文报发送到远程主机

表01:UdpClient类中常用方法及其说明。

属性说明
Active获取或设置一个值,该值指示是否已建立了与远程主机的连接
Client获取或设置基础网络套接字

表02:UdpClient类中常用方法及其说明。

1.Visual C#使用UdpClient类发送UDP数据包:

在具体使用中,一般分成二种情况:

(1). 知道远程计算机IP地址:

"Send"方法的调用语法如下:

public int Send(
    byte[] dgram ,
    int bytes ,
    IPEndPoint endPoint
);

参数说明:

dgram 要发送的 UDP 数据文报(以字节数组表示)。
bytes 数据文报中的字节数。
endPoint 一个 IPEndPoint,它表示要将数据文报发送到的主机和端口。
返回值 已发送的字节数。

下面使用UdpClient发送UDP数据包的具体的调用例子:

IPAddress HostIP = new IPAddress.Parse("远程计算机IP地址");
IPEndPoint host = new IPEndPoint(HostIP, 8080);
UdpClient.Send("发送的字节", "发送的字节长度", host);

(2). 知道远程计算机名称::

知道远程计算机名称后,利用"Send"方法直接把UDP数据包发送到远程主机的指定端口号上了,这种调用方式也是最容易的,语法如下:

public int Send(
    byte[] dgram ,
    int bytes ,
    string hostname ,
    int port
);

参数说明:

dgram 要发送的 UDP 数据文报(以字节数组表示)。
bytes 数据文报中的字节数。
hostname 要连接到的远程主机的名称。
port 要与其通讯的远程端口号。
返回值 已发送的字节数。

2.Visual C#使用UdpClient类接收UDP数据包:

接收UDP数据包使用的是UdpClient中的“Receive”方法。此方法的调用语法如下:

public byte [] Receive(
    ref IPEndPoint remoteEP
);

参数

remoteEP 是一个 IPEndPoint类的实例,它表示网络中发送此数据包的节点。

如果指定了远程计算机要发送到本地机的端口号,也可以通过侦听本地端口号来实现对数据的获取,下面就是通过侦听本地端口号“8080”来获取信息代码:

server = new UdpClient();
receivePoint = new IPEndPoint
        (new IPAddress ( "127.0.0.1" ) , 8080);
byte[] recData = server.Receive(ref receivePoint);

三.Visual C#实现UDP协议之网络对时系统的体系结构及功能简介

在局域网中有很多应用软件为了协同工作,需要保证客户机上时间统一,而为了实现这一点,通常的做法是客户机从一个时间相对正确的服务器读取时间,以此来校正本地时间。如经常看到的GPS对时系统等。本节编写的局域网上对时系统的主要的功能是保证局域网上计算机时间、日期的统一。网络对时程序是体系结构分成服务器端程序和客户端程序二个部分,具体的作法是:在同一个网段上,固定一台计算机作为对时的服务器,在这个网段的所有计算机都可以读取这台服务器上的时间和日期,并依此服务器上的时间和日期为基准,来确定本地的时间和日期。在服务器端程序需要达到以下功能:

  • 能够接收局域网中任一台客户机的请求
  • 记录请求客户机的计算机名称,和请求时间
  • 准确发送服务器端的时间和日期

端程序要达到以下功能:

  • 能够设定服务器的主机或者IP地址
  • 能够接收服务器端发送的时间、日期信息
  • 能够以接收的服务器端时间、日期为基准,校正本地时间

因此在具体用Visual C#实现网络对时系统时就包括二个部分:服务器端程序和客户端程序。下面首先介绍Visual C#实现网络对时系统中服务器端程序的具体步骤。

四.Visual C#实现网络对时系统之服务器端程序的具体步骤

服务器端程序比客户端程序相对要简单,主要因是服务器端程序的工作比较简单,就是接收客户端的对时请求、发送服务器端的时间数据。而于客户端不仅要传送和接收数据,还要把服务器端的时间提取出来,并以此来修改本地计算机的时间、日期。下面是用Visual C#实现网络对时系统之服务器端程序的具体步骤步骤。

1.启动Visual Studio .Net。

2.选择菜单【文件】|【新建】|【项目】后,弹出【新建项目】对话框。

3.将【项目类型】设置为【Visual C#项目】。

4.将【模板】设置为【Windows应用程序】。

5.在【名称】文本框中输入【UDP对时服务器端】。

6.在【位置】的文本框中输入【E:\VS.NET项目】,然后单击【确定】按钮

7.在【解决方案资源管理器】窗口中,双击Form1.cs文件,进入Form1.cs文件的编辑界面。

8.在Form1.cs文件的开头,用下列导入命名空间代码替代系统缺省的导入命名空间代码。

using System ;
using System.Drawing ;
using System.Collections ;
using System.ComponentModel ;
using System.Windows.Forms ;
using System.Data ;
using System.Net ;
using System.Net.Sockets ;
using System.Threading ;
//程序中使用到线程
using System.Text ;
//程序中使用到编码

9.切换到【Form1.cs(设计)】窗口,并从【工具箱】中的【Windows窗体组件】中往窗体中拖入下列组件,并执行相应操作:

一个Label组件,显示对时服务器正在运行信息
一个ListBox组件,名称为listBox1,用以显示客户端和服务器端交流的日志
一个Button组件,名称为button1,并在其拖入窗体后,双击,则系统会在Form1.cs文件中产生其Click事件对应的处理代码。

10.在【解决方案资源管理器】窗口中,双击Form1.cs文件,进入Form1.cs文件的编辑界面。在Form1.cs中的class代码区添加下列代码,下列代码是定义程序中使用的全局变量和创建全局使用的实例:

private UdpClient server ;
private IPEndPoint receivePoint ;
private int port = 8080 ;
//定义端口号
private int ip = 127001 ;
//设定本地IP地址
private Thread startServer ;

11.以下面代码替代系统产生的InitializeComponent过程。

private void InitializeComponent()
{
    this.listBox1 = new System.Windows.Forms.ListBox ( ) ;
    this.label1 = new System.Windows.Forms.Label ( ) ;
    this.button1 = new System.Windows.Forms.Button ( ) ;
    this.SuspendLayout ( ) ;
    this.listBox1.ItemHeight = 12 ;
    this.listBox1.Location = new System.Drawing.Point ( 14 , 40 ) ;
    this.listBox1.Name = "listBox1" ;
    this.listBox1.Size = new System.Drawing.Size ( 268 , 220 ) ;
    this.listBox1.TabIndex = 0 ;
    this.label1.ForeColor = System.Drawing.Color.Red ;
    this.label1.Location = new System.Drawing.Point ( 44 , 10 ) ;
    this.label1.Name = "label1" ;
    this.label1.Size = new System.Drawing.Size ( 210 , 24 ) ;
    this.label1.TabIndex = 1 ;
    this.label1.Text = "UDP对时服务器端正在运行......" ;
    this.button1.FlatStyle = System.Windows.Forms.FlatStyle.Flat ;
    this.button1.Location = new System.Drawing.Point ( 106 , 278 ) ;
    this.button1.Name = "button1" ;
    this.button1.Size = new System.Drawing.Size ( 75 , 34 ) ;
    this.button1.TabIndex = 2 ;
    this.button1.Text = "清除信息" ;
    this.button1.Click += new System.EventHandler ( this.button1_Click ) ;
    this.AutoScaleBaseSize = new System.Drawing.Size ( 6 , 14 ) ;
    this.ClientSize = new System.Drawing.Size ( 300 , 329 ) ;
    this.Controls.AddRange ( new System.Windows.Forms.Control[] {
        this.button1 ,
        this.listBox1 ,
        this.label1} ) ;
    this.MaximizeBox = false ;
    this.Name = "Form1" ;
    this.Text = "UDP对时服务器端" ;
    this.Load += new System.EventHandler ( this.Form1_Load ) ;
    this.ResumeLayout ( false ) ;
}

至此,【UDP对时服务器端】项目的界面设计和功能实现的前期工作就完成了,设计界面如图01所示:

43_1
图01:【UDP对时服务器端】项目的设计界面

12.在Form1.cs文件中的InitializeComponent过程的后面添加下面代码,下列代码是定义过程“start_server”。此过程的功能是获取客户端对时请求数据,并向客户端发送服务器当前时间和日期。

public void start_server()
{
    while (true)
    {
        //接收从远程主机发送到本地8080端口的数据
        byte[] recData = server.Receive(ref receivePoint);
        ASCIIEncoding encode = new ASCIIEncoding();
        //获得客户端请求数据
        string Read_str = encode.GetString(recData);
        //提取客户端的信息,存放到定义为temp的字符串数组中
        string[] temp = Read_str.Split("/".ToCharArray());
        //显示端口号的请求信息
        listBox1.Items.Add("时间:" + DateTime.Now.ToLongTimeString() + "   接收信息如下:");
        listBox1.Items.Add("客户机:" + temp[0]);
        listBox1.Items.Add("端口号:" + temp[1]);
        //发送服务器端时间和日期
        byte[] sendData = encode.GetBytes(System.DateTime.Now.ToString());
        listBox1.Items.Add("发送服务器时间!");
        //对远程主机的指定端口号发送服务器时间
        server.Send(sendData, sendData.Length, temp[0], Int32.Parse(temp[1]));
    }
}

请注意:上述代码中约定客户机程序发送对时请求信息到服务器的8080端口号。服务器端程序接收发送到本地8080端口号的数据就完成了数据接收。为了能够让服务器端程序知道是那台客户机提出请求和要把对时信息发送到客户机的那个端口号上,客户端程序对发送的对时请求信息进行了设计。客户端的对时请求信息结构为:

计算机名称 + / + 客户机接收信息端口号

这样如果客户端计算机名称为:majinhu,接收服务器端时间数据的端口号是8080,则客户端程序发送的对时请求数据就为:majinhu/8080

服务器端程序在接收到客户端对时请求数据,并进行分析后,就能够通过UdpClient类的Send方法准确的把服务器端当前的时间和日期发送到客户端指定的端口号上。这样客户端程序通过读取指定的端口号,就能够获得服务器端当前的时间和日期,从而以此来修正客户端的时间和日期了。

13.在“start_server”过程之后面添加下面代码,下列代码是定义“run”过程。“run”过程的作用是创建一个线程实例,并以“start_server”过程来初始化线程实例。之所以采用线程是因为服务器端程序需要不间断读取发送到8080端口号,并且Receive方法是一个阻塞式方法。采用线程就是为了保证服务器端程序正常运行:

public void run()
{
    //利用本地8080端口号来初始化一个UDP网络服务
    server = new UdpClient(port);
    receivePoint = new IPEndPoint(new IPAddress(ip), port);
    //开一个线程
    startServer = new Thread(new ThreadStart(start_server));
    //启动线程
    startServer.Start();
}

14.在Form1.cs中的Main函数之后添加下列代码,下列代码是定义“Form1_Load”事件,在此事件中将调用“run”过程,这样当服务器端程序运行后,就启动网络对时服务:

private void Form1_Load(object sender, System.EventArgs e)
{
    //启动对时服务
    run();
}

15.在Form1.cs文件中的“Form1_Load”事件之后,添加下列代码,下列代码是定义button1的“Click”事件,此事件的作用是清除服务器端程序显示的日志信息:

private void button1_Click(object sender, System.EventArgs e)
{
    //清除服务器端程序日志
    listBox1.Items.Clear();
}

16.用下列代码替换Form1.cs中的Dispose方法。下列代码的功能是手动收集程序中使用的资源:

protected override void Dispose(bool disposing)
{
    try
    {
        //关闭线程
        startServer.Abort();
        //清除资源
        server.Close();
    }
    catch
    {
    };
    if (disposing)
    {
        if (components != null)
        {
            components.Dispose();
        }
    }
    base.Dispose(disposing);
}

至此,在上述步骤都正确完成,【UDP对时服务器端】项目的全部工作就完成了。图02【UDP对时服务器端】运行后的界面,在日志信息中记录了对时请求客户机的名称,发送对时数据的端口号以及客户端请求的时间:

netcode43_2
图02:【UDP对时服务器端】项目的运行界面


2004-11-14 09:24:07 abc

发现在接收时,程序无响应

2004-10-07 16:27:00 xx-boy

下一篇,客户端的东西呢?

2004-08-29 19:59:00

问题解决,原来调试和实际运行情况有点不太一样。:)

2004-08-29 19:54:42

这段代码有问题。

private void Form1_Load(object sender, System.EventArgs e)
{
    //启动对时服务
    run();//放到这个事件里将执行两次。报建立两个相同的套接字错误。
}

正确的应该是这样

public void start_server ( )
{
while ( true )
{
run();//必须在这里调用,在FORM_LOAD事件中重复调用两次。

.............
..............
在C#里关闭这个程序停止时间过长,如何快速的关闭这个程序,有没有好的办法?
DFW:hygsxy

Contributors: FHL