Ping笔记(一)

Ping的c语言实现

Ping和ICMP

Ping简介:

ping 命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具 ping类似于声呐系统: ping 命令使用回显请求和回显应答消息。具体表现是向网络上的另一个主机系统发送 ICMP 报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者 ping 命令所使用到的 TCP/IP 协议:ICMP 协议 ping执行后的返回内容 : 1.显示出被测试系统主机名和相应 IP 地址 111.13.101.208 (111.13.101.208) 2.返回给当前主机的 ICMP 报文顺序号 icmp_seq=3 3.ttl 生存时间和往返时间 rtt(单位是毫秒,即千分之一秒) ttl=51 time=16.6 ms 以ping baidu.com为例:

64 bytes from 111.13.101.208 (111.13.101.208): icmp_seq=1 ttl=51 time=16.4 ms 64 bytes from 111.13.101.208 (111.13.101.208): icmp_seq=2 ttl=51 time=17.3 ms 64 bytes from 111.13.101.208 (111.13.101.208): icmp_seq=3 ttl=51 time=16.6 ms

ICMP介绍

ICMP 是(Internet Control Message Protocol)Internet 控制报文协议。它是 TCP/IP 协议族的一个子协议,用于在 IP 主机、路由器之间传递控制消息。 - 控制消息有:目的不可达消息,超时信息,重定向消息,时间戳请求和时间戳响应消息,回显请求和回显应答消息 注意ICMP协议是-IP层-的一个协议,。这是因为ICMP报文在发送给报文接收方时可能要经过若干子网,会牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送,是IP层的

ICMP报文格式:

1.回显 请求 报文其中类型为 0,代码为 0。 2.回显 应答 报文其中类型为 8,代码为 0。 3.校验和字段:包括数据在内的整个 ICMP 协议数据包的校验和,具体实现方法会在下面详细介绍。 4.标识符字段:用于唯一标识 ICMP 报文,本项目使用程序的进程 id。因为如果同时在两个命令行终端执行 ping 命令的话, 每个 ping 命令都会接收到所有的回显应答,所以需要根据标识符来判断回显应答是否应该接收。 5.序号字段:ICMP 报文的序号。 6.数据字段:也就是报文,本项目中我们将发送报文的时间戳放入数据字段,这样当接收到该报文应答的时候可以取出发送时间戳, 将接收应答的时间戳减去发送时间戳就是报文往返时间(rtt)。提前预告一下,这里使用gettimeofday()API函数获取时间戳,详细介绍会在函数介绍部分说明。 - c语言数据结构表示: ICMP 报文 C 语言实现可以用下面的数据结构表示:

struct icmp{ unsigned char type; // 类型 unsigned char code; // 代码 unsigned short checksum; // 校验和 unsigned short id; // 标识符 unsigned short sequence; // 序号 struct timeval timestamp; // 时间戳 };

unsigned char 正好一字节,也就是 8bit,unsigned short 二字节,也就是 16bit,unsigned int4 字节(32bit), 不过上面没用到。其中 struct timeval 类型可能有人不清楚,不过没关系,函数部分说明。

  • IP报文首部C语言实现的数据结构表示 > struct ip{ unsigned char version:4; // 版本 unsigned char hlen:4; // 首部长度 unsigned char tos; // 服务类型 unsigned short len; // 总长度 unsigned short id; // 标识符 unsigned short offset; // 标志和片偏移 unsigned char ttl; // 生存时间 unsigned char protocol; // 协议 unsigned short checksum; // 校验和 struct in_addr ipsrc; // 32位源ip地址 struct in_addr ipdst; // 32位目的ip地址 };

地址信息表示

编写网络应用程序时,要使用地址信息指定数据传输给哪个主机

  • 地址信息内容 1.地址族,基于IPv4的地址族还是IPv6的地址族。 2.IP地址。 3.端口号。

地址信息的结构体(一)

struct sockaddr_in{ sa_family_t sin_family; // 地址族 uint16_t sin_port; // 端口号 struct in_addr sin_addr; // 32位IP地址 char sin_zero[8]; // 不使用 };

其中struct in_addr结构体定义如下:

struct in_addr{ in_addr_t s_addr; // 32位IP地址 };

in_addr_t使用如下宏指令定义,也就是无符号整型32位。

#define in_addr_t uint32_t

地址信息的结构体(二)

struct sockaddr{ sa_family_t sin_family; // 地址族 char sa_data[14]; // IP地址和端口 };

成员sa_data保存的信息包含 IP 地址和端口号,剩余部分填充0。

  • 网络编程中常用struct sockaddr_in,因为填充数据更方便
  • 网络编程接口函数定义使用的是struct sockaddr,因为它是最先开始使用的
  • 两个结构体之间的转换: 定义地址信息时使用 struct sockaddr_in 结构体,然后将该结构体类型转为 struct sockaddr 结构体类型传递给网络编程接口函数

字节顺序的转换

主机字节序与网络字节序

在不同 CPU 中,4字节整型数值1在内存空间的保存方式是不同的。 4字节整型数值1可用二进制表示如下:

00000000 00000000 00000000 00000001

而有些CPU则以倒序保存

00000001 00000000 00000000 00000000

所以,如果发送主机与接收主机CPU字节序不一样则就会出现问题。 引申上面的问题,这就涉及到CPU解析数据的方式,其方式有2种: - 大端序(Big Endian):高位字节存放到低位地址。 - 小端序(Little Endian):高位字节存放到高位地址。由于不同CPU字节序不一样,因此,在通过网络传输数据时约定统一方式,这种约定称为网络字节序(Network Byte Order),非常简单——统一为大端序。 所以,进行网络传输数据前,需要先把数据数组转化为 大端序 格式再进行网络传输。接收到网络数据后,需要转换本机字节序格式然后进行后续处理。不过,幸运地是这些工作不需要我们自己完成,系统会自动转换的。

用到字节序转换的地方

我们唯一需要转换的是向struct sockaddr_in结构体变量填充IP地址和端口号数据的时候。当然,系统已经提供了一些函数,只需调用相应函数即可。 转换字节序的函数有:

unsigned short htons(unsigned short); unsigned short ntohs(unsigned short); unsigned long htonl(unsigned long); unsigned long ntohl(unsigned long);

上面的函数非常简单,通过函数名就能知道它们的功能, htonl/htons 中的h代表主机(host)字节序,n代表网络(network)字节序,s指的是 short,l指的是 long (需要注意一下,linux 中 long 类型只占4个字节,跟int类型一样)。 示例:

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <arpa/inet.h>

int main(void){
  unsigned short hosts = 0x1234;
  unsigned short nets;
  unsigned long hostl = 0x12345678;
  unsigned long netl;

  nets = htons(hosts);
  netl = htonl(hostl);

  printf("Host ordered short: %#x \n", hosts);
  printf("Network ordered short: %#x \n", nets);

  printf("Host ordered long: %#lx \n", hostl);
  printf("Network ordered long: %#lx \n", netl);

  return 0;
}

大家通过上面的程序也可以判断自己主机是大端序的还是小端序的,Intel 系列 CPU 采用的都是小端序标准

函数介绍

函数gettimeofday()

#include <sys/time.h> int gettimeofday(struct timeval *tv, struct timezone *tz); 该函数的作用是把当前的时间放入 struct timeval 结构体中返回。

  • 注意: 1.精确级别,微秒级别 2.受系统时间修改影响 3.返回的秒数是从1970年1月1日0时0分0秒开始

其参数 tv 是保存获取时间结果的结构体,参数 tz 用于保存时区结果。 结构体 timeval 的定义为:

struct timeval { long int tv_sec; // 秒数 long int tv_usec; // 微秒数 }

结构体timezone的定义为:

struct timezone { int tz_minuteswest;/格林威治时间往西方的时差/ int tz_dsttime; /DST 时间的修正方式/ }

timezone 参数若不使用则传入0即可,本项目传入0。

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 本程序实现计算程序运行时间
*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>

// 计算时间差,单位:ms
float timediff(struct timeval *begin,
      struct timeval *end){
  int n;
  // 先计算两个时间点相差多少微秒
  n = ( end->tv_sec - begin->tv_sec ) * 1000000
      + ( end->tv_usec - begin->tv_usec );

  // 转化为毫秒返回
  return (float) (n / 1000);
}

int main(void){
  struct timeval begin, end;

  gettimeofday(&begin, 0);

  // 这里让程序挂起一秒
  printf("do something here...");
  sleep(1);

  gettimeofday(&end, 0);

  printf("running time : %fms\n", timediff(&begin, &end));

  return 0;
}

大约都运行了1秒钟时间。但有时候多1毫秒,有时候又恰好1秒。这是因为系统中运行的程序不只本程序一个,有时候恰好遇到内核进行进程切换需要时间

inet_addr 函数

#include <arpa/inet.h> in_addr_t inet_addr(const char *string); 该函数的作用是将用点分十进制字符串格式表示的IP地址转换成32位大端序整型。 成功时返回32位大端序整型数值,失败时返回 INADDR_NONE 。 简单的示例:

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <arpa/inet.h>

int main(void){
  char *addr1 = "1.2.3.4";
  char *addr2 = "192.168.1.1";

  in_addr_t data;

  data = inet_addr(addr1);
  printf(" %s -> %#lx \n", addr1, (long)data );

  data = inet_addr(addr2);
  printf(" %s -> %#lx \n", addr2, (long)data );

  return 0;
}

inet_ntoa 函数

char * inet_ntoa(struct in_addr addr); 该函数的作用与 inet_addr 正好相反。将32位大端序整型格式IP地址转换为点分十进制格式。 成功时返回转换的字符串地址值,失败时返回-1。 简单的示例

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <arpa/inet.h>

int main(void){
  struct in_addr addr1, addr2;
  char * str1, * str2;

  addr1.s_addr = htonl(0x1020304);
  addr2.s_addr = htonl(0xc0a80101);

  str1 = inet_ntoa(addr1);
  str2 = inet_ntoa(addr2);

  printf("%#lx -> %s \n", (long)addr1.s_addr, str1);
  printf("%#lx -> %s \n", (long)addr2.s_addr, str2);

  return 0;
}

函数两次执行的结果怎么是一样的,其实这是 C 语言编程中常会出现的陷阱。 我们可以再看下上面函数的定义,注意该函数返回值类型为 char 指针类型,大家看出端倪了吗? inet_addr函数在执行过程中,在内部会申请内存并保存结果字符串,然后返回内存地址。所以调用完该函数应该将字符串复制到其他内存空间。 因为再次调用该函数时,之前保存的字符串很有可能被覆盖。知道了原因,大家可以动手修改上面的代码了。 首先定义一个接收缓冲区 char buf[20]; ,然后使用 memcpy(buf, str1, sizeof(str)) 将 inet_ntoa 函数生成的字符串保存缓冲区。 记住,memcpy 函数在头文件 #include <string.h> 中声明的,需要加上这个头文件。

函数 gethostbyname –根据域名获取IP地址

#include <netdb.h> struct hostent * gethostbyname(const char * hostname); 成功时返回hostent结构体地址,失败时返回NULL指针。 struct hosten结构体定义如下:

struct hostent{ char * h_name; char ** h_aliases; char h_addrtype; char h_length; char ** h_addr_list; };

我们最关心的是h_addr_list成员,它保存的就是域名对应IP地址。由于一个域名对应的IP地址不止一个,所以h_addr_list成员是char **类型,相当于二维字符数组。 下面通过一张图来表示h_addr_list成员的参数,希望大家可以加深对hostent结构体的理解。 图片描述信息 简单的示例

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>

int main(int argc, char *argv[]){
  int i;
  struct hostent *host;

  if(argc < 2){
      printf("Use : %s <hostname> \n", argv[0]);
      exit(1);
  }

  host = gethostbyname(argv[1]);

  for(i = 0; host->h_addr_list[i]; i++){
      printf("IP addr %d : %s \n", i+1,
              inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
  }

  return 0;
}

上面代码中,打印IP地址是出现了令人困惑的类型转换。 host->h_addr_list[i]得到的是字符串指针,但该字符串指针实际指向的是struct in_addr结构体变量地址值。 (struct in_addr *)host->h_addr_list[i]将字符串指针转换为struct in_addr结构体指针。 由于inet_ntoa参数类型是struct in_addr而不是struct in_addr ,所以用运算符取出struct in_addr结构体的值。 Ping笔记(二)

参考文献

参考文献一