您的位置:首页 > 其它

ping程序实现

2017-11-28 21:51 232 查看
参考网上几篇文章进行进行实现
http://blog.csdn.net/zzucsliang/article/details/41407387 http://blog.csdn.net/tigerjibo/article/details/7356936 https://www.cnblogs.com/scrat/archive/2012/08/02/2620163.html http://blog.csdn.net/qq_33724710/article/details/51576444
//定义ICMP首部
typedef struc
4000
t _icmphdr{
unsigned char i_type; //8位类型
unsigned char i_code; //8位代码
unsigned short i_cksum; //16位校验和, 从TYPE开始,直到最后一位用户数据,如果为字节数为奇数则补充一位
unsigned short i_id ; //识别号(一般用进程号作为识别号), 用于匹配ECHO和ECHO REPLY包
unsigned short i_seq ; //报文序列号, 用于标记ECHO报文顺序
unsigned int timestamp; //时间戳
}ICMP_HEADER;


类型8,代码0:表示回显请求(ping请求)。

类型0,代码0:表示回显应答(ping应答)

类型11,代码0:超时



ip报文结构体

//定义IP首部

typedef struct _iphdr{

unsigned char h_lenver; //4 位IP版本号+4位首部长度

unsigned char tos; //8位服务类型TOS

unsigned short total_len; //16位IP包总长度(字节)

unsigned short ident; //1 6位标识, 用于辅助IP包的拆装

unsigned short frag_and_flags; //3位标志位+13位偏移位, 也是用于IP包的拆装

unsigned char ttl; //8位IP包生存时间 TTL

unsigned char proto; //8位协议 (TCP, UDP 或其他)

unsigned short checksum; //16位IP首部校验和,最初置零,等所有包头都填写正确后,计算并替换.

unsigned int sourceIP; //32位源IP地址

unsigned int destIP; //32位目的IP地址

}IP_HEADER;





ping的实现和代码分析

原创 2014年11月23日
08:39:57

标签:
多线程 /
tcp /
线程 /
网络 /
ping

4346

一.介绍

ping命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。ping命令的工作原理是:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者,这有点象潜水艇声纳系统中使用的发声装置。
例如,在Linux终端上执行ping

如下:



二.分析

由上面的执行结果可以看到,ping命令执行后显示出被测试系统主机名和相应IP地址、返回给当前主机的ICMP报文顺序号、ttl生存时间和往返时间rtt(单位是毫秒,即千分之一秒)。要写一个模拟ping命令,这些信息有启示作用。要真正了解ping命令实现原理,就要了解ping命令所使用到的TCP/IP协议。 ICMP(Internet
Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。由于IP层协议是一种点对点的协议,而非端对端的协议,它提供无连接的数据报服务,没有端口的
1b5d7
概念,因此很少使用bind()和connect()函数,若有使用也只是用于设置IP地址。

明白了工作原理,我们就可以写我们自己的ping命令:myping

1.头文件和定义函数以及变量:

[cpp] view
plain copy

/*

* 作者: greenday

* 名称: myping

* 程序应用: ping命令是向目的主机发送ICMP报文,检验本地主机和远程的目的主机是否连接

* 日期: 2014.11.22

*

*/

/*ICMP必须使用原始套接字进行设计,要手动设置IP的头部和ICMP的头部并行校验*/

/***********主函数*********************************************

myping.c*/

#include <sys/socket.h>

#include <netinet/in.h>

#include <netinet/ip.h>

#include <netinet/ip_icmp.h>

#include <unistd.h>

#include <signal.h>

#include <arpa/inet.h>

#include <errno.h>

#include <sys/time.h>

#include <stdio.h>

#include <string.h> /* bzero */

#include <netdb.h>

#include <pthread.h>

//保存发送包的状态值

typedef struct pingm_pakcet{

struct timeval tv_begin; //发送时间

struct timeval tv_end; //接收到的时间

short seq; //序列号

int flag; //1,表示已经发送但是没有接收到回应,0,表示接收到回应

}pingm_pakcet;

static pingm_pakcet *icmp_findpacket(int seq);

static unsigned short icmp_cksum(unsigned char *data, int len);

static struct timeval icmp_tvsub(struct timeval end, struct timeval begin);

static void icmp_statistics(void);

static void icmp_pack(struct icmp *icmph, int seq, struct timeval *tv,int length);

static int icmp_unpack(char *buf,int len);

static void *icmp_recv(void *argv);

static void icmp_sigint(int signo);

static void icmp_usage();

static pingm_pakcet pingpacket[128];

#define K 1024

#define BUFFERSIZE 72 //发送缓冲区的大小

static unsigned char send_buff[BUFFERSIZE];

static unsigned char recv_buff[2*K]; //防止接收溢出,设置大一些

static struct sockaddr_in dest; //目的地址

static int rawsock = 0; //发送和接收线程需要的socket描述符

static pid_t pid; //进程PID

static int alive = 0; //是否接收到退出信号

static short packet_send = 0; //已经发送的数据包数量

static short packet_recv = 0; //已经接收的数据包数量

static char dest_str[80]; //目的主机字符串

static struct timeval tv_begin, tv_end, tv_interval;


2.计算发送和接收的时间

[cpp] view
plain copy

static void icmp_usage()

{

//ping加IP地址或者域名

printf("ping aaa.bbb.ccc.ddd\n");

}

/*终端信号处理函数SIGINT*/

static void icmp_sigint(int signo)

{

alive = 0;

gettimeofday(&tv_end,NULL);

tv_interval = icmp_tvsub(tv_end, tv_begin);

return;

}

3.统计数据结果

[cpp] view
plain copy

/*统计数据结果函数******************************************

打印全部ICMP发送的接收统计结果*/

static void icmp_statistics(void)

{

long time = (tv_interval.tv_sec * 1000) + (tv_interval.tv_usec/1000);

printf("--- %s ping statistics ---\n", dest_str);

printf("%d packets transmitted, %d received, %d%c packet loss, time %ld ms\n",

packet_send,packet_recv,(packet_send-packet_recv)*100/packet_send,'%',time);

}

/*************查找数组中的标识函数***********************

查找合适的包的位置

当seq为1时,表示查找空包

其他值表示查找seq对应的包*/

static pingm_pakcet *icmp_findpacket(int seq)

{

int i;

pingm_pakcet *found = NULL;

//查找包的位置

if(seq == -1){

for(i=0;i<128;i++){

if(pingpacket[i].flag == 0){

found = &pingpacket[i];

break;

}

}

}

else if(seq >= 0){

for(i =0 ;i< 128;i++){

if(pingpacket[i].seq == seq){

found = &pingpacket[i];

break;

}

}

}

return found;

}

4.校验和函数

[cpp] view
plain copy

/*************校验和函数*****************************

TCP/IP协议栈使用的校验算法是比较经典的,对16位的数据进行累加计算,并返回计算结果,

CRC16校验和计算icmp_cksum

参数:

data:数据

len:数据长度

返回值:

计算结果,short类型

*/

static unsigned short icmp_cksum(unsigned char *data, int len)

{

int sum = 0; //计算结果

int odd = len & 0x01; //是否为奇数

/*将数据按照2字节为单位累加起来*/

while(len & 0xfffe){

sum += *(unsigned short*)data;

data += 2;

len -= 2;

}

/*判断是否为奇数个数据,若ICMP报头为奇数个字节,会剩下最后一个字节*/

if(odd){

unsigned short tmp = ((*data)<<8)&0xff00;

sum += tmp;

}

sum = (sum >> 16) + (sum & 0xffff); //高地位相加

sum += (sum >> 16); //将溢出位加入

return ~sum; //返回取反值

}

5.ICMP头部校验打包和拆包

[cpp] view
plain copy

/**********进行ICMP头部校验********************/

//设置ICMP报头

static void icmp_pack(struct icmp *icmph, int seq, struct timeval *tv, int length)

{

unsigned char i = 0;

//设置报头

icmph->icmp_type = ICMP_ECHO; //ICMP回显请求

icmph->icmp_code = 0; //code的值为0

icmph->icmp_cksum = 0; //先将cksum的值填为0,便于以后的cksum计算

icmph->icmp_seq = seq; //本报的序列号

icmph->icmp_id = pid & 0xffff; //填写PID

for(i=0; i< length; i++)

icmph->icmp_data[i] = i; //计算校验和

icmph->icmp_cksum = icmp_cksum((unsigned char*)icmph, length);

}

/*解压接收到的包,并打印信息*/

static int icmp_unpack(char *buf, int len)

{

int i,iphdrlen;

struct ip *ip = NULL;

struct icmp *icmp = NULL;

int rtt;

ip = (struct ip *)buf; //IP报头

iphdrlen = ip->ip_hl * 4; //IP头部长度

icmp = (struct icmp *)(buf+iphdrlen); //ICMP段的地址

len -= iphdrlen;

//判断长度是否为ICMP包

if(len < 8){

printf("ICMP packets\'s length is less than 8\n");

return -1;

}

//ICMP类型为ICMP_ECHOREPLY并且为本进程的PID

if((icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id == pid)){

struct timeval tv_interval,tv_recv,tv_send;

//在发送表格中查找已经发送的包,按照seq

pingm_pakcet *packet = icmp_findpacket(icmp->icmp_seq);

if(packet == NULL)

return -1;

packet->flag = 0; //取消标志

tv_send = packet->tv_begin; //获取本包的发送时间

gettimeofday(&tv_recv,NULL); //读取此时间,计算时间差

tv_interval = icmp_tvsub(tv_recv,tv_send);

rtt = tv_interval.tv_sec * 1000 + tv_interval.tv_usec/1000;

/*打印结果包含

ICMP段的长度

源IP地址

包的序列号

TTL

时间差

*/

printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%d ms\n",

len,inet_ntoa(ip->ip_src),icmp->icmp_seq,ip->ip_ttl,rtt);

packet_recv ++; //接收包数量加1

}

else {

return -1;

}

}

6.计算时间差函数

[cpp] view
plain copy

/************计算时间差time_sub************************

参数:

end:接收到时间

begin:开始发送的时间

返回值:

使用的时间

*/

static struct timeval icmp_tvsub(struct timeval end, struct timeval begin)

{

struct timeval tv;

//计算差值

tv.tv_sec = end.tv_sec - begin.tv_sec;

tv.tv_usec = end.tv_usec - begin.tv_usec;

//如果接收的时间的usec值小于发送时的usec,从uesc域借位

if(tv.tv_usec < 0){

tv.tv_sec --;

tv.tv_usec += 1000000;

}

return tv;

}

7.发送报文函数

[cpp] view
plain copy

//**********发送报文***************************

static void *icmp_send(void *argv)

{

//保存程序开始发送数据的时间

gettimeofday(&tv_begin, NULL);

while(alive){

int size = 0;

struct timeval tv;

gettimeofday(&tv, NULL); //当前包的发送时间

//在发送包状态数组中找到一个空闲位置

pingm_pakcet *packet = icmp_findpacket(-1);

if(packet){

packet->seq = packet_send;

packet->flag = 1;

gettimeofday(&packet->tv_begin,NULL);

}

icmp_pack((struct icmp *)send_buff,packet_send,&tv, 64);

//打包数据

size = sendto(rawsock, send_buff,64,0,(struct sockaddr *)&dest, sizeof(dest));

if(size < 0){

perror("sendto error");

continue;

}

packet_send ++;

//每隔1s发送一个ICMP回显请求包

sleep(1);

}

}

8.接收目的主机的回复函数

[cpp] view
plain copy

//***********接收ping目的主机的回复***********

static void *icmp_recv(void *argv)

{

//轮询等待时间

struct timeval tv;

tv.tv_usec = 200;

tv.tv_sec = 0;

fd_set readfd;

//当没有信号发出一直接收数据

while(alive){

int ret = 0;

FD_ZERO(&readfd);

FD_SET(rawsock,&readfd);

ret = select(rawsock+1,&readfd,NULL,NULL,&tv);

switch(ret)

{

case -1:

//错误发生

break;

case 0:

//超时

break;

default :

{

//收到一个包

int fromlen = 0;

struct sockaddr from;

//接收数据

int size = recv(rawsock,recv_buff,sizeof(recv_buff),0);

if(errno == EINTR){

perror("recvfrom error");

continue;

}

//解包

ret = icmp_unpack(recv_buff,size);

if(ret == 1){

continue;

}

}

break;

}

}

}

9.设置ICMP头部(程序中不需要,这里只是作为了解)

[cpp] view
plain copy

/**********设置ICMP发送报文的头部*********************************

回显请求的ICMP报文

*/

/*struct icmp

{

u_int8_t icmp_type; //消息类型

u_int8_t icmp_code; //消息类型的子码

u_int16_t icmp_cksum; //校验和

union

{

struct ih_idseq //显示数据报

{

u_int16_t icd_id; //数据报ID

u_int16_t icd_seq; //数据报的序号

}ih_idseq;

}icmp_hun;

#define icmp_id icmp_hun.ih_idseq.icd_id;

#define icmp_seq icmp_hun.ih_idseq.icd_seq;

union

{

u_int8_t id_data[1]; //数据

}icmp_dun;

#define icmp_data icmp_dun.id_data;

}; */

10.主函数

[cpp] view
plain copy

//主程序

int main(int argc, char const *argv[])

{

struct hostent *host = NULL;

struct protoent *protocol = NULL;

char protoname[] = "icmp";

unsigned long inaddr = 1;

int size = 128*K;

if(argc < 2) //参数是否数量正确

{

icmp_usage();

return -1;

}

//获取协议类型

protocol = getprotobyname(protoname);

if(protocol == NULL)

{

perror("getprotobyname()");

return -1;

}

//复制目的地址字符串

memcpy(dest_str, argv[1],strlen(argv[1])+1);

memset(pingpacket, 0, sizeof(pingm_pakcet) * 128);

//socket初始化

rawsock = socket(AF_INET, SOCK_RAW, protocol->p_proto);

if(rawsock < 0){

perror("socket");

return -1;

}

pid = getuid(); //为与其他线程区别,加入pid

//增大接收缓冲区,防止接收包被覆盖

setsockopt(rawsock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

bzero(&dest, sizeof(dest));

//获取目的地址的IP地址

dest.sin_family = AF_INET;

//输入的目的地址为字符串IP地址

inaddr = inet_addr(argv[1]);

if(inaddr == INADDR_NONE){ //输入的是DNS地址

host = gethostbyname(argv[1]);

if(host == NULL){

perror("gethostbyname");

return -1;

}

//将地址复制到dest

memcpy((char *)&dest.sin_addr, host->h_addr, host->h_length);

} //IP地址字符串

else {

memcpy((char *)&dest.sin_addr, &inaddr,sizeof(inaddr));

}

//打印提示

inaddr = dest.sin_addr.s_addr;

printf("PING %s (%ld.%ld.%ld.%ld) 56(84) bytes of data.\n",

dest_str,(inaddr&0x000000ff)>>0,(inaddr&0x0000ff00)>>8,(inaddr&0x00ff0000)>>16,(inaddr&0xff000000)>>24);

//截取信号SIGINT,将icmp_sigint挂接上

signal(SIGINT,icmp_sigint);

/*发送数据并接收回应

建立两个线程,一个用于发数据,另一个用于接收响应数据,主程序等待两个线程运行完毕后再进行

下一步,最后对结果进行统计并打印

*/

alive = 1; //初始化可运行

pthread_t send_id, recv_id; //建立两个线程,用于发送和接收

int err = 0;

err = pthread_create(&send_id, NULL, icmp_send, NULL); //发送

if(err < 0){

return -1;

}

err = pthread_create(&recv_id, NULL, icmp_recv, NULL); //接收

if(err < 0){

return -1;

}

//等待线程结束

pthread_join(send_id, NULL);

pthread_join(recv_id, NULL);

//清理并打印统计结果

close(rawsock);

icmp_statistics();

return 0;

}

三.程序运行方法

由于在程序中用到了<pthread.h>多线程,所以在编译时要加上-lpthread,并且想要运行的话要在root权限下,一般的用户是没有权限的

此程序编译方法为

[cpp] view
plain copy

gcc -o myping myping.c -lpthread

运行:

[cpp] view
plain copy

./myping www.baidu.com

结果为:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: