Linux网络编程之原始套接字-ping协议实现

时间:2022-12-18 00:28:49

转自http://blog.csdn.net/chenjin_zhong/article/details/7271830


1.概述

PING协议是用来检验本地主机与远程主机是否连接,发送的是ICMP ECHO_REQUEST包。普通的套接字是基于TCP或者是UDP的,无法发送ICMP包,所以必须用原始套接字来实现。PING协议的客户端类型值为8,代码值为0,表示请求。而PING协议的响应端类型值为0,代码值也为0,表示应答. 以太网数据部分的最小值为46字节,而IP首部占20个字节,ICMP的首部占8个字节,所以PING的数据部分至少为4字节。

2.实现细节

主机端:

(1)创建原始套接字 socket(AF_INET,SOCK_RAW,htons(proto)),能够直接得到IP包

(2)填写ICMP首部和数据部分,即icmp_type(8),icmp_code(0)和icmp_data部分

(3)封装后发送ICMP请求包

响应端:

(1)创建原始套接字socket(AF_INET,SOCK_RAW,htons(proto))

(2)填写ICMP首部和数据部分,即icmp_type(0),icmp_code(0),icmp_data

(3)发送ICMP响应包

主机端收到ICMP响应包之后,即原始的IP包,将收到包的时间减去包的发送时间就可以得到响应时延。


3. PING协议的实现例子

#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 <string.h>
#include <netdb.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
/**
通过原始套接字发送ICMP回显请求报文来实现ping协议
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_6 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[i];//数据
 

}icmp_dun;
#define icmp_data icmp_dun.id_data

}

**/
typedef struct pingm_packet{
    struct timeval tv_begin;//发送的时间
    struct timeval tv_end;//接收到响应包的时间
    short seq;//序号值
    int flag;//1表示已经发送但没有接收到回应包,0表示接收到响应包

}pingm_packet;
//保存已经发送包的状态值
static pingm_packet pingpacket[128];//定义一个包数组
static pingm_packet *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_send(void*argv);
static void icmp_sigint(int signo);
static void icmp_usage();
#define K 1024
#define BUFFERSIZE 512
static unsigned char send_buff[BUFFERSIZE];//定义发送缓冲区的大小
static unsigned char recv_buff[2*K];//定义接收缓冲区的大小,为防止接收端溢出,接收缓冲区稍微大一些
static struct sockaddr_in dest;//目的地址
static int rawsock=0;//原始套接字描述符
static pid_t pid=0;//进程id
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;//本程序开始发送,结束时间和时间间隔
static void icmp_usage(){

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


}
//计算ICMP首部校验和
static unsigned short icmp_cksum(unsigned char* data,int len){
 int sum=0;
 int odd=len&0x01;
unsigned short *value=(unsigned short*)data;
while(len&0xfffe){
  sum+=*(unsigned short*)data;
  data+=2;
  len-=2;
}


if(odd){
  unsigned short tmp=((*data)<<8)&0xff00;
  sum+=tmp;
}
sum=(sum>>16)+(sum&0xffff);
sum+=(sum>>16);
return ~sum;
}


//设置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值
icmph->icmp_seq=htons(seq);//数据报的序列号
icmph->icmp_id=htons(pid&0xffff);//数据报的ID
 for(i=0;i<length;i++){
   icmph->icmp_data[i]=htons(i);//注意主机字节序转换成网络字节序
 }//发送的数据
 //计算校验和
icmph->icmp_cksum=icmp_cksum((unsigned char*)icmph,length+8);

}
//计算时间差函数
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;
  if(tv.tv_usec<0){
     tv.tv_sec--;
     tv.tv_usec+=1000000;
 }
  return tv;

}

//发送报文
static void *icmp_send(void *argv){
   struct timeval tv;
   tv.tv_usec=0;
   tv.tv_sec=1;//每隔一秒发送报文
   gettimeofday(&tv_begin,NULL);//保存程序开始发送数据的时间
   while(alive){
  
    memset(send_buff,0,sizeof(send_buff));
    int size=0;
    struct timeval tv;
    gettimeofday(&tv,NULL);//当前包发送的时间
    icmp_pack((struct icmp*)send_buff,packet_send,&tv,203);//packet_send为发送包的序号,发送的数据长度为64个字节,填充ICMP首部信息
    size=sendto(rawsock,send_buff,203+8,0,(struct sockaddr*)&dest,sizeof(dest));//dest为ICMP包发送的目的地址
    if(size<0){
     perror("sendto error");
     continue;
   }
  else{
  //在发送包的状态数组找一空闲位置记录发送状态信息
  pingm_packet *packet=icmp_findpacket(-1);
  if(packet){
     packet->seq=packet_send;
     packet->flag=1;
     gettimeofday(&packet->tv_begin,NULL);
     packet_send++;//发送序号+1
  }

 }
 sleep(1);
}


}

//寻找一个空闲位置,seq=-1表示空闲位置
static pingm_packet* icmp_findpacket(int seq){
   int i=0;
   pingm_packet* found=NULL;
  if(seq==-1){
    for(i=0;i<128;i++){
     if(pingpacket[i].flag==0){
      found=&pingpacket[i];
      break;
    }
   }

}else if(seq>=0){//查找对应seq的数据包
    for(i=0;i<128;i++){
        if(pingpacket[i].seq==seq){
              found=&pingpacket[i];
        break;
    }
   }

 }


return found;

}

//获得ICMP接收报文,buf存放的除去了以太网部分的IP数据报文,len为数据长度,ip_hl标识IP头部长度以4字节为单位,获得ICMP数据报后判断是否为ICMP_ECHOREPLY并检查是否为本进程的ID
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;
  iphdrlen=ip->ip_hl*4;//IP头部长度
  icmp=(struct icmp*)(buf+iphdrlen);//ICMP报文的地址
  len-=iphdrlen;//ICMP报文的长度,ICMP报文至少8个字节
  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;
 //在发送数组中查找已经发送的包
pingm_packet*packet=icmp_findpacket(ntohs(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
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;
}

}
//接收报文

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;
  tv.tv_usec=200;//轮循时间
  tv.tv_sec=0;
  FD_ZERO(&readfd);
  FD_SET(rawsock,&readfd);
  ret=select(rawsock+1,&readfd,NULL,NULL,&tv);
  int fromlen=0;
  struct sockaddr from;
  switch(ret){
case -1://发生错误
break;
case 0://超时
 //printf("timeout\n");
break;
default://收到数据包
 fromlen=sizeof(from);
 int size=recvfrom(rawsock,recv_buff,sizeof(recv_buff),0,(struct sockaddr*)&from,&fromlen);//利用原始套接字,原始套接字与IP层网络协议栈核心打交道
 if(errno==EINTR){
   perror("recvfrom error");
 }
//解包,得到RTT
 ret=icmp_unpack(recv_buff,size);
 if(ret==-1){
   continue;
 }
break;
}
}

}
//统计数据结果,成功发送的报文数量,成功接收的报文数量,丢失报文百分比和程序总共运行时间
static void icmp_statistics(void){
  long time=(tv_interval.tv_sec*1000)+(tv_interval.tv_usec/1000);
  printf("--- %s ping statistics ---\n",dest_str);//目的IP
  printf("%d packets transmitted, %d recevied, %d%c packet loss, time %d ms\n",packet_send,packet_recv,(packet_send-packet_recv)*100/packet_send,'%',time);
}

//信号处理函数
static void icmp_sigint(int signo){
   alive=0;//alive=0程序将会终止
   gettimeofday(&tv_end,NULL);//程序结束时间
   tv_interval=icmp_tvsub(tv_end,tv_begin);//计算程序一共运行了多长时间
  return;
}


//主函数实现
int main(int argc,char*argv[]){
  struct hostent *host=NULL;
  struct protoent*protocol=NULL;
  char protoname[]="icmp";
  unsigned long inaddr=1;
  int size=128*K;
  int ret;
 if(argc<2){
   icmp_usage();
   return -1;
 }
//获取协议类型ICMP,协议类型的值作为设置原始套接字的第3个参数,type类型下的具体协议值不止一个,当type为SOCK_RAW
protocol=getprotobyname(protoname);
if(protocol==NULL){
  perror("getprotobyname()");
  return -1;
}

//复制目的地址
memcpy(dest_str,argv[1],strlen(argv[1])+1);
memset(pingpacket,0,sizeof(pingm_packet)*128);//pingpacket数组初始化
//建立原始套接字
rawsock=socket(AF_INET,SOCK_RAW,protocol->p_proto);
if(rawsock<0){
  perror("raw sock error");
  return -1;
}
//得到程序的pid
pid=getuid();
//增大接收端缓冲区防止接收的包被覆盖
ret=setsockopt(rawsock,SOL_SOCKET,SO_RCVBUF,&size,sizeof(size));
if(ret==-1){
   perror("SO_RCVBUF ERROR");
   return -1;
}
//输入的目的IP
inaddr=inet_addr(argv[1]);//转换成二进制IP
bzero(&dest,sizeof(dest));
dest.sin_family=AF_INET;//设置地址族
if(inaddr==INADDR_NONE){
  //输入的是DNS
    host=gethostbyname(argv[1]);
    if(host==NULL){
     perror("gethostbyname");
    return -1;
  }

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

}

else{

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

}

inaddr=dest.sin_addr.s_addr;
//由于是ICMP不涉及到端口绑定
printf("PING %s (%d.%d.%d.%d) 56(84)bytes of data.\n",dest_str,(inaddr&0x000000FF)>>0,(inaddr&0x0000FF00)>>8,(inaddr&0x00FF0000)>>16,(inaddr&0xFF000000)>>24);
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);//等待子线程结束send
pthread_join(recv_id,NULL);//等待子线程的结束recv
close(rawsock);
icmp_statistics();
return 0;
}

运行结果:

PING 222.27.253.1 (222.27.253.1) 56(84)bytes of data.
60 byte from 222.27.253.1:icmp_seq=0 ttl=255 rtt=5 ms
60 byte from 222.27.253.1:icmp_seq=1 ttl=255 rtt=12 ms
60 byte from 222.27.253.1:icmp_seq=2 ttl=255 rtt=5 ms
60 byte from 222.27.253.1:icmp_seq=3 ttl=255 rtt=7 ms
60 byte from 222.27.253.1:icmp_seq=4 ttl=255 rtt=2 ms
60 byte from 222.27.253.1:icmp_seq=5 ttl=255 rtt=23 ms
60 byte from 222.27.253.1:icmp_seq=6 ttl=255 rtt=27 ms
60 byte from 222.27.253.1:icmp_seq=7 ttl=255 rtt=10 ms
60 byte from 222.27.253.1:icmp_seq=8 ttl=255 rtt=17 ms
60 byte from 222.27.253.1:icmp_seq=9 ttl=255 rtt=3 ms
60 byte from 222.27.253.1:icmp_seq=10 ttl=255 rtt=6 ms
60 byte from 222.27.253.1:icmp_seq=11 ttl=255 rtt=10 ms
60 byte from 222.27.253.1:icmp_seq=12 ttl=255 rtt=3 ms
60 byte from 222.27.253.1:icmp_seq=13 ttl=255 rtt=2 ms
60 byte from 222.27.253.1:icmp_seq=14 ttl=255 rtt=2 ms
60 byte from 222.27.253.1:icmp_seq=15 ttl=255 rtt=6 ms
60 byte from 222.27.253.1:icmp_seq=16 ttl=255 rtt=8 ms
60 byte from 222.27.253.1:icmp_seq=17 ttl=255 rtt=9 ms
60 byte from 222.27.253.1:icmp_seq=18 ttl=255 rtt=25 ms
60 byte from 222.27.253.1:icmp_seq=19 ttl=255 rtt=24 ms
--- 222.27.253.1 ping statistics ---
20 packets transmitted, 20 recevied, 0% packet loss, time 19249 ms


说明:程序用了两个线程,一个是用来发送ICMP,另一个是用来接收ICMP.


总结:本文主要介绍了基于原始套接字的ICMP协议的实现,最后给出了简单PING协议的例子.