原理簡介:
--------
用RAW Socket實現的ping可能比上一節的應用ICMP.DLL的程序龐大些, 但是這才是我們需要關注的東西, 我的觀點真正想做網絡開發的程序員應該靜下心來讀讀這篇文章, 相信你會從中獲益頗多. 中間我也會講解一些東西為後一章的路由追蹤做一些鋪墊.
另一個重要的要講的東西, 微軟宣布隨時不支持上節講的ping用到的開發接口, 但是本節的講的是更一般的東西. 所以它不會過時, 甚至做很小的改動就可以移植到別的系統上去. 系統移植不是我們的講的重點. 但是微軟的長期支持足以引起我們充分的重視.
如何少作變動來使的這個程序實現追蹤路由的功能, 這裡只是拋磚引玉. 將ICMP包中IP包的包頭該為特定的值就能得到那個路由器的IP(要求到達目的地的跳數大於你設的特定值).
這個程序需要windows2k/WindowsXP/WindowsNT平台和系統管理員的權限.
具體實現:
--------
這段源代碼大部分來自:
http://tangentsoft.net/wskfaq/examples/rawping.html
[bugfree]只做了少量修改,給出了大量的注釋, 最後結合經驗給出了自己的建議.
----------
/*
* 程序名: rawping_driver.cpp
* 說明:
* 驅動程序,也是主函數
*/
#include <winsock2.h>
#include <iostream.h>
#include "rawping.h"
#define DEFAULT_PACKET_SIZE 32 // 默認ICMP包字節數
#define DEFAULT_TTL 30 // 默認TTL值
#define MAX_PING_DATA_SIZE 1024 // 最大數據塊
#define MAX_PING_PACKET_SIZE (MAX_PING_DATA_SIZE + sizeof(IPHeader)) //最大ICMP包長度
/* 為 send_buf 和 recv_buf 分配內存
* send_buf大小為 packet_size
* recv_buf大小為 MAX_PING_PACKET_SIZE, 保證大於send_buf
*/
int allocate_buffers(ICMPHeader*& send_buf, IPHeader*& recv_buf,
int packet_size);
///////////////////////////////////////////////////////////////////////
// Program entry point
int main(int argc, char* argv[])
{
int seq_no = 0; //用在發送和接受的ICMP包頭中
ICMPHeader* send_buf = 0;
IPHeader* recv_buf = 0;
// 判斷命令行是否合法
if (argc < 2) {
cerr << "usage: " << argv[0] << " <host> [data_size] [ttl]" <<
endl;
cerr << "\tdata_size can be up to " << MAX_PING_DATA_SIZE <<
" bytes. Default is " << DEFAULT_PACKET_SIZE << "." <<
endl;
cerr << "\tttl should be 255 or lower. Default is " <<
DEFAULT_TTL << "." << endl;
return 1;
}
// 處理命令行參數
int packet_size = DEFAULT_PACKET_SIZE;
int ttl = DEFAULT_TTL;
if (argc > 2) {
int temp = atoi(argv[2]);
if (temp != 0) {
packet_size = temp;
}
if (argc > 3) {
temp = atoi(argv[3]);
if ((temp >= 0) && (temp <= 255)) {
ttl = temp;
}
}
}
packet_size = max(sizeof(ICMPHeader),
min(MAX_PING_DATA_SIZE, (unsigned int)packet_size));
// 啟動 Winsock
WSAData wsaData;
if (WSAStartup(MAKEWORD(2, 1), &wsaData) != 0) {
cerr << "Failed to find Winsock 2.1 or better." << endl;
return 1;
}
SOCKET sd; // RAW Socket句柄
sockaddr_in dest, source;
// 三個任務(創建sd, 設置ttl, 初試dest的值)
if (setup_for_ping(argv[1], ttl, sd, dest) < 0) {
goto cleanup; //釋放資源並退出
}
// 為send_buf和recv_buf分配內存
if (allocate_buffers(send_buf, recv_buf, packet_size) < 0) {
goto cleanup;
}
// 初試化IMCP數據包(type=8,code=0)
init_ping_packet(send_buf, packet_size, seq_no);
// 發送ICMP數據包
if (send_ping(sd, dest, send_buf, packet_size) >= 0) {
while (1) {
// 接受回應包
if (recv_ping(sd, source, recv_buf, MAX_PING_PACKET_SIZE) <
0) {
// Pull the sequence number out of the ICMP header. If
// it's bad, we just complain, but otherwise we take
// off, because the read failed for some reason.
unsigned short header_len = recv_buf->h_len * 4;
ICMPHeader* icmphdr = (ICMPHeader*)
((char*)recv_buf + header_len);
if (icmphdr->seq != seq_no) {
cerr << "bad sequence number!" << endl;
continue;
}
else {
break;
}
}
if (decode_reply(recv_buf, packet_size, &source) != -2) {
// Success or fatal error (as opposed to a minor error)
// so take off.
break;
}
}
}
cleanup:
delete[]send_buf; //釋放分配的內存
delete[]recv_buf;
WSACleanup(); // 清理winsock
return 0;
}
// 為send_buf 和 recv_buf的內存分配. 太簡單, 我略過
int allocate_buffers(ICMPHeader*& send_buf, IPHeader*& recv_buf,
int packet_size)
{
// First the send buffer
send_buf = (ICMPHeader*)new char[packet_size];
if (send_buf == 0) {
cerr << "Failed to allocate output buffer." << endl;
return -1;
}
// And then the receive buffer
recv_buf = (IPHeader*)new char[MAX_PING_PACKET_SIZE];
if (recv_buf == 0) {
cerr << "Failed to allocate output buffer." << endl;
return -1;
}
return 0;
}
/*
* 程序名: rawping.h
* 說明:
* 主要函數庫頭文件
*/
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
// ICMP 包類型, 具體參見本文的第一節
#define ICMP_ECHO_REPLY 0
#define ICMP_DEST_UNREACH 3
#define ICMP_TTL_EXPIRE 11
#define ICMP_ECHO_REQUEST 8
// 最小的ICMP包大小
#define ICMP_MIN 8
// IP 包頭
struct IPHeader {
BYTE h_len:4; // Length of the header in dwords
BYTE version:4; // Version of IP
BYTE tos; // Type of service
USHORT total_len; // Length of the packet in dwords
USHORT ident; // unique identifier
USHORT flags; // Flags
BYTE ttl; // Time to live, 這個字段我在下一節中用來實現Tracert功能
BYTE proto; // Protocol number (TCP, UDP etc)
USHORT checksum; // IP checksum
ULONG source_ip;
ULONG dest_ip;
};
// ICMP 包頭(實際的包不包括timestamp字段,
// 作者用來計算包的回應時間,其實完全沒有必要這樣做)
struct ICMPHeader {
BYTE type; // ICMP packet type
BYTE code; // Type sub code
USHORT checksum;
USHORT id;
USHORT seq;
ULONG timestamp; // not part of ICMP, but we need it
};
extern USHORT ip_checksum(USHORT* buffer, int size);
extern int setup_for_ping(char* host, int ttl, SOCKET& sd, sockaddr_in& dest);
extern int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader* send_buf, int packet_size);
extern int recv_ping(SOCKET sd, sockaddr_in& source, IPHeader* recv_buf,
int packet_size);
extern int decode_reply(IPHeader* reply, int bytes, sockaddr_in* from);
extern void init_ping_packet(ICMPHeader* icmp_hdr, int packet_size, int seq_no);
/*
* 程序名: rawping.cpp
* 說明:
* 主要函數庫實現部分
*/
include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream.h>
#include "rawping.h"
// 計算ICMP包的校驗和的簡單算法, 很多地方都有說明, 這裡沒有必要詳細將
// 只是一點要提, 做校驗之前, 務必將ICMP包頭的checksum字段置為0
USHORT ip_checksum(USHORT* buffer, int size)
{
unsigned long cksum = 0;
// Sum all the words together, adding the final byte if size is odd
while (size > 1) {
cksum += *buffer++;
size -= sizeof(USHORT);
}
if (size) {
cksum += *(UCHAR*)buffer;
}
// Do a little shuffling
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16);
// Return the bitwise complement of the resulting mishmash
return (USHORT)(~cksum);
}
//初試化RAW Socket, 設置ttl, 初試化dest
// 返回值 <0 表失敗
int setup_for_ping(char* host, int ttl, SOCKET& sd, sockaddr_in& dest)
{
// Create the socket
sd = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, 0, 0, 0);
if (sd == INVALID_SOCKET) {
cerr << "Failed to create raw socket: " << WSAGetLastError() <<
endl;
return -1;
}
if (setsockopt(sd, IPPROTO_IP, IP_TTL, (const char*)&ttl,
sizeof(ttl)) == SOCKET_ERROR) {
cerr << "TTL setsockopt failed: " << WSAGetLastError() << endl;
return -1;
}
// Initialize the destination host info block
memset(&dest, 0, sizeof(dest));
// Turn first passed parameter into an IP address to ping
unsigned int addr = inet_addr(host);
if (addr != INADDR_NONE) {
// It was a dotted quad number, so save result
dest.sin_addr.s_addr = addr;
dest.sin_family = AF_INET;
}
else {
// Not in dotted quad form, so try and look it up
hostent* hp = gethostbyname(host);
if (hp != 0) {
// Found an address for that host, so save it
memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
dest.sin_family = hp->h_addrtype;
}
else {
// Not a recognized hostname either!
cerr << "Failed to resolve " << host << endl;
return -1;
}
}
return 0;
}
//初試化ICMP的包頭, 給data部分填充數據, 最後計算整個包的校驗和
void init_ping_packet(ICMPHeader* icmp_hdr, int packet_size, int seq_no)
{
// Set up the packet's fields
icmp_hdr->type = ICMP_ECHO_REQUEST;
icmp_hdr->code = 0;
icmp_hdr->checksum = 0;
icmp_hdr->id = (USHORT)GetCurrentProcessId();
icmp_hdr->seq = seq_no;
icmp_hdr->timestamp = GetTickCount();
// "You're dead meat now, packet!"
const unsigned long int deadmeat = 0xDEADBEEF;
char* datapart = (char*)icmp_hdr + sizeof(ICMPHeader);
int bytes_left = packet_size - sizeof(ICMPHeader);
while (bytes_left > 0) {
memcpy(datapart, &deadmeat, min(int(sizeof(deadmeat)),
bytes_left));
bytes_left -= sizeof(deadmeat);
datapart += sizeof(deadmeat);
}
// Calculate a checksum on the result
icmp_hdr->checksum = ip_checksum((USHORT*)icmp_hdr, packet_size);
}
// 發送生成的ICMP包
// 返回值 <0 表失敗
int send_ping(SOCKET sd, const sockaddr_in& dest, ICMPHeader* send_buf,
int packet_size)
{
// Send the ping packet in send_buf as-is
cout << "Sending " << packet_size << " bytes to " <<
inet_ntoa(dest.sin_addr) << "..." << flush;
int bwrote = sendto(sd, (char*)send_buf, packet_size, 0,
(sockaddr*)&dest, sizeof(dest));
if (bwrote == SOCKET_ERROR) {
cerr << "send failed: " << WSAGetLastError() << endl;
return -1;
}
else if (bwrote < packet_size) {
cout << "sent " << bwrote << " bytes..." << flush;
}
return 0;
}
// 接受ICMP包
// 返回值 <0 表失敗
int recv_ping(SOCKET sd, sockaddr_in& source, IPHeader* recv_buf,
int packet_size)
{
// Wait for the ping reply
int fromlen = sizeof(source);
int bread = recvfrom(sd, (char*)recv_buf,
packet_size + sizeof(IPHeader), 0,
(sockaddr*)&source, &fromlen);
if (bread == SOCKET_ERROR) {
cerr << "read failed: ";
if (WSAGetLastError() == WSAEMSGSIZE) {
cerr << "buffer too small" << endl;
}
else {
cerr << "error #" << WSAGetLastError() << endl;
}
return -1;
}
return 0;
}
// 對收到的ICMP解碼
// 返回值 -2表忽略, -1 表失敗, 0 成功
int decode_reply(IPHeader* reply, int bytes, sockaddr_in* from)
{
// 跳過IP包頭, 找到ICMP的包頭
unsigned short header_len = reply->h_len * 4;
ICMPHeader* icmphdr = (ICMPHeader*)((char*)reply + header_len);
// 包的長度合法, header_len + ICMP_MIN為最小ICMP包的長度
if (bytes < header_len + ICMP_MIN) {
cerr << "too few bytes from " << inet_ntoa(from->sin_addr) <<
endl;
return -1;
}
// 下面的包類型詳細參見我的第一部分 "透析ICMP協議(一): 協議原理"
else if (icmphdr->type != ICMP_ECHO_REPLY) { //非正常回復
if (icmphdr->type != ICMP_TTL_EXPIRE) { //ttl減為零
if (icmphdr->type == ICMP_DEST_UNREACH) { //主機不可達
cerr << "Destination unreachable" << endl;
}
else { //非法的ICMP包類型
cerr << "Unknown ICMP packet type " << int(icmphdr->type) <<
" received" << endl;
}
return -1;
}
}
else if (icmphdr->id != (USHORT)GetCurrentProcessId()) {
//不是本進程發的包, 可能是同機的其它ping進程發的
return -2;
}
// 指出包傳遞了多遠
// [bugfree]我認為作者這裡有問題, 因為有些系統的ttl初值為128如winXP,
// 有些為256如我的DNS服務器211.97.168.129, 作者假設為256有點武斷,
// 可以一起探討這個問題, 回email:[email protected]
int nHops = int(256 - reply->ttl);
if (nHops == 192) {
// TTL came back 64, so ping was probably to a host on the
// LAN -- call it a single hop.
nHops = 1;
}
else if (nHops == 128) {
// Probably localhost
nHops = 0;
}
// 所有工作結束,打印信息
cout << endl << bytes << " bytes from " <<
inet_ntoa(from->sin_addr) << ", icmp_seq " <<
icmphdr->seq << ", ";
if (icmphdr->type == ICMP_TTL_EXPIRE) {
cout << "TTL expired." << endl;
}
else {
cout << nHops << " hop" << (nHops == 1 ? "" : "s");
cout << ", time: " << (GetTickCount() - icmphdr->timestamp) <<
" ms." << endl;
}
return 0;
}
總結和建議:
-----------
bugfree建議其中的這些方面需要改進:
1. 頭文件iostream.h 改為 iostream, 後者是標准C++的頭文件
同時添加對std::cout 和 std::endl;的引用
對於cerr 建議都改為std::cout(因為後者頭文件不支持)
2. 程序的發送和接受采用了同步的方式, 這使得如果出現網絡問題recv_ping將陷入持續等待.
這是我們不想看到的.
這三種技術可以達到目的:
- 使用多線程, 將ping封裝進線程, 在主程序中對它的超時進行處理
- 使用select()函數來實現
- 使用windows的 WSAAsyncSelect()
這裡對這些方法不作具體討論, 留給讀者自已完成.