网络协议 -- ICMP协议(2) Ping程序
一、Ping实现原理
大多数系统都已经在内核中内置了ping服务器的功能,所以不需要单独的其他进程来接收主机的ping请求。
windows系统下,输入ping /?
命令查看ping的用法如下:
用法: ping [-t] [-a] [-n count] [-l size] [-f] [-i TTL] [-v TOS]
[-r count] [-s count] [[-j host-list] | [-k host-list]]
[-w timeout] [-R] [-S srcaddr] [-4] [-6] target_name
选项:
-t Ping 指定的主机,直到停止。
若要查看统计信息并继续操作 - 请键入 Control-Break;
若要停止 - 请键入 Control-C。
-a 将地址解析成主机名。
-n count 要发送的回显请求数。
-l size 发送缓冲区大小。
-f 在数据包中设置“不分段”标志(仅适用于 IPv4)。
-i TTL 生存时间。
-v TOS 服务类型(仅适用于 IPv4。该设置已不赞成使用,且
对 IP 标头中的服务字段类型没有任何影响)。
-r count 记录计数跃点的路由(仅适用于 IPv4)。
-s count 计数跃点的时间戳(仅适用于 IPv4)。
-j host-list 与主机列表一起的松散源路由(仅适用于 IPv4)。
-k host-list 与主机列表一起的严格源路由(仅适用于 IPv4)。
-w timeout 等待每次回复的超时时间(毫秒)。
-R 同样使用路由标头测试反向路由(仅适用于 IPv6)。
-S srcaddr 要使用的源地址。
-4 强制使用 IPv4。
-6 强制使用 IPv6。
其中-r
参数用于记录跃点路由,类似tracert的功能,但他们的实现方式不一样,ping是通过在IP头部“选项”字段记录经过的每个路由的IP来实现记录路由功能的,这种实现有个限制,就是IP首部“选项”字段的最大字节数为40字节,所以最多只能记录10个IP。
tracert的实现方式见:http://blog.csdn.net/china_jeffery/article/details/79142031
ping功能通过网络协议 – ICMP协议(1) 报文格式中介绍的ICMP的回显请求和回显应答来实现,也就是说ping是基于ICMP协议实现的。
ICMP回显请求和回显应答的报文格式如下:
- 标识符:在实现中,一般将该字段设置为当前进程ID。这样即使在同一台主机上同时运行了多个ping程序实例, ping程序也可以识别出返回的信息属于哪个进程。
- 序号:序号一般从0开始(没有强制性,从任何数字开始都可以),每发送一次新的回显请求就加1。因为ICMP是在IP数据报内部被传输的,而IP协议又是不可靠、无连接的,所以ping程序打印出返回的每个分组的***,方便我们查看是否有分组丢失、失序或重复。
- 选项:在“选项”字段中,我们一般放入发送时间戳,这样在收到回应的时候可以用来计算本次ping的耗时。我们经常会指定ping包的大小,所以也会在“选项”字段中填充一些废数据来让包达到一定大小,在下面的
FillPingPacket
函数就有这样的实现。
二、C++代码实现
完整代码见https://gitee.com/china_jeffery/webrtc项目中的
webrtc/src/msvc/test/ping
工程。
2.1 定义ICMP、ping首部
networkprotocolheader.h
头文件中定义了IP协议、ICMP协议等协议的首部结构体。
#pragma pack(1)
#define __u8 unsigned char
#define __u16 unsigned short
#define __u32 unsigned long
// See: http://blog.csdn.net/china_jeffery/article/details/79045630
//
struct icmp_common_hdr {
__u8 type;
__u8 code;
__u16 check;
/*Other content start here. */
};
struct ping_header {
icmp_common_hdr common_hdr;
__u16 id;
__u16 seq;
__u32 timestamp;
};
#pragma pack()
2.2 程序执行参数
DECLARE_bool(h); // 帮助
DECLARE_bool(t); // ping指定的主机直到停止
DECLARE_int(w); // 等待每次回复的超时时间(毫秒)
DECLARE_int(s); // 发送ping包超时时间(毫秒)
DECLARE_int(l); // 发送缓冲区大小
DECLARE_int(i); // TTL
DEFINE_bool(h, false, "帮助");
DEFINE_bool(t, false, "ping指定的主机直到停止");
DEFINE_int(w, 3000, "等待每次回复的超时时间(毫秒)");
DEFINE_int(s, 3000, "发送ping包超时时间(毫秒)");
DEFINE_int(l, 32, "发送缓冲区大小");
DEFINE_int(i, 128, "TTL");
ping程序的执行参数的定义和解析由webrtc的"rtc_base/flags.h"
支持,具体使用方法见:WebRTC-命令行参数解析。
2.3 完整代码
代码中的某些功能,如参数解析、断言、时间戳等基于webrtc的rtc_base
实现,这些功能也可以很方便的自己实现。
rtc_base
的使用参考:http://blog.csdn.net/china_jeffery/article/details/78887619
另外,使用原始套接字需要管理员权限,如果需要绕开管理员权限,可以使用windows提供的IcmpSendEcho
系列函数。
在发送ping请求的时候,我们只封装了一个ICMP报文,并没有自己手动添加IP头,封装IP报文。因为内核会自动添加IP头,如果想自己添加IP头,可以调用setsockopt
设置IP_HDRINCL
选项,告诉内核由我们自己来封装IP头。
#include <WinSock2.h>
#include <ws2spi.h>
#include <ws2tcpip.h>
#include "rtc_base/networkprotocolheader.h"
#include "rtc_base/checks.h"
#include "rtc_base/flags.h"
#include "rtc_base/timeutils.h"
DECLARE_bool(h); // 帮助
DECLARE_bool(t); // ping指定的主机直到停止
DECLARE_int(w); // 等待每次回复的超时时间(毫秒)
DECLARE_int(s); // 发送ping包超时时间(毫秒)
DECLARE_int(l); // 发送缓冲区大小
DECLARE_int(i); // TTL
DEFINE_bool(h, false, "帮助");
DEFINE_bool(t, false, "ping指定的主机直到停止");
DEFINE_int(w, 3000, "等待每次回复的超时时间(毫秒)");
DEFINE_int(s, 3000, "发送ping包超时时间(毫秒)");
DEFINE_int(l, 32, "发送数据大小");
DEFINE_int(i, 128, "TTL");
void FillPingPacket(__u8* icmp_packet, __u16 seq, __u16 icmp_packet_size) {
RTC_DCHECK(icmp_packet);
ping_hdr* pping_hdr = reinterpret_cast<ping_hdr*>(icmp_packet);
pping_hdr->common_hdr.type = 8;
pping_hdr->common_hdr.code = 0;
pping_hdr->id = (__u16)GetCurrentProcessId();
pping_hdr->seq = seq;
__u32 now = rtc::Time32();
memcpy((icmp_packet + sizeof(ping_hdr)), &now, sizeof(__u32));
// fill some junk in the buffer.
int junk_data_size = FLAG_l - sizeof(__u32); // timestamp
int junk_offset = icmp_packet_size - junk_data_size;
if(junk_data_size > 0)
memset((icmp_packet + junk_offset), 'E', junk_data_size);
pping_hdr->common_hdr.check = 0;
pping_hdr->common_hdr.check = rtc::GetCheckSum(reinterpret_cast<__u16*>(icmp_packet), icmp_packet_size);
}
void DecodeIPPacket(__u8* ip_packet, __u16 packet_size) {
iphdr* ip_hdr = reinterpret_cast<iphdr*>(ip_packet);
__u32 now = rtc::Time32();
__u16 ip_hdr_len = ip_hdr->ihl * 4; // bytes
ping_hdr *pping_hdr = reinterpret_cast<ping_hdr*>(ip_packet + ip_hdr_len);
if (pping_hdr->common_hdr.type != 0 || pping_hdr->common_hdr.code != 0) {
printf("non-echo response, type=%d, code=%d\n", pping_hdr->common_hdr.type, pping_hdr->common_hdr.code);
return;
}
if (pping_hdr->id != (__u16)GetCurrentProcessId()) {
printf("other process ping response packet, pid=%d\n", GetCurrentProcessId());
return;
}
__u32 timestamp = 0;
memcpy(×tamp, reinterpret_cast<__u32*>((__u8*)pping_hdr + sizeof(ping_hdr)), sizeof(__u32));
in_addr from;
from.s_addr = ip_hdr->saddr;
printf("%d bytes from %s, time < %d ms, icmp_seq = %d, TTL = %d \n",
packet_size - ip_hdr_len - sizeof(ping_hdr),
inet_ntoa(from),
now - timestamp,
pping_hdr->seq,
ip_hdr->ttl
);
}
int main(int argc, char**argv)
{
rtc::FlagList::SetFlagsFromCommandLine(&argc, argv, true);
if (FLAG_h) {
rtc::FlagList::Print(NULL, false);
return 1;
}
char *hostname = argv[argc - 1];
if (!hostname || strlen(hostname) == 0) {
printf("Invalid host name\n");
return 1;
}
if (FLAG_l <= 4) {
return 1;
}
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD(2, 2);
WSAStartup(wVersionRequested, &wsaData);
sockaddr_in from;
int from_len = sizeof(sockaddr_in);
sockaddr_in dest;
memset(&dest, 0, sizeof(sockaddr_in));
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = inet_addr(hostname);
// resolve host name
if (dest.sin_addr.s_addr == INADDR_NONE) {
unsigned long begin_time = rtc::Time32();
struct addrinfo* result = nullptr;
struct addrinfo hints = { 0 };
hints.ai_family = AF_UNSPEC;
hints.ai_flags = AI_ADDRCONFIG;
int ret = getaddrinfo(hostname, nullptr, &hints, &result);
if (ret != 0) {
printf("Resolve host name failed, error code = %d\n", ret);
return 1;
}
unsigned long end_time = rtc::Time32();
struct addrinfo* cursor = result;
printf("------------------------------\n");
printf("Resolve [time < %d ms]: \n", end_time - begin_time);
bool flag = false;
for (; cursor; cursor = cursor->ai_next) {
sockaddr_in *paddr_in = reinterpret_cast<sockaddr_in *>(cursor->ai_addr);
printf("%s\n", inet_ntoa(paddr_in->sin_addr));
if (!flag) {
dest.sin_addr = paddr_in->sin_addr;
flag = true;
}
}
freeaddrinfo(result);
printf("-------------------------------\n");
}
printf("Ping %s [TTL %d]: \n", inet_ntoa(dest.sin_addr), FLAG_i);
// socket函数需要管理员权限
// 需要绕开管理员权限,可以使用IcmpSendEcho系列函数
//
SOCKET s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (s == INVALID_SOCKET) {
printf("create socket failed, error code = %d\n", WSAGetLastError());
WSACleanup();
return 1;
}
int err = setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&FLAG_s), sizeof(FLAG_s));
RTC_DCHECK(err != SOCKET_ERROR);
if (err == SOCKET_ERROR) {
printf("setsockopt for SO_SNDTIMEO failed, error code = %d\n", WSAGetLastError());
closesocket(s);
WSACleanup();
return 1;
}
err = setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&FLAG_w), sizeof(FLAG_w));
RTC_DCHECK(err != SOCKET_ERROR);
if (err == SOCKET_ERROR) {
printf("setsockopt for SO_RCVTIMEO failed, error code = %d\n", WSAGetLastError());
closesocket(s);
WSACleanup();
return 1;
}
err = setsockopt(s, IPPROTO_IP, IP_TTL, reinterpret_cast<const char*>(&FLAG_i), sizeof(FLAG_i));
RTC_DCHECK(err != SOCKET_ERROR);
if (err == SOCKET_ERROR) {
printf("setsockopt for IP_TTL failed, error code = %d\n", WSAGetLastError());
closesocket(s);
WSACleanup();
return 1;
}
// ping request
int icmp_packet_size = sizeof(ping_hdr)
+ FLAG_l; // data
__u8 *icmp_packet = new __u8[icmp_packet_size];
RTC_DCHECK(icmp_packet);
// ping response
__u16 ip_packet_size = icmp_packet_size + 20; // 20 bytes ip header, no option.
__u8 *ip_packet = new __u8[ip_packet_size];
RTC_DCHECK(ip_packet);
if (!icmp_packet || !ip_packet) {
closesocket(s);
WSACleanup();
return 1;
}
__u16 i = 0;
while (true) {
if (i == 0xFFFF)
i = 0;
i++;
if (!FLAG_t) {
if(i > 4)
break;
}
FillPingPacket(icmp_packet, i, icmp_packet_size);
int sent = sendto(s,
reinterpret_cast<const char*>(icmp_packet),
icmp_packet_size,
0,
reinterpret_cast<const sockaddr*>(&dest),
sizeof(sockaddr));
if (sent == SOCKET_ERROR) {
int gle = WSAGetLastError();
if (gle == WSAETIMEDOUT) {
printf("request timeout\n");
continue;
}
else {
printf("ping %s failed, error code = %d\n", inet_ntoa(dest.sin_addr), gle);
break;
}
}
if (sent < FLAG_l) {
printf("warning, sent %d bytes\n", sent);
}
int bread = recvfrom(s,
reinterpret_cast<char*>(ip_packet),
ip_packet_size,
0,
reinterpret_cast<sockaddr*>(&from),
&from_len);
if (bread == SOCKET_ERROR) {
int gle = WSAGetLastError();
if (gle == WSAETIMEDOUT) {
printf("receive timeout\n");
continue;
}
else {
printf("ping %s failed, error code = %d\n", inet_ntoa(dest.sin_addr), gle);
break;
}
}
if (bread < ip_packet_size) {
printf("too few bytes from %s\n", inet_ntoa(from.sin_addr));
continue;
}
DecodeIPPacket(reinterpret_cast<__u8*>(ip_packet), ip_packet_size);
Sleep(1000);
}
delete [] icmp_packet;
delete [] ip_packet;
if (s != INVALID_SOCKET) {
closesocket(s);
}
WSACleanup();
return 0;
}
运行效果: