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也存同樣的問題。