最近一直在看《TCP/IP详解》这本书,也许因为我不是计算机专业的缘故,总感觉看了跟没看一样,想着写写博客,能加深印象,结果简直是尴尬。所以想着不如自己动手来实现一个Ping程序,当是学习学习吧,几年以前用Boost带的asio库实现过一个,此次不依赖任何第三方库,实现一个最基本的Ping程序。
要实现ping程序,当然要知道他的原理,ping的原理其实很简单,几乎每个系统都实现了icmp服务器,当有icmp请求到来的时候,目的端就会做出相应的回应,ping程序正是利用了这一点。通过向目的端发送回显请求报文,观察目的端是否回应请求,从而判断网络是否连通。目的端会将源端发送过来的数据包再重新发回给源端。
该程序中涉及的数据报包括IP和ICMP两种数据报,各种数据报的定义如下,在定义数据报的时候必须准确,否则目的主机会丢弃数据报,而不会给出相应的应答。
ICMP报文格式如下图所示:
ICMP源端使用的类型是0x08,表示回显请求,代码是0,ICMP目的端使用的类型是0x00,表示回显应答,代码是0。类型及代码的意义如下图所示,检验和的计算方法网络上有很,请自行查询。
详细说明请参见:
ICMP:报文控制协议
ICMP报头的定义如下所示:
其中,compute_checksum函数,用于计算ICMP报的校验和,计算方式如下:
构造函数用于在数据返回时,从目的端返回的数据缓冲区中读取出对应的ICMP报头。如下所示:
IP报文格式如下图所示:
其中的数据部分就是我们的ICMP数据报头及数据部分。详细说明请参见
IP:网际协议
其C语言定义如下:
该结构的构造函数如下所示,该构造函数将字符串转换到IP报头中
此处,由于IP并非所关注的重点,所以也就并不关心它的数据存储方式了。
编写Ping程序,大致可以分为以下几个步骤,我也将从以下几个方面来进行描述:
也许各位觉得这两个步骤我分得太简单,我这里只是站在main函数的角度来进行分析,main函数越简单越好,如下所示:
参数解析的代码如下所示,我只是粗略的实现一个Ping程序,并不是说像系统自带的Ping那样完整和完美,其实还存在一些问题,我也就不去追究和处理了。
从函数中可以看出,ping程序的使用格式是
./ping [-c count] [-m ttl] [-t timeout] [-s packagesize] host
其中,-c选项用于设置ping的次数,-m用于修改发送的IP数据报的TTL(生存时间),没有设置则使用系统默认的TTL,-t用于设置超时等待,默认为1秒。-s设置发送的数据报的大小,若没有默认为56字节。host表示要ping的目的端地址。返回true表示可以开始ping,否则表示参数有误。
该过程是一个比较复杂的过程,可以细分步骤如下:
下面我将按照这几个方面来说明。
主机地址解析,主要是从给定的域名或者IP地址中解析出主机名称和地址,代码如下所示:
我们使用gethostbyname,来获取到主机地址的相关信息,并将返回的信息转换成点分十进制的IP地址。struct hostent的定义如下所示:
其中h_name表示主机名称,h_aliases表示主机另外列表,h_addrtype表示主机的地址类型,值一般为2,表示AF_INET,h_length表示地址的长度,h_addr_list表示的是主机的IP地址列表,一般我们使用h_addr宏,表示主机IP地址列表的第一个IP地址。
套接字的类型有三种,流式套接字SOCK_STREAM,数据报套接字SOCK_DGRAM,原始套接字SOCK_RAW,其中流式套接字和数据报套接字一般使用于TCP/UDP,而ICMP协议要低于应用层,所以要想绕过应用层,我们应该使用原始套接字,定义函数SOCKET_RAW来创建套接字,并且设置TTL,如下所示:
该函数返回该套接字的描述符。
套接字建立之后,需要告诉IP层,把ICMP数据报发送给谁,所以我们应该填写IP地址信息,由struct sockaddr_in结构表示,填写格式如下:
我们可以看到,因为端口是应用层的概念,对于IP层来说,根本就不存在什么端口,所以sin_port设置为0,sin_family设置为AF_INET,地址我们已经在主机解析中解析出来。
我们在ping程序中可以看到,每次程序结束之后,都会有一个统计数据出来,我们可以注意到,不管是中断程序的执行,还是程序正常结束,都会出现统计信息,应该怎么办呢?其实在C语言里,每个程序都可以使用atexit注册最多32个结束例程,定义如下:
int atexit(void (*)(void));
但是我们可以注意到,当我们按下Ctrl + C之后,系统的Ping程序也能输出统计信息,所以我们应该捕获中断信号。
为了实现Ctrl + C输出统计信息,我们捕获SIGINT信号,代码很简单,如下所示:
这样,在信号处理函数中,我们调用统计函数并退出程序即可。
一切准备就绪,我们就可以向主机发送ICMP数据报了,但是在发送之前,我们首先要构建ICMP数据报,首先定义一个宏用于释放指针,如下所示:
然后开始构建数据报。。在构建数据报之前,我们应应该保证数据报的字节数是偶数,如果是奇数,则应该填充相应字节,所以定义MAKEEVEN宏使用户指定的包大小确保为偶数,如下所示:
现在可以开始构建ICMP数据报,如下所示:
ICMP数据报应该包括数据和报头两部分,所以我们分配MAKEEVEN(packagesize)和ICMP报头(8字节)长度的空间,数据部分并不为我们所关心,所以全部填充(char)1过去。然后设置类型为8(回显请求),代码为0,sFlag表示标识符,通常情况下我们使用进程ID来设置,sSeqNum表示包的序列号,然后计算校验和,如下所示:
在计算的时候要注意网络字节序和主机字节序的相互转换。因为在数据报中,字节流使用的是网络字节序(大端序),而在本机使用的是主机字节序(小端序),所以在计算的时候也要注意转换。
数据报构建完毕之后,我们就可以使用sendto将数据报发送给主机了。代码片断如下所示:
发送出去之后,使用gettimeofday来获取到当前的时间,gettimeofday可以精确到微秒。在本例中,我们定义无限次数为-1
#define INFINITE -1
因为次数必定大于0,但是为了防止溢出,当计数器达到最大数值的时候,就应该归0。
本例中我使用select模型,超时等待设置在select中,首先上代码片断,如下所示:
如果select函数返回-1,说明有错误发生,中止程序,如果返回0,说明已经等待超时,我们可以确定在我们所设置的时间内,主机没有返回任何数据到该套接字上。输出超时提示,否则,我们记录接收到数据报的时间,解析接收到的数据,隔一秒再次传送下一个数据报。
当我们收到回应时,数据是包含IP报头,ICMP报头,数据三个部分,其中数据部分是我们之前传送过去的数据。代码如下所示:
在这个方法里面,我们做了几件事
其实在IP数据报中已经包含了IP地址,但之前我们在解析主机地址的时候已经解析出来,所以不必再花时间去解析,设置参数将之前解析出来的地址。
在ICMP报头中,我们查看代码,对应上表中的数据,设置相应的错误提示信息。在这里我只有三种类型,回显应答,目的无法到达,TTL生存时间太短。
一切完毕,最终在退出的时候应该统计数据,如下所示:
至此,一个粗略的Ping程序就已经全部实现了。虽然还有问题,不过我也就不再去追究了。
激动人心的时刻,搞了这么久,终于完成了这样一个似模似样的Ping程序,赶快来看看结果如何,我们需要使用sudo来运行该程序,否则会提示没有权限:
Ping代码链接
本代码使用Xcode 编写。。。。