計算機網路——Tracert與Ping程式設計與實現
阿新 • • 發佈:2021-01-22
計算機網路——Tracert與Ping程式設計與實現
一、實驗目的
瞭解Tracert程式的實現原理,並除錯通過。
二、總體設計
1. 基本原理
tracert(跟蹤路由)是路由跟蹤實用程式,用於確定IP資料包訪問目標所採取的路徑。tracert 有一個固定的時間等待響應(ICMP TTL到期訊息)。如果這個時間過了,它將打印出一系列的*號表明:在這個路徑上,這個裝置不能在給定的時間內發出ICMP TTL到期訊息的響應。然後,Tracert給TTL記數器加1,繼續進行。
2. 設計步驟
(1)載入套接字,建立套接字型檔;
使用Socket的程式在使用Socket之前必須呼叫WSAStartup函式,以後應用程式就可以呼叫所請求的Socket庫中的其他Socket函數了。
(2)用inet_addr()將輸入的點分十進位制的IP地址轉換為無符號長整型數,轉換不成功時,按域名解析得到IP地址;
gethostbyname()是查詢主機名最基本的函式,如果呼叫成功,就返回一個指向hosten結構的指標,該結構中含有對應於給定主機名的主機名字和地址資訊,用來承接域名解析的結構。
(3)設定傳送接收超時時間,即請求超時,設定接收、傳送超時的套接字;
(4)構造ICMP回顯請求訊息,並以TTL遞增順序傳送報文,填充ICMP報文中每次傳送時不變的欄位,構造ICMP頭;
(6)指定對方資訊,傳送TCP回顯請求資訊;
sendto()函式利用資料表的方式進行資料傳輸,指定哪個socket傳送給對方
(7)接收ICMP差錯報文並進行解析:如果有資料到達,解析資料包,如果到達目的地址,輸出IP地址;如果沒有資料到達,輸出接收超時,遞增TTL值,TTL增為最大時,若還沒有到達目的地址,退出迴圈,輸出目的地址不線上;
recvform()利用資料報方式進行資料傳輸,當recvfrom()返回時,(sockaddr*)&from包含實際存入from中的資料位元組數。Recvfrom函式返回接收到的位元組數或當出現錯誤時返回-1,並置相應的errno。
三、詳細設計
1. 程式流程圖
2. 實驗程式碼
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <sstream>
using namespace std;
#pragma comment(lib, "Ws2_32.lib")
const int ipAddressSize = 14;
//int count11=0;
//IP 報頭
typedef struct
{
unsigned char hdr_len : 4; //4 位頭部長度
unsigned char version : 4; //4 位版本號
unsigned char tos; //8 位服務型別
unsigned short total_len; //16 位總長度: 和頭部長度一起就能區分 頭 主體資料了
unsigned short identifier; //16 位識別符號: 作用是分片後的重組
unsigned short frag_and_flags; //3 位標誌加 13 位片偏移: 標誌:MF 1是否還有分配 0 沒有分片了
// DF 0 可以分片
// 片偏移:分片後的相對於原來的偏移
unsigned char ttl; //8 位生存時間
unsigned char protocol; //8 位上層協議號: 指出是何種協議
unsigned short checksum; //16 位校驗和: 檢驗是否出錯
unsigned long sourceIP; //32 位源 IP 地址
unsigned long destIP; //32 位目的 IP 地址
} IP_HEADER;
//ICMP 報頭,一共八個位元組,前四個位元組為:型別(1位元組)、程式碼(1位元組)和檢驗和(2位元組)。後四個位元組取決於型別
typedef struct
{
BYTE type; //8 位型別欄位:標識ICMP的作用
BYTE code; //8 位程式碼欄位
USHORT cksum; //16 位校驗和
USHORT id; //16 位識別符號
USHORT seq; //16 位序列號
} ICMP_HEADER;
//報文解碼結構
//接收到的資料快取是字元陣列char bufRev[],因此需要通過特定的解析(也就是拆成一段一段的)獲取想要的資訊
//把資訊封裝到結構體中,就比較方便的得到序列號、往返時間和目的IP了。
typedef struct
{
USHORT usSeqNo; //序列號
DWORD dwRoundTripTime; //往返時間
in_addr dwIPaddr; //返回報文的 IP 地址
} DECODE_RESULT;
//計算網際校驗和函式
USHORT checksum(USHORT *pBuf, int iSize)
{
unsigned long cksum = 0;
while (iSize > 1)
{
cksum += *pBuf++;
iSize -= sizeof(USHORT);
}
if (iSize)
{
cksum += *(UCHAR *)pBuf;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >> 16);
return (USHORT)(~cksum);
}
//對資料包進行解碼
// 1)接收到的Buf 2)接收到的資料長度 3)解析結果封裝到Decode 4)ICMP回顯型別 5)TIMEOUT時間
BOOL DecodeIcmpResponse2(char * pBuf, int iPacketSize, DECODE_RESULT &DecodeResult, BYTE
ICMP_ECHO_REPLY, BYTE ICMP_TIMEOUT)
{
//查詢資料報大小合法性
//pBuf的首地址,就是IP報的首地址
IP_HEADER *pIpHdr = (IP_HEADER*)pBuf;
int iIpHdrLen = pIpHdr->hdr_len * 4;
if(iPacketSize < (int)(iIpHdrLen + sizeof(ICMP_HEADER)))
return FALSE;
// 根據 ICMP 報文型別提取 ID 欄位和序列號欄位
//ICMP欄位包含在 IP資料段的起始位置,因此扣掉IP頭,得到的就是ICMP頭
ICMP_HEADER *pIcmpHdr = (ICMP_HEADER *)(pBuf + iIpHdrLen);
USHORT usID, usSquNo;
if (pIcmpHdr->type == ICMP_ECHO_REPLY) // ICMP 回顯應答報文
{
usID = pIcmpHdr->id;//報文 ID
usSquNo = pIcmpHdr->seq;//報文序列號
}
else if (pIcmpHdr->type == ICMP_TIMEOUT)//ICMP超時差錯報文
{
// 如果是TIMEOUT ,那麼在ICMP資料包中,會夾帶一個IP報(荷載IP)
char * pInnerIpHdr = pBuf + iIpHdrLen + sizeof(ICMP_HEADER); // 荷載中的 IP 的頭
int iInnerIPHdrLen = ((IP_HEADER*)pInnerIpHdr)->hdr_len * 4;// 荷載中的IP 頭長度
ICMP_HEADER * pInnerIcmpHdr = (ICMP_HEADER*)(pInnerIpHdr + iInnerIPHdrLen); //荷載中的ICMP頭
usID = pInnerIcmpHdr->id;// 報文ID
usSquNo = pInnerIcmpHdr->seq; // 序列號
}
else
{
return false;
}
// 檢查 ID 和序列號以確定收到期待資料報
if (usID != (USHORT)GetCurrentProcessId() || usSquNo != DecodeResult.usSeqNo)
{
return false;
}
// 記錄 IP 地址並計算往返時間
DecodeResult.dwIPaddr.S_un.S_addr = pIpHdr->sourceIP;
DecodeResult.dwRoundTripTime = GetTickCount() - DecodeResult.dwRoundTripTime;
//處理正確收到的 ICMP 資料包
if (pIcmpHdr->type == ICMP_ECHO_REPLY || pIcmpHdr->type == ICMP_TIMEOUT)
{
// 輸出往返時間資訊
if (DecodeResult.dwRoundTripTime)
cout << " " << DecodeResult.dwRoundTripTime << "ms" << flush;
else
cout << " " << "<1ms" << flush;
}
return true;
}
char * findNextIp(char * nowIp);
int main()
{
//初始化 Windows sockets 網路環境
WSADATA wsa;//儲存被WSAStartup函式呼叫後返回的Windows Sockets資料
//使用Socket的程式在使用Socket之前必須呼叫WSAStartup函式,以後應用程式就可以呼叫所請求的Socket庫中的其他Socket函數了
WSAStartup(MAKEWORD(2, 2), &wsa);//進行相應的socket庫繫結
cout << "請輸入你要查詢的起始IP:" << endl;
char IpAddressBeg[ipAddressSize]; // 255.255.255.255
cin >> IpAddressBeg;
cout << "請輸入你要查詢的終止IP:" << endl;
char IpAddressEnd[ipAddressSize]; // 255.255.255.255
cin >> IpAddressEnd;
char nextIpAddress[17];
strcpy(nextIpAddress, IpAddressBeg);
while (strcmp(nextIpAddress, IpAddressEnd) != 0)
{
// 執行,單執行緒執行,實現後改成多執行緒
//得到IP地址
u_long ulDestIP = inet_addr(nextIpAddress);//inet_addr()的功能是將一個點分十進位制的IP轉換成一個無符號長整型數
//轉換不成功時按域名解析
if (ulDestIP == INADDR_NONE)
{
//gethostbyname()是查詢主機名最基本的函式
//如果呼叫成功,就返回一個指向hosten結構的指標
//該結構中含有對應於給定主機名的主機名字和地址資訊,用來承接域名解析的結構
hostent * pHostent = gethostbyname(nextIpAddress);
if (pHostent)//呼叫成功
{
//得到IP地址
//套了兩層,IP和ICMP,ICMP是套在IP裡面的
//h_addr返回主機IP地址
//in_addr返回報文的IP地址
//sin_addr.s_addr指向IP地址
ulDestIP = (*(in_addr*)pHostent->h_addr).s_addr;
}
else
{
cout << "輸入的 IP 地址或域名無效!" << endl;
WSACleanup();//解除與Socket庫的繫結並且釋放Socket庫所佔用的系統資源
return 0;
}
}
// 填充目的 sockaddr_in
sockaddr_in destSockAddr;//sockaddr_in是Internet環境下套接字的地址形式
//將指定的記憶體塊清零,使用結構前清零,而不讓結構體的成員數值具有不確定性,是一個好的程式設計習慣
ZeroMemory(&destSockAddr, sizeof(sockaddr_in));
destSockAddr.sin_family = AF_INET;//指代協議簇,在socket程式設計中只能是AF_INET
destSockAddr.sin_addr.S_un.S_addr = ulDestIP;//按照網路位元組順序儲存IP地址
//建立原始套接字
//WSASocket()的傳送操作和接收操作都可以被重疊使用。接收函式可以被多次呼叫,發出接收緩衝區,準備接收到來的資料。傳送函式也可以被多次呼叫,組成一個傳送緩衝區佇列
//如無錯誤發生,返回新套介面的描述字,否則的話,返回INVALID_SOCKET
//AF_INET為地址簇描述,SOCK_RAW為新套介面的型別描述,SOCK_RAW為原始套接字,可處理PING報文等
//IPPROTO_ICMP為套介面使用的協議,為ICMP;NULL是一個指向PROTOCOL_INFO結構的指標,該結構定義所建立套介面的特性
//0為套介面的描述字;WSA_FLAG_OVERLAPPED為套介面屬性描述,WSA_FLAG_OVERLAPPED表示要使用重疊模型
SOCKET sockRaw = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
WSA_FLAG_OVERLAPPED);
// 設定傳送接收超時時間,即請求超時
//比如請求B站的一個視訊,他超過一個時間沒回我,我就認為超時了
//超時時間是可能變化的,這個超時時間用來儲存在不同的變數,它剛好在一個變數而已
int iTimeout = 500;//如果沒超過超時時間就會一直等著,超過超時時間就不等了
//接收超時
//sockRaw為將要被設定或者獲取選項的套接字;SOL_SOCKET為在套接字級別上設定選項;SO_RCVTIMEO設定接收超時時間
//(char*)&iTimeout指向存放選項值的緩衝區;sizeof(iTimeout)為緩衝區的長度
setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO, (char *)&iTimeout, sizeof(iTimeout));
//傳送超時
//sockRaw為將要被設定或者獲取選項的套接字;SOL_SOCKET為在套接字級別上設定選項;SO_SNDTIMEO設定傳送超時時間
//(char*)&iTimeout指向存放選項值的緩衝區;sizeof(iTimeout)為緩衝區的長度
setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO, (char *)&iTimeout, sizeof(iTimeout));
// 構造 ICMP 回顯請求訊息, 並以TTL 遞增順序傳送報文
// ICMP 型別欄位
//採用const修飾變數,功能是對變數宣告為只讀特性,並保護變數值以防被修改
const BYTE ICMP_ECHO_REQUEST = 8;//請求回顯
const BYTE ICMP_ECHO_REPLY = 0;//回顯應答
//其他常量定義
const int DEF_ICMP_DATA_SIZE = 32; // ICMP 報文預設資料欄位長度
const int MAX_ICMP_PACKET_SIZE = 1024;//ICMP 報文最大長度(加上報頭)
const DWORD DEF_ICMP_TIMEOUT = 500;// 回顯應答超時時間
const int DEF_MAX_HOP = 20; // 最大跳站數
// 填充 ICMP 報文中每次傳送時不變的欄位
char IcmpSendBuf[sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE];// 傳送緩衝區
memset(IcmpSendBuf, 0, sizeof(IcmpSendBuf));//初始化傳送緩衝區
char IcmpRecvBuf[MAX_ICMP_PACKET_SIZE]; // 接收緩衝區
memset(IcmpRecvBuf, 0, sizeof(IcmpRecvBuf)); //初始化接收緩衝區
// 構造ICMP頭
ICMP_HEADER * pIcmpHeader = (ICMP_HEADER*)IcmpSendBuf;
pIcmpHeader->type = ICMP_ECHO_REQUEST; // 型別為請求回顯
pIcmpHeader->code = 0;//程式碼欄位為0
pIcmpHeader->id = (USHORT)GetCurrentProcessId();// ID欄位為當前程序號
memset(IcmpSendBuf + sizeof(ICMP_HEADER), 'E', DEF_ICMP_DATA_SIZE);//資料欄位
USHORT usSeqNo = 0; // ICMP 報文序列號
int iTTL = 1; // TTL初始化值為1
BOOL bReachDestHost = FALSE; // 迴圈退出標誌
int iMaxHot = DEF_MAX_HOP; // 最大迴圈數
DECODE_RESULT DecodeResult;// 傳遞給報文解碼函式的結構化引數
//int count11=0;
while (!bReachDestHost && iMaxHot--)
{
bReachDestHost = FALSE;
// 設定 IP 報頭的 TTL 欄位
//sockRaw為將要被設定或者獲取選項的套接字;IPPROTO_IP為套介面使用的協議,為IP;IP_TTL為設定IP報頭的TTL欄位
//(char*)&iTTL指向存放選項值的緩衝區;sizeof(iTTL)為緩衝區的長度
setsockopt(sockRaw, IPPROTO_IP, IP_TTL, (char *)&iTTL, sizeof(iTTL));
cout << iTTL << flush; // 輸出當前序號,flush的作用是重新整理緩衝區
// 填充 ICMP報文中每次傳送變化的欄位
((ICMP_HEADER *)IcmpSendBuf)->cksum = 0;//校驗和為0
((ICMP_HEADER *)IcmpSendBuf)->seq = htons(usSeqNo++);// 填充序列號
((ICMP_HEADER *)IcmpSendBuf)->cksum = checksum((USHORT *)IcmpSendBuf,
sizeof(ICMP_HEADER) + DEF_ICMP_DATA_SIZE); //計算校驗和
// 記錄序列號和當前時間
DecodeResult.usSeqNo = ((ICMP_HEADER*)IcmpSendBuf)->seq;//當前序號
DecodeResult.dwRoundTripTime = GetTickCount();// 當前時間
// 指定對方資訊
// 傳送 TCP 回顯請求資訊
//sendto()利用資料報的方式進行資料傳輸
// 1)指定哪個Socket發給對方 2)傳送的資料 3)flag 4)目的地址 5)目的地址的sockaddr_in結構
sendto(sockRaw, IcmpSendBuf, sizeof(IcmpSendBuf), 0, (sockaddr*)&destSockAddr, sizeof(destSockAddr));
//接收 ICMP 差錯報文並進行解析
sockaddr_in from; // 對端 socket地址,對方的
int iFromLen = sizeof(from);//地址結構大小
int iReadDataLen;// 接收資料長度
// 接收正常的話,這個迴圈只會執行一次
while (true)
{
//接收資料
//recvfrom()利用資料報方式進行資料傳輸
//當recvfrom()返回時,(sockaddr*)&from包含實際存入from中的資料位元組數。
//Recvfrom()函式返回接收到的位元組數或當出現錯誤時返回-1,並置相應的errno。
iReadDataLen = recvfrom(sockRaw, IcmpRecvBuf, MAX_ICMP_PACKET_SIZE, 0, (sockaddr*)&from, &
iFromLen);
if (iReadDataLen != SOCKET_ERROR) // 有資料到達
{
//解析資料包
// 1)接收到的Buf 2)接收到的資料長度 3)解析結果封裝到Decode 4)ICMP回顯型別 5)TIMEOUT時間
if (DecodeIcmpResponse2(IcmpRecvBuf, iReadDataLen, DecodeResult, ICMP_ECHO_REPLY, DEF_ICMP_TIMEOUT))
{
// 到達目的地,退出迴圈
//返回報文的IP地址等於輸入的IP地址
if (DecodeResult.dwIPaddr.S_un.S_addr == destSockAddr.sin_addr.S_un.S_addr)
{
bReachDestHost = true;
// 輸出 IP 地址
//inet_ntoa()功能是將網路地址轉換成“.”點隔的字串格式。
cout << '\t' << inet_ntoa(DecodeResult.dwIPaddr) << endl;
strcpy(nextIpAddress, inet_ntoa(DecodeResult.dwIPaddr));
break;
}
}
}
//WSAGetLastError()當一特定的Sockets API函式指出一個錯誤已經發生,該函式就應呼叫來獲得對應的錯誤程式碼。
//WSAETIMEDOUT在嘗試連線超時,而不建立連線。
else if (WSAGetLastError() == WSAETIMEDOUT) //接收超時,輸出*號
{
cout << " *" << '\t' << "Request timed out." << endl;
break;
}
else
{
break;
}
}
iTTL++;//遞增TTL值
}
cout << "查詢: " << nextIpAddress << "結果為 ->" << (bReachDestHost ? "線上" : "不線上") << endl;
//if nextIpAddress ==bReachDestHost;
// 向下推
strcpy(nextIpAddress, findNextIp(nextIpAddress));
}
return 0;
}
char * findNextIp(char * nowIp)
{
char nextIpAddress[ipAddressSize];
char z[4][4];
int idxIp = 0, idxj = 0;
for (int i = 0; i < strlen(nowIp); i++)
{
if (nowIp[i] == '.')
{
z[idxIp][idxj] = '\0';
idxIp++;
idxj = 0;
continue;
}
z[idxIp][idxj++] = nowIp[i];
}
z[idxIp][idxj] = '\0';
//for (int i = 0; i < 4; i++)
//{
// puts(z[i]);
//}
//cout << endl;
for (int i = 3; i >= 0; i--)
{
if (strcmp("254", z[i]) == 0)
{
strcpy(z[i], "1"); // 這裡讓ip 1-254
}
else
{
int x;
x = atoi(z[i]) + 1;
//stringstream ss;
//ss << x;
//string z[i] = ss.str();
itoa(x,z[i],10); // 第三個引數是 int的進位制
break;
}
}
char retIp[ipAddressSize];
strcpy(retIp, z[0]);
char c[2] = ".";
for (int i = 1; i < 4; i++)
{
strcat(retIp, c);
strcat(retIp, z[i]);
}
/*cout << retIp << endl;*/
return retIp;
}