粗略实现Ping程序

最近一直在看《TCP/IP详解》这本书,也许因为我不是计算机专业的缘故,总感觉看了跟没看一样,想着写写博客,能加深印象,结果简直是尴尬。所以想着不如自己动手来实现一个Ping程序,当是学习学习吧,几年以前用Boost带的asio库实现过一个,此次不依赖任何第三方库,实现一个最基本的Ping程序。

  • Ping程序原理
  • Ping程序演示
  • 数据报格式定义
    • ICMP报头定义
    • IP报头定义
  • 程序编写
    • 参数解析
    • 开始Ping
      • 解析主机地址
      • 建立套接字
      • 填写IP地址信息
      • 设置结束统计函数
      • 捕获中断信号
      • 构建ICMP数据报
      • 发送ICMP数据报
      • 轮询套接字
      • 解析数据
      • 统计Ping数据
  • 编写Makefile
  • 效果演示
  • 代码链接

Ping程序原理

要实现ping程序,当然要知道他的原理,ping的原理其实很简单,几乎每个系统都实现了icmp服务器,当有icmp请求到来的时候,目的端就会做出相应的回应,ping程序正是利用了这一点。通过向目的端发送回显请求报文,观察目的端是否回应请求,从而判断网络是否连通。目的端会将源端发送过来的数据包再重新发回给源端。

Ping程序演示

系统自带的ping程序演示如图所示:
粗略实现Ping程序_第1张图片

数据报格式定义

该程序中涉及的数据报包括IP和ICMP两种数据报,各种数据报的定义如下,在定义数据报的时候必须准确,否则目的主机会丢弃数据报,而不会给出相应的应答。

ICMP报头定义

ICMP报文格式如下图所示:
粗略实现Ping程序_第2张图片
ICMP源端使用的类型是0x08,表示回显请求,代码是0,ICMP目的端使用的类型是0x00,表示回显应答,代码是0。类型及代码的意义如下图所示,检验和的计算方法网络上有很,请自行查询。
粗略实现Ping程序_第3张图片
详细说明请参见:
ICMP:报文控制协议

ICMP报头的定义如下所示:
粗略实现Ping程序_第4张图片
其中,compute_checksum函数,用于计算ICMP报的校验和,计算方式如下:

粗略实现Ping程序_第5张图片

构造函数用于在数据返回时,从目的端返回的数据缓冲区中读取出对应的ICMP报头。如下所示:

粗略实现Ping程序_第6张图片

IP报头定义

IP报文格式如下图所示:
粗略实现Ping程序_第7张图片
其中的数据部分就是我们的ICMP数据报头及数据部分。详细说明请参见
IP:网际协议

其C语言定义如下:
粗略实现Ping程序_第8张图片
该结构的构造函数如下所示,该构造函数将字符串转换到IP报头中
粗略实现Ping程序_第9张图片
此处,由于IP并非所关注的重点,所以也就并不关心它的数据存储方式了。

程序编写

编写Ping程序,大致可以分为以下几个步骤,我也将从以下几个方面来进行描述:

  1. 参数解析
  2. 开始Ping

也许各位觉得这两个步骤我分得太简单,我这里只是站在main函数的角度来进行分析,main函数越简单越好,如下所示:
粗略实现Ping程序_第10张图片

参数解析

参数解析的代码如下所示,我只是粗略的实现一个Ping程序,并不是说像系统自带的Ping那样完整和完美,其实还存在一些问题,我也就不去追究和处理了。

粗略实现Ping程序_第11张图片
粗略实现Ping程序_第12张图片

从函数中可以看出,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,否则表示参数有误。

开始Ping

该过程是一个比较复杂的过程,可以细分步骤如下:

  1. 解析主机地址
  2. 建立套接字socket
  3. 填写IP地址信息
  4. 设置结束统计函数
  5. 捕捉中断信号
  6. 构建ICMP数据报
  7. 发送ICMP数据报
  8. 轮询套接字
  9. 解析数据
  10. 统计Ping数据

下面我将按照这几个方面来说明。

解析主机地址

主机地址解析,主要是从给定的域名或者IP地址中解析出主机名称和地址,代码如下所示:
粗略实现Ping程序_第13张图片
我们使用gethostbyname,来获取到主机地址的相关信息,并将返回的信息转换成点分十进制的IP地址。struct hostent的定义如下所示:
粗略实现Ping程序_第14张图片
其中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,如下所示:
粗略实现Ping程序_第15张图片
该函数返回该套接字的描述符。

填写IP地址信息

套接字建立之后,需要告诉IP层,把ICMP数据报发送给谁,所以我们应该填写IP地址信息,由struct sockaddr_in结构表示,填写格式如下:
填写IP地址信息
我们可以看到,因为端口是应用层的概念,对于IP层来说,根本就不存在什么端口,所以sin_port设置为0,sin_family设置为AF_INET,地址我们已经在主机解析中解析出来。

设置结束统计函数

我们在ping程序中可以看到,每次程序结束之后,都会有一个统计数据出来,我们可以注意到,不管是中断程序的执行,还是程序正常结束,都会出现统计信息,应该怎么办呢?其实在C语言里,每个程序都可以使用atexit注册最多32个结束例程,定义如下:

int atexit(void (*)(void));

我们定义函数,用于在退出时显示统计信息:
粗略实现Ping程序_第16张图片

但是我们可以注意到,当我们按下Ctrl + C之后,系统的Ping程序也能输出统计信息,所以我们应该捕获中断信号。

捕获中断信号

为了实现Ctrl + C输出统计信息,我们捕获SIGINT信号,代码很简单,如下所示:
捕获信号
这样,在信号处理函数中,我们调用统计函数并退出程序即可。

构建ICMP数据报

一切准备就绪,我们就可以向主机发送ICMP数据报了,但是在发送之前,我们首先要构建ICMP数据报,首先定义一个宏用于释放指针,如下所示:
SAFEFREE
然后开始构建数据报。。在构建数据报之前,我们应应该保证数据报的字节数是偶数,如果是奇数,则应该填充相应字节,所以定义MAKEEVEN宏使用户指定的包大小确保为偶数,如下所示:
MAKEEVEN
现在可以开始构建ICMP数据报,如下所示:
粗略实现Ping程序_第17张图片
ICMP数据报应该包括数据和报头两部分,所以我们分配MAKEEVEN(packagesize)和ICMP报头(8字节)长度的空间,数据部分并不为我们所关心,所以全部填充(char)1过去。然后设置类型为8(回显请求),代码为0,sFlag表示标识符,通常情况下我们使用进程ID来设置,sSeqNum表示包的序列号,然后计算校验和,如下所示:
粗略实现Ping程序_第18张图片
在计算的时候要注意网络字节序和主机字节序的相互转换。因为在数据报中,字节流使用的是网络字节序(大端序),而在本机使用的是主机字节序(小端序),所以在计算的时候也要注意转换。

发送ICMP数据报

数据报构建完毕之后,我们就可以使用sendto将数据报发送给主机了。代码片断如下所示:
粗略实现Ping程序_第19张图片
发送出去之后,使用gettimeofday来获取到当前的时间,gettimeofday可以精确到微秒。在本例中,我们定义无限次数为-1

#define INFINITE -1

因为次数必定大于0,但是为了防止溢出,当计数器达到最大数值的时候,就应该归0。

轮询套接字

本例中我使用select模型,超时等待设置在select中,首先上代码片断,如下所示:
粗略实现Ping程序_第20张图片
如果select函数返回-1,说明有错误发生,中止程序,如果返回0,说明已经等待超时,我们可以确定在我们所设置的时间内,主机没有返回任何数据到该套接字上。输出超时提示,否则,我们记录接收到数据报的时间,解析接收到的数据,隔一秒再次传送下一个数据报。

解析数据

当我们收到回应时,数据是包含IP报头,ICMP报头,数据三个部分,其中数据部分是我们之前传送过去的数据。代码如下所示:
粗略实现Ping程序_第21张图片
在这个方法里面,我们做了几件事

  • 解析IP数据报头
  • 解析ICMP数据报头
  • 分析ICMP数据报头
  • 统计最大时间,最小时间,接收到的数据报(这些数据供退出时计算)

其实在IP数据报中已经包含了IP地址,但之前我们在解析主机地址的时候已经解析出来,所以不必再花时间去解析,设置参数将之前解析出来的地址。

在ICMP报头中,我们查看代码,对应上表中的数据,设置相应的错误提示信息。在这里我只有三种类型,回显应答,目的无法到达,TTL生存时间太短。

统计Ping数据

一切完毕,最终在退出的时候应该统计数据,如下所示:
粗略实现Ping程序_第22张图片
至此,一个粗略的Ping程序就已经全部实现了。虽然还有问题,不过我也就不再去追究了。

编写Makefile

Makefile的作用不言而喻,编写如下:
粗略实现Ping程序_第23张图片

效果演示

激动人心的时刻,搞了这么久,终于完成了这样一个似模似样的Ping程序,赶快来看看结果如何,我们需要使用sudo来运行该程序,否则会提示没有权限:
粗略实现Ping程序_第24张图片

代码链接

Ping代码链接
本代码使用Xcode 编写。。。。

你可能感兴趣的:(C/C++)