打印

[转帖] 利用Visual C#实现ICMP网络协议

利用Visual C#实现ICMP网络协议

ICMP就是所谓Internet控制报文协议(Internet Control Message Protocol),在网络中,一般用它来传递差错报文以及其他应注意信息。ICMP一般被认为是和IP协议同一层协议,IMCP报文通常被IP层或者更高层协议(如:TCP或者UDP)使用,ICMP对于互联网以及其他基于IP协议网络正常运行起着非常重要作用。有许多重要网络程序都是基于ICMP协议上,最为著名如Ping和Tracert等。本文就来介绍用Visual C#实现基于ICMP协议重要网络命令Ping方法。  Ping命令是可以说是一个"跨平台"程序,这是因为Ping命令不仅存在Windows系统上,在Unix系统上也有Ping命令,其实对其他只要是支持网络操作系统,一般也都存在该命令。Ping命令主要作用是检测网络上主机状态。要是在几年前,我们还可以下如此断言,如果不能Ping通某台主机,那么也就无法Telnet或者FTP到这台主机,但随着互联网安全意识增加,出现了访问控制清单路由器和防火墙,由于ICMP报文是在IP数据包中被传输,而到达一台主机不仅取决于IP层是否到达,还取决于使用何种协议和端口。譬如金山公司金山网镖就可以禁止其他机器Ping通这台主机。所以在现在情况下,即时Ping不通某台机器,但也有可能FTP登陆到这台机器,或者通过HTTP来浏览这台机器上Web页面。
  一.Ping命令简介
  首先进入Windows系统中命令提示符,输入"Ping/?"后,单击回车键,您就可以了解Ping命令各种参数使用方法。最为常见使用方法是"Ping 远程计算机名称(或者远程计算机IP地址)",如果在Ping命令返回字符中有"Reply from",说明此主机在线,具体如图01:
  图01:Ping通主机时运行界面
  如果返回字符中有"Request timeout",一般情况此主机不在线,具体如图02:
  图02:Ping不通主机时运行界面
  二.Ping命令、ICMP报文和IP数据包
  Ping命令基于是TCP/IP协议簇中ICMP协议,在编写基于ICMP协议网络应用程序时,应注意下面二点:
  1. ICMP报文是封装在IP数据包中传输
  了解这一点对后面编程非常重要,图03是IP数据包组成结构图:
  图03:IP数据包组成结构图
  习惯上把IP数据包划分为三个部分:
  (1).IP数据包中前二十个字节数据,即图03中从【4位版本】到【32位目地址IP】,这称为IP首部。
  (2).选项,即图03中【选项(如果有)】部分。
  (3).数据,即图03中【数据】部分。
  其中后面二个部分组成就是ICMP报文。ICMP报文具体组成结构如图04所示:
  图04:ICMP报文组成结构图
  2. ICMP协议没有固定端口号。
  ICMP协议和其他协议不同,其他协议基本都对应固定端口号,如HTTP协议是通过80端口号来交换数据
  了解上面二点对后面在Visual C#实现Ping命令是非常有用。因为在下面在编写Visual C#实现Ping命令程序中,程序中定义一个名称为"IcmpPacket"类,通过这个类来构造ICMP报文,而定义"IcmpPacket"类依据就是图03所示ICMP报文组成结构。同样由于ICMP协议没有对应固定端口号,这就意味着,编写Visual C#实现Ping命令中可以随意选择端口号,本文选择端口号是"30"。
  由于ICMP协议是一个复杂协议,而本文由于篇幅所限,对ICMP很多细节,就不能一一介绍,如果你对ICMP协议感兴趣或对上面介绍仍然感觉有点模糊,那就请参阅探讨ICMP协议相关书籍,它们一般介绍都很详细。
  三.简介Visual C#实现Ping命令使用类:
  Visual C#实现Ping命令中涉及到很多类,其中最重要是Socket类。这是因为程序中发送含有ICMP报文IP数据包,接收含有ICMP超时或ICMP会显报文IP数据包和设定IP数据包中TTL数值都会使用到Socket类。表01和表02是Socket类中常用属性和方法及其简要说明。
  
属性 说明
AddressFamily 获取Socket地址族。
Available获取已经从网络接收且可供读取数据量。
Blocking获取或设置一个值,该值指示Socket是否处于阻塞模式。
Connected 获取一个值,该值指示Socket是否已连接到远程资源。
Handle 获取Socket操作系统句柄。
LocalEndPoint 获取本地终结点。
ProtocolType获取Socket协议类型。
RemoteEndPoint 获取远程终结点。
SocketType获取Socket类型。
         表01:Socket类常用属性及其说明
  
方法说明
Accept 创建新Socket以处理传入连接请求。
BeginAccept开始一个异步请求,以创建新Socket来接受传入连接请求。
BeginConnect开始对网络设备连接异步请求。
BeginReceive开始从连接Socket中异步接收数据。
BeginReceiveFrom 开始从指定网络设备中异步接收数据。
BeginSend将数据异步发送到连接
BeginSendTo 向特定远程主机异步发送数据。
Bind 使Socket与一个本地终结点相关联。
Close强制Socket连接关闭。
Connect建立到远程设备连接。
EndAccept结束异步请求以创建新Socket来接受传入连接请求。
EndConnect结束挂起异步连接请求。
EndReceive结束挂起异步读取。
EndReceiveFrom结束挂起、从特定终结点进行异步读取。
EndSend 结束挂起异步发送。
EndSendTo结束挂起、向指定位置进行异步发送。
GetSocketOption 返回Socket选项值。
IOControl为Socket设置低级别操作模式。
Listen将Socket置于侦听状态。
Poll 确定Socket状态。
Receive接收来自连接Socket数据。
ReceiveFrom 接收数据文报并存储源终结点。
Select确定一个或多个套接字状态。
Send 将数据发送到连接
SendTo 将数据发送到特定终结点。
SetSocketOption 设置Socket选项。
Shutdown 禁用某Socket上发送和接收。
       表02:Socket类常用方法及其说明
  其中包含六组异步方法,它们是:
  ·BeginAccept和EndAccept
  ·BeginConnect和EndConnect
  ·BeginReceive和EndReceive
  ·BeginReceiveFrom和EndReceiveFrom"
  ·BeginSend和EndSend
  ·BeginSendTo"和"EndSendTo
  其功能分别相当于"Accept"、"Connect"、"Receive"、"ReceiveFrom"、"Send"和"SendTo"方法。
  四.Visual C#实现Ping命令关键步骤及其解决方法
  根据Ping命令执行过程,可以把Ping命令分成三个主要步骤:
  1. 定义ICMP报文。
  2. 客户机发送封装ICMP回显请求报文IP数据包。
  3. 客户机接收封装ICMP应答报文IP数据包。
  解决了上述三个步骤,Visual C#实现Ping命令就基本可以完成了。下面是这三个步骤具体解决方法。
  1. 定义ICMP报文:
  根据图05所示ICMP报文组成结构,定义了一个类--IcmpPacket类。IcmpPacket类通过实例化就能够得到ICMP报文。下面代码是定义IcmpPacket类:
  public class IcmpPacket
{
 private Byte _type ;
 // 类型
 private Byte _subCode ;
 // 代码
 private UInt16 _checkSum ;
 // 校验和
 private UInt16 _identifier ;
 // 识别符
 private UInt16 _sequenceNumber ;
 // 序列号
 private Byte [ ] _data ;
 //选项数据
 public IcmpPacket ( Byte type , Byte subCode , UInt16 checkSum , UInt16 identifier , UInt16 sequenceNumber , int dataSize )
 {
  _type = type ;
  _subCode = subCode ;
  _checkSum = checkSum ;
  _identifier = identifier ;
  _sequenceNumber = sequenceNumber ;
  _data=new Byte [ dataSize ] ;
  //在数据中,写入指定数据大小
  for ( int i = 0 ; i < dataSize ; i )
  {
   //由于选项数据在此命令中并不重要,所以你可以改换任何你喜欢字符
   _data [ i ] = ( byte )'#' ;
  }
 }
 public UInt16 CheckSum
 {
  get
  {
   return _checkSum ;
  }
  set
  {
   _checkSum=value ;
  }
 }
 //初始化ICMP报文
 public int CountByte ( Byte [ ] buffer )
 {
  Byte [ ] b_type = new Byte [ 1 ] { _type } ;
  Byte [ ] b_code = new Byte [ 1 ] { _subCode } ;
  Byte [ ] b_cksum = BitConverter.GetBytes ( _checkSum ) ;
  Byte [ ] b_id = BitConverter.GetBytes ( _identifier ) ;
  Byte [ ] b_seq = BitConverter.GetBytes ( _sequenceNumber ) ;
  int i = 0 ;
  Array.Copy ( b_type , 0 , buffer , i , b_type.Length ) ;
  i = b_type.Length ;
  Array.Copy ( b_code , 0 , buffer , i , b_code.Length ) ;
  i = b_code.Length ;
  Array.Copy ( b_cksum , 0 , buffer ,i , b_cksum.Length ) ;
  i = b_cksum.Length ;
  Array.Copy ( b_id , 0 , buffer , i , b_id.Length ) ;
  i = b_id.Length ;
  Array.Copy ( b_seq , 0 , buffer , i , b_seq.Length ) ;
  i = b_seq.Length ;
  Array.Copy ( _data , 0 , buffer , i , _data.Length ) ;
  i = _data.Length ;
  return i ;
 }
 //将整个ICMP报文信息和数据转化为Byte数据包
 public static UInt16 SumOfCheck ( UInt16 [ ] buffer )
 {
  int cksum = 0 ;
  for ( int i = 0 ; i < buffer.Length ; i )
   cksum = ( int ) buffer [ i ] ;
   cksum = ( cksum >> 16 ) ( cksum & 0xffff ) ;
   cksum = ( cksum >> 16 ) ;
   return ( UInt16 ) ( ~cksum ) ;
 }
}

  下列代码是利用IcmpPacket类来创建ICMP报文:
  IcmpPacket packet = new IcmpPacket ( 0 , 0 , 0 , 45 , 0 , 4 ) ;
  此代码定义ICMP报文中数据段长度为4个字节,所以整个ICMP报文长度为12个字节(即:8+4),而封装此ICMP报文IP数据包长度就是32个字节(即:8 4 20)。在后面介绍程序中,从客户端发送ICMP会显请求报文数据长度为4个字节,但从服务器介绍到数据却是32个字节原因。
  2. 发送封装ICMP回显请求报文IP数据包:
  发送IP数据包首先要创建一个能够发送封装ICMP回显请求报文IP数据包Socket实例,然后调用此Socket实例中"SendTo"方法就可以了。下列代码是创建并初始化一个发送封装ICMP回显请求报文IP数据包Socket实例:
  Socket socket = new Socket ( AddressFamily.InterNetwork , SocketType.Raw , ProtocolType.Icmp ) ;
  创建初始化Socket实例有三个参数,下面是这些参数说明:
  第一个参数定义目前网络寻址方案,目前还是IPV4,所有只有定义为"AddressFamily.InterNetwork"。
  第二个参数定义Socket实例类型,由于Socket通讯协议是ICMP,所以选择枚举值"Raw Socket"。
  第三个参数是定义Socket实例有效协议类型,由于此Socket实例要传送是ICMP报文,所以选定枚举值为"ProtocolType.Icmp"。
  3.客户机接收封装ICMP应答报文IP数据包:
  接收服务器端返回封装ICMP应答报文IP数据包只需调用Socket实例中"ReceiveFrom"方法就可以了,具体可参阅下面介绍程序实现中代码。
  五.Visual C#实现Ping命令设计、调试、运行软件环境:
  (1).微软公司视窗2000服务器版
  (2).Visual Studio .Net正式版,.Net FrameWork SDK版本号3705
  六.Visual C#实现Ping命令实现步骤:
  下面是Visual C#实现Ping命令具体实现步骤:
  1. 启动Visual Studio .Net。
  2. 选择菜单【文件】|【新建】|【项目】后,弹出【新建项目】对话框。
  3. 将【项目类型】设置为【Visual C#项目】。
  4. 将【模板】设置为【Windows应用程序】。
  5. 在【名称】文本框中输入【Visual C#实现Ping命令】。
  6. 在【位置】文本框中输入【E:\VS.NET项目】,然后单击【确定】按钮,具体如图05所示:
  图05:【Visual C#实现Ping命令】项目【新建项目】对话框
  7. 【解决方案资源管理器】窗口中,双击Form1.cs文件,进入Form1.cs文件编辑界面。
  8. 在Form1.cs文件开头导入命名空间代码区,添加下列代码,下列代码是导入下面程序中使用到类所在命名空间:
  using System.Net ;
using System.Net.Sockets ;

  9. 把Visual Studio .Net当前窗口切换到【Form1.cs(设计)】窗口,并从【工具箱】中【Windows窗体组件】选项卡中拖入下列组件到设计窗体,并执行相应操作:
  一个TextBox组件,用来输入进行Ping操作远程主机名称或IP地址。
  一个ListBox组件,用以显示Ping操作结果。
  一个Label组件。
  一个Button组件,名称为button1,并在它拖入窗体后,双击它,则Visual Studio .Net会在Form1.cs文件中产生其Click事件对应处理代码。
  10. 把Visual Studio .Net当前窗口切换到Form1.cs代码编辑窗口,并用下列代码替换Form1.cs中InitializeComponent过程对应处理代码:
  private void InitializeComponent ( )
{
this.textBox1 = new System.Windows.Forms.TextBox ( ) ;
this.label1 = new System.Windows.Forms.Label ( ) ;
this.listBox1 = new System.Windows.Forms.ListBox ( ) ;
this.button1 = new System.Windows.Forms.Button ( ) ;
this.SuspendLayout ( ) ;
this.textBox1.Location = new System.Drawing.Point ( 116 , 14 ) ;
this.textBox1.Name = "textBox1" ;
this.textBox1.Size = new System.Drawing.Size ( 148 , 21 ) ;
this.textBox1.TabIndex = 0 ;
this.textBox1.Text = "" ;
this.textBox1.TextChanged = new System.EventHandler ( this.textBox1_TextChanged ) ;
this.label1.Location = new System.Drawing.Point ( 12 , 14 ) ;
this.label1.Name = "label1" ;
this.label1.TabIndex = 1 ;
this.label1.Text = "请输入主机名:" ;
this.listBox1.BackColor = System.Drawing.SystemColors.WindowText ;
this.listBox1.ForeColor = System.Drawing.SystemColors.Window ;
this.listBox1.ItemHeight = 12 ;
this.listBox1.Location = new System.Drawing.Point ( 6 , 42 ) ;
this.listBox1.Name = "listBox1" ;
this.listBox1.Size = new System.Drawing.Size ( 400 , 280 ) ;
this.listBox1.TabIndex = 2 ;
this.button1.Location = new System.Drawing.Point ( 274 , 12 ) ;
this.button1.Name = "button1" ;
this.button1.TabIndex = 3 ;
this.button1.Text = "Ping" ;
this.button1.Click = new System.EventHandler ( this.button1_Click ) ;
this.AutoScaleBaseSize = new System.Drawing.Size ( 6 , 14 ) ;
this.ClientSize = new System.Drawing.Size ( 410 , 333 ) ;
this.Controls.AddRange ( new System.Windows.Forms.Control[ ] {
this.button1 ,
this.listBox1 ,
this.label1 ,
this.textBox1 } ) ;
this.MaximizeBox = false ;
this.Name = "Form1" ;
this.Text = "Visual C#实现Ping" ;
this.ResumeLayout ( false ) ;
}

  至此【Visual C#实现Ping命令】项目界面设计工作和功能实现前期准备工作就完成了,设计后界面如图06所示:
  图06:【Visual C#实现Ping命令】项目设计界面
  11. 用下列代码替换Form1.cs文件中button1组件Click事件对应处理代码,下列代码作用是创建、发送ICMP报文,实现Ping命令:
  Private Void Button1_click ( Object Sender , System.eventargs E )
{
 Listbox1.items.clear ( ) ;
 String Hostclient = Textbox1.text ;
 Int K ;
 For ( K = 0 ; K < 3 ; K )
 {
  Socket Socket = New Socket ( Addressfamily.internetwork , Sockettype.raw , Protocoltype.icmp ) ;
  Iphostentry Hostinfo ;
  Try
  {
   //解析主机ip入口
   Hostinfo = Dns.gethostbyname ( Hostclient ) ;
  }
  Catch ( Exception )
  {
   //解析主机名错误。
   Listbox1.items.add ( "没有发现此主机!" ) ;
   Return ;
  }
  // 取服务器端主机30号端口
  Endpoint Hostpoint = ( Endpoint ) New Ipendpoint ( Hostinfo.addresslist[ 0 ] , 30 ) ;
  Iphostentry Clientinfo ;
  Clientinfo = Dns.gethostbyname ( Hostclient ) ;
  // 取客户机端主机30端口
  Endpoint Clientpoint = ( Endpoint ) New Ipendpoint ( Clientinfo.addresslist[ 0 ] , 30 ) ;
  //设置icmp报文
  Int Datasize = 4 ; // Icmp数据包大小 ;
  Int Packetsize = Datasize 8 ;//总报文长度
  Const Int Icmp_echo = 8 ;
  Icmppacket Packet = New Icmppacket ( Icmp_echo , 0 , 0 , 45 , 0 , Datasize ) ;
  Byte [ ] Buffer = New Byte [ Packetsize ] ;
  Int Index = Packet.countbyte ( Buffer ) ;
  //报文出错
  If ( Index != Packetsize )
  {
   Listbox1.items.add ( "报文出现问题!" ) ;
   Return ;
  }
  Int Cksum_buffer_length = ( Int ) Math.ceiling ( ( ( Double )index )/ 2 ) ;
  Uint16 [ ] Cksum_buffer = New Uint16 [ Cksum_buffer_length ] ;
  Int Icmp_header_buffer_index = 0 ;
  For ( Int I = 0 ; I < Cksum_buffer_length ; I )
  {
   //将两个byte转化为一个uint16
   Cksum_buffer[ I ] = Bitconverter.touint16 ( Buffer , Icmp_header_buffer_index ) ;
   Icmp_header_buffer_index = 2 ;
  }
  //将校验和保存至报文里
  Packet.checksum = Icmppacket.sumofcheck ( Cksum_buffer ) ;
  // 保存校验和后,再次将报文转化为数据包
  Byte [ ] Senddata = New Byte [ Packetsize ] ;
  Index = Packet.countbyte ( Senddata ) ;
  //报文出错
  If ( Index != Packetsize )
  {
   Listbox1.items.add ( "报文出现问题!" ) ;
   Return ;
  }
  Int Nbytes = 0 ;
  //系统计时开始
  Int Starttime = Environment.tickcount ;
  //发送数据包
  If ( ( Nbytes = Socket.sendto ( Senddata , Packetsize , Socketflags.none , Hostpoint ) ) == -1 )
  {
   Listbox1.items.add ( "无法传送报文!" ) ;
  }
  Byte [ ] Receivedata = New Byte[ 256 ] ; //接收数据
  Nbytes = 0 ;
  Int Timeout = 0 ;
  Int Timeconsume = 0 ;
  While ( True )
  {
   Nbytes = Socket.receivefrom ( Receivedata , 256 , Socketflags.none , Ref Clientpoint ) ;
   If ( Nbytes == -1 )
   {
    Listbox1.items.add ( "主机没有响应!" ) ;
    Break ;
   }
   Else If ( Nbytes > 0 )
   {
    Timeconsume = System.environment.tickcount - Starttime ;
    //得到发送报文到接收报文之间花费时间
    Listbox1.items.add ( "reply From " Hostinfo.addresslist[ 0 ].tostring ( ) " In "
Timeconsume "ms :bytes Received " Nbytes ) ;
    Break ;
   }
   Timeconsume = Environment.tickcount - Starttime ;
   If ( Timeout > 1000 )
   {
    Listbox1.items.add ( "time Out" ) ;
    Break ;
   }
  }
  //关闭套接字
  Socket.close ( ) ;
 }
}

  12. 在Form1.cs文件中Main函数之后,添加下列代码,下列代码是在Form1.cs中定义IcmpPacket类,程序是通过此类来构造ICMP报文:
  {
 private Byte _type ;
 // 类型
 private Byte _subCode ;
 // 代码
 private UInt16 _checkSum ;
 // 校验和
 private UInt16 _identifier ;
 // 识别符
 private UInt16 _sequenceNumber ;
 // 序列号
 private Byte [ ] _data ;
 //选项数据
 public IcmpPacket ( Byte type , Byte subCode , UInt16 checkSum , UInt16 identifier , UInt16 sequenceNumber , int dataSize )
 {
  _type = type ;
  _subCode = subCode ;
  _checkSum = checkSum ;
  _identifier = identifier ;
  _sequenceNumber = sequenceNumber ;
  _data=new Byte [ dataSize ] ;
  //在数据中,写入指定数据大小
  for ( int i = 0 ; i < dataSize ; i )
  {
   //由于选项数据在此命令中并不重要,所以你可以改换任何你喜欢字符
   _data [ i ] = ( byte )'#' ;
  }
 }
 public UInt16 CheckSum
 {
  get
  {
   return _checkSum ;
  }
  set
  {
   _checkSum=value ;
  }
 }
 //初始化ICMP报文
 public int CountByte ( Byte [ ] buffer )
 {
  Byte [ ] b_type = new Byte [ 1 ] { _type } ;
  Byte [ ] b_code = new Byte [ 1 ] { _subCode } ;
  Byte [ ] b_cksum = BitConverter.GetBytes ( _checkSum ) ;
  Byte [ ] b_id = BitConverter.GetBytes ( _identifier ) ;
  Byte [ ] b_seq = BitConverter.GetBytes ( _sequenceNumber ) ;
  int i = 0 ;
  Array.Copy ( b_type , 0 , buffer , i , b_type.Length ) ;
  i = b_type.Length ;
  Array.Copy ( b_code , 0 , buffer , i , b_code.Length ) ;
  i = b_code.Length ;
  Array.Copy ( b_cksum , 0 , buffer ,i , b_cksum.Length ) ;
  i = b_cksum.Length ;
  Array.Copy ( b_id , 0 , buffer , i , b_id.Length ) ;
  i = b_id.Length ;
  Array.Copy ( b_seq , 0 , buffer , i , b_seq.Length ) ;
  i = b_seq.Length ;
  Array.Copy ( _data , 0 , buffer , i , _data.Length ) ;
  i = _data.Length ;
  return i ;
 }
 //将整个ICMP报文信息和数据转化为Byte数据包
 public static UInt16 SumOfCheck ( UInt16 [ ] buffer )
 {
  int cksum = 0 ;
  for ( int i = 0 ; i < buffer.Length ; i )
   cksum = ( int ) buffer [ i ] ;
   cksum = ( cksum >> 16 ) ( cksum & 0xffff ) ;
   cksum = ( cksum >> 16 ) ;
   return ( UInt16 ) ( ~cksum ) ;
 }
}

  13. 至此,在上述步骤都正确完成,并全部保存后,【Visual C#实现Ping命令】项目全部工作就完成了。此时单击【F5】快捷键运行程序。在程序【请输入主机名】文本框中输入远程主机名,这里输入是互联网主机"WWW.163.com",单击【Ping】按钮,则程序把Ping操作后信息显示出来。具体如图07所示:
  图06:【Visual C#实现Ping命令】运行界面
  七.总结:
  在运行上述程序时,如果网络状况良,则ICMP报文发送和返回时间差就很短,"in"后面带时间就小,这也就是所谓"离""近";如果网络状况不,则ICMP报文发送和返回时间差就长,"in"后面带时间就大,甚至可能出现timeout,即超时。这表明"离""远"。当然如果对方没有开机,也会出现超时情况,所以实际操作要具体情况,具体对待。
  细心读者可能多次运行此程序时候,就会发现,第一次发送时所耗时间往往比本程序紧接着几次大得多。这是程序数据缓存造成。这也就是说ping命令第一次数据是不准确。这种情况不仅在本文中Ping命令中存在,对于Windows系统Ping也存同样问题。

[ 本帖最后由 kenknigh 于 2008-1-18 09:03 编辑 ]
是日已过,命亦随减。如少水鱼,斯有何乐? 大众当勤精进,如救头燃。但念无常,慎勿放逸

                      

TOP