您的位置:首页 > 其它

ping工具模拟实现

2017-10-08 23:16 447 查看


PING工具实现


关于ping程序

用于确定本地主机与网络中其它主机的网络通信情况,常使用ping程序。ping程序向指定的IP地址发送ICMP数据包,通过返回的信息来判断网络的连接状况。 

ping程序的返回信息中有一个值为TTL(time to live),表示ping程序发送的icmp数据包的生存周期,每经过一个网段,TTL的值减1,当其值被减为0时,该数据包将被丢弃,但该数据包的源地址将会被告知情况,以重新发送该数据包。另外,不同操作系统的TTL值是不相同的,linux操作系统是64。


ICMP协议

ICMP(Internet Control Message Protocol),即网际控制报文协议,可用在网络中实现主机探测、路由维护、路由选择和流量控制。 

由于IP协议没有机制来获取网络错误信息以及没有对错误进行处理,所以需要另一协议来解决这一问题,这个协议就是ICMP协议。ICMP常被认为是IP层的一部分,用于传输差错报文及控制报文。ICMP报文是封装在IP数据报内部的。


模拟实现流程图




涉及结构体&函数接口

struct sockaddr_in :此数据结构用做bind、connect、recvfrom、sendto等函数的参数,指明地址信息。
       sa_family是地址家族,一般都是“AF_xxx”的形式。通常大多用的是都是AF_INET,代表TCP/IP协议族;
sin_port存储端口号(使用网络字节顺序);
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节;

struct protoent :相关函数: getprotobyname, getprotoent, setprotoent, endprotoent
         p_name: 网络协议名
p_aliases: 别名
p_proto: 网络协议编号

struct hostent
     hostent是host entry的缩写,该结构记录主机的信息,包括主机名、别名、地址类型、地址长度和地址列表
注:其所需要的头文件还有这一宏定义 #define h_addr h_addr_list[0]

struct imcp



getprotobyname(const char *protoname)

        getprotobyname()会返回一个protoent结构,参数protoname为欲查询的网络协议名。此函数会从 /etc/protocols中查找符合条件的数据并由结构protoent返回。

gethostbyname()
          gethostbyname()返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针。

socket :函数原型 int socket(int domain, int type, int protocol);
          第一个参数指定应用程序使用的通信协议的协议族,对于TCP/IP协议族,该参数置AF_INET;
第二个参数指定要创建的套接字类型,流套接字类型为SOCK_STREAM、数据报套接字类型为SOCK_DGRAM、原始套接字SOCK_RAW;
第三个参数指定应用程序所使用的通信协议;
该函数如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该套接字描述符表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构,另外需知套接字数据结构是在操作系统的内核缓冲里的。

setsockopt():用于任意类型、任意状态套接口的设置选项值
        函数原型:int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd:标识一个套接口的描述字
level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
optname:需设置的选项
optval:指针,指向存放选项值的缓冲区
optlen:optval缓冲区长度

inet_addr():将一个点分十进制的IP转换成一个长整数型数
sendto():经socket传送数据
    函数原型:int sendto ( socket s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen);
函数说明:sendto() 用来将数据由指定的socket传给对方主机。
参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作;
参数msg指向欲连线的数据内容,参数flags 一般设0,详细描述请参考send();
参数to用来指定欲传送的网络地址,结构sockaddr请参考bind();
参数tolen为sockaddr的结果长度;
返回值:成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。

recvfrom():经socket接收数据
        函数原型:ssize_t recvfrom(int sockfd,void *buf,int len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);ssize_t 相当于 int,socket_t 相当于int ,这里用这个名字为的是提高代码的自说明性。
参数说明:
s:标识一个已连接套接口的描述。
buf:接收数据缓冲区
len:缓冲区长度
flags:调用操作方式 
from:(可选)指针,指向装有源地址的缓冲区
fromlen:(可选)指针,指向from缓冲区长度值
返回值:如果正确接收返回接收到的字节数,失败返回0。

代码

#include <signal.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <setjmp.h>
#include <errno.h>

#define PACKET_SIZE 1024
#define MAX_WAIT_TIME 5
#define MAX_NO_PACKETS 4
#define DATA_LEN 56

int nsend = 0; // 发送的数据包编号
int nreceived = 0; // 接收的数据包编号

char sendpacket[PACKET_SIZE]; // 发送的数据包
char recvpacket[PACKET_SIZE]; // 接收的数据包

struct sockaddr_in from;
struct timeval tvrecv; // 收取数据包时间

void send_packet(int sfd, pid_t pid, struct sockaddr_in dst_addr); // 发送数据包
void recv_packet(int sfd, pid_t pid); // 接收数据包
void statistics(int signo); // 输出统计信息

int main( int argc, char *argv[] )
{
pid_t pid;
int sockfd;
int size = 50*1024;
int waittime = MAX_WAIT_TIME;
in_addr_t inaddr = 1;

struct hostent* host;
struct sockaddr_in dst_addr;

if ( argc < 2 ) { // 输入目标主机名或IP
fprintf(stderr, "usage:%s hostname/IP \n", argv[0]);
exit(EXIT_FAILURE);
}

if ( (sockfd=socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) == -1) // 创建socket
perror("socket"),exit(EXIT_FAILURE);

dst_addr.sin_family = AF_INET;
if ( (inaddr=inet_addr(argv[1])) == INADDR_NONE ) { // 如果不为INADDR_NONE 则是ip,否则是域名
if ((host=gethostbyname(argv[1])) == NULL ) // 如果是域名,先DNS解析该域名
perror("gethostbyname"),exit(EXIT_FAILURE);
memcpy((char*)&(dst_addr.sin_addr), host->h_addr, host->h_length);
} else { // ip
dst_addr.sin_addr.s_addr = inaddr;
}

printf("ping %s(%s):%d bytes data in ICMP packet.\n", argv[1],
inet_ntoa(dst_addr.sin_addr), DATA_LEN);

pid = getpid();

send_packet(sockfd, pid, dst_addr); // 发送数据包
recv_packet(sockfd, pid); // 接收数据包
statistics(SIGALRM); // 输出统计信息

close(sockfd);
}

// 计算校验和
int cal_chksum(unsigned short* addr, int len)
{
int nleft = len;
int sum = 0;
unsigned short *w = addr;
unsigned short answer = 0;

while ( nleft > 1 ) {
sum += *w++;
nleft -= 2;
}

if ( nleft == 1 ) {
*(unsigned char *)(&answer) = *(unsigned char *)w;
sum += answer;
}

sum = (sum>>16) + (sum&0xffff);
sum += (sum>>16);
answer = ~sum;

return answer;
}

int pack(int pack_no, int pid)
{
int i, packsize;
struct icmp *icmp;
struct timeval *tval;

icmp = (struct icmp*)sendpacket; // 封装ICMP包头
icmp->icmp_type = ICMP_ECHO; // 协议
icmp->icmp_code = 0; // ECHO
icmp->icmp_cksum = 0; // 校验值
icmp->icmp_seq = pack_no; // 包序号
icmp->icmp_id = pid; // ID,使用pid唯一标识,以区分同主机多个ICMP
packsize = 8+DATA_LEN; // 长度
tval = (struct timeval*)icmp->icmp_data; // 数据,为当前的时间,用于计算延时
gettimeofday(tval, NULL);
icmp->icmp_cksum = cal_chksum((unsigned short*)icmp, packsize); // 计算校验和

return packsize;
}

// 封装数据包并发送代码
void send_packet(int sfd, pid_t pid, struct sockaddr_in dst_addr)
{
int packetsize;

while ( nsend < MAX_NO_PACKETS ) {
nsend++;
packetsize = pack(nsend, pid);
if ( sendto(sfd, sendpacket, packetsize,0, (struct sockaddr*)&dst_addr, sizeof(dst_addr)) == -1) {
perror("sendto");
continue;
}
sleep(1);
}
}

// 两个时间值减法
void tv_sub(struct timeval *out, struct timeval *in)
{
if ( (out->tv_usec -= in->tv_usec) < 0 ) {
--out->tv_sec;
out->tv_usec += 1000000;
}
out->tv_sec -= in->tv_sec;
}

// 解包
int unpack(char *buf, int len, int pid)
{
int i;
int iphdrlen;
double rtt;
struct ip *ip;
struct icmp *icmp;
struct timeval *tvsend;

ip = (struct ip*)buf; // IP数据头
iphdrlen = ip->ip_hl<<2; // IP数据长度
icmp = (struct icmp*)(buf+iphdrlen); // icmp数据位置

len -= iphdrlen;
if ( len < 8 ) { // 如果 icmp数据小于8byte,出错
printf("icmp packet\'s length is less than 8\n");
return -1;
}

// 如果是ICMP_ECHOREPLY并且是回复给本进程
if ( (icmp->icmp_type == ICMP_ECHOREPLY) && (icmp->icmp_id==pid) ) {
tvsend = (struct timeval*)icmp->icmp_data; // 获取此数据发送时间
tv_sub(&tvrecv, tvsend); // 计算发送与接收时间差
rtt = tvrecv.tv_sec * 1000 + tvrecv.tv_usec/1000; // 计算差值
printf("%d byte from %s:icmp_seq=%u ttl=%d rtt=%.3f ms\n",
len, inet_ntoa(from.sin_addr), icmp->icmp_seq, ip->ip_ttl, rtt);
return 0;
} else {
return -1;
}
}

// 接收数据包
void recv_packet(int sfd, pid_t pid)
{
int n;
socklen_t fromlen;
extern int errno;

signal(SIGALRM, statistics);// 安装SIGALRM, 到指定时间计算,无论收到多少包
fromlen = sizeof(from);

while ( nreceived < nsend ) {
alarm(MAX_WAIT_TIME); // 设置等待时间
if( (n=recvfrom(sfd, recvpacket, sizeof(recvpacket), 0, (struct sockaddr*)&from, &fromlen)) == -1 ) {
if ( errno == EINTR ) continue;
perror("recvfrom");
continue;
}
gettimeofday(&tvrecv, NULL); // 获取数据包的时间
if ( unpack(recvpacket, n, pid) == -1 ) // 解包
continue;
nreceived++;
}
}

void statistics(int signo)
{
printf("\n-----------------------------PING statistics----------------------\n");
printf("%d packets transmitted, %d received, %%%f lost\n", nsend, nreceived, ((nsend-nreceived)*1.0) / (nsend*100));
exit(EXIT_SUCCESS);
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: