Linux 网络编程基础(4) -- Ping 的C代码实现

1、背景

  在进行网络编程的时候,通常使用的协议有TCP协议,UDP协议。这些协议在简历套接字之初需要制定套接字的类型,比如TCP应当设置为 SOCK_STREAM,

UDP对应的套接字应当设置为SOCK_DGRAM。但是这些套接字并非能够提供网络所需的全部功能,我们还需要其他的套接字,比如原始套接字OCK_RAW。原始

套接字可以提供SOCK_STREAM和SOCK_DGRAM所不及的能力。比如:

(1)有了原始套接字,进程可以读取ICMPV4、ICMPV6、IGMP等的分组。正如ping所使用的套接字,就是SOCK_RAW类型的。这样使得使用ICMP和IGMP的程

完全能够作为用户进程处理,而无需向内核添加代码。

(2)有了原始套接字,进程可以处理内核不处理其协议字段的IPV4数据报。

(3)有了原始套接字,进程使用IP_HDRINCL套接字选项定制自己的IPV4头部。

  当然,上述的三个功能,并不是本文都要涉及的;只关注第一个能力,编写代码,实现ping程序。

2、基本使用

  a.定义原始套接字与定义其他套接字没有形式上的巨大差别。

  int sockfd;

  sockfd = socket(AF_INET, SOCK_RAW, protocol);

  protocol 的值是型为 IPPROTO_XXX的量,这些量定义在<netinet/in.h>中,比如ping使用的 IPPROTO_ICMP(关于IPV6的实现,再后续补充)。

  只有超级用户才可以创建SOCK_RAW类型的套接字。

  b. 原始套接字并不存在端口的概念。可以在原始套接字上调用bind函数,但是这么做并不常见。bind函数会设置发送数据报的源IP地址,如果没有使用

  bind函数,那么内核将出发的借口地址作为源地址。

  c. 同样,一般不会使用connect函数,connect函数会指定目的地址,但是因为原始套接字不存在端口概念,所以connect函数并不重要了。

  使用sendto函数发送原始套接字的数据。

3、ping简介

  ping是检查网络是否通畅或者网络连接速度的命令,它的原理是想服务端发送一个定长的数据包,再要求对方返回一个同样大小的数据包来确定两台网络机器

是否连接相通,延迟是多少。

     先看一下,ip协议的头部和ICMP协议的头部。

  IP头部一般在ICMP协议中没有附加数据,所以IP头部的长度为20字节,ICMP头部的长度为8字节,规定ICMP数据包的数据部分为56字节,那么最终使用的

ICMP的数据包的长度为64字节。

 

ICMP的头部数据结构:

在Linux中定义了不同协议的头部信息,对于ICMP协议的头部定义如下</include/linux/icmp.h>:

struct icmphdr {
  unsigned char        type;     //标记当前ICMP数据包的类型 ECHO、ECHOREPLY等等。
  unsigned char        code;     //code取值与type协同,或者对type具体化
  unsigned short   checksum;   //校验和
  union {
    struct {
        unsigned short    id;
        unsigned short    sequence;
    } echo;
    unsigned long gateway;
  } un;
};

      这个ICMP的头部可能与最新的版本不太一致,但是基本结构是类似的。从这个结构可以计算出来,对于32位的机器来说,这个头部的长度就是8字节。

IP头部如果不考虑附加信息的话,固定长度应该为20字节。当然在ping代码中,我们并不考虑IP头部的初始化等操作,只是了解一下。

 1 struct iphdr {
 2 #if defined(LITTLE_ENDIAN_BITFIELD)
 3     __u8    ihl:4,
 4         version:4;
 5 #elif defined (BIG_ENDIAN_BITFIELD)
 6     __u8    version:4,
 7           ihl:4;
 8 #else
 9 #error    "Please fix <asm/byteorder.h>"
10 #endif
11     __u8     tos;
12     __u16    tot_len;
13     __u16    id;
14     __u16    frag_off;
15     __u8     ttl;
16     __u8     protocol;
17     __u16    check;
18     __u32    saddr;
19     __u32    daddr;
20     /*The options start here. */
21 };

       基本的头部信息就介绍到这里,现在说一下,发送和接收ICMP数据包的过程。

  对于一个ICMP数据包来说,我们要做的大致分为这么几步:

  1、申请一个空间sendbuf,对这个缓冲区内容的更改结果,作为我们生成的ICMP数据包。

  2、在写入ICMP的头部以后,将这个缓冲区的内容交给原始套接字发送,并等待服务端的ICMP回复。

  3、收到返回的数据放入recvbuf中,此时收到的数据,是包含了IP头部的,我们首先解析IP头部,然后解析ICMP头部,最后解析数据包的内容。

  4、循环往复上述步骤。

  说起来容易做起来就就不容易了。现在一步一步的做吧。

 

      首先,我们要发送一个ICMP数据包,那目的IP地址要知道的,但是一般IP都记不住的,而且有的服务器IP地址有很多,所以我们一般都是通过域名来获取IP

地址。相对gethostent()函数,getaddrinfo()更加优秀一些,怎么个优秀法,咱们别处再说,在这里只说怎么获取的。代码丑陋,将就看。

void GetAddrInfo(char *host, char *canonname, char *ip_char, int *sockfamily, 
socklen_t *addrlen, struct sockaddr *send_addr){ int i = 0; int retval = 0; char IPParse[20]; char *service = "domain"; struct addrinfo hint ; struct addrinfo *result, *p, *q; memset(&hint, 0, sizeof(hint)); hint.ai_flags = AI_CANONNAME; hint.ai_family = AF_INET; retval = getaddrinfo(host, service, &hint, &result); if(retval != 0) return ; printf("We successfully get access to the Host %s\n",host); if(result->ai_flags == AI_CANONNAME){ printf("AI_CANONNAMW\n"); strcpy(canonname, result->ai_canonname); } if(result->ai_family == AF_INET){ printf("AF_INET\n"); *sockfamily = result->ai_family; } if(result->ai_addr){ inet_ntop(result->ai_family, (void *)result->ai_addr, ip_char, result->ai_addrlen); *send_addr = *(result->ai_addr); // CANONNAME 对应的IP地址 printf("IP Address We Get Is %s\n",ip_char); } if(result->ai_addrlen > 0) *addrlen = result->ai_addrlen; i=0; //虽然函数回调后会自动释放内存,但是显示的释放还是可取的 p = result; q = p->ai_next; while(p) { q = p->ai_next; free(p); p = q; i++; } printf("There are %d Address Here\n",i); }

  运行这个函数,会发现result链表中的IP地址都一样,这是咋回事呢?这与getaddrinfo()中的参数hint有关,怎么个关系,不讨论。我们知道这个函数可以

获得一个可用的IP地址结构,可以用来进行ICMP数据包的发送就好了。

  然后,现在有一个IP地址了,那就可以建立一个原始套接字了。

sockfd = socket(sockfamily, SOCK_RAW, IPPROTO_ICMP);
setuid(getuid());                  //so we get the root authority

  现在,地址有了,套接字有了,再申请个空间,套上ICMP的头部,我们就可以给服务器发过去了。

void send_ipv4(void)
{
    int len;
   int datalen = 56;
struct icmp *icmp;
   char sendbuf[1500]; icmp
= (struct icmp *)sendbuf; icmp->icmp_type = ICMP_ECHO; icmp->icmp_code = 0; icmp->icmp_id = getpid(); icmp->icmp_seq = nsent++; memset(icmp->icmp_data, 0xa5, datalen); gettimeofday((struct timeval *)icmp->icmp_data, NULL); //用这个计算RTT len = 8+datalen; //这里为啥要+8 ? 因为ICMP头部长度是 8 字节 icmp->icmp_cksum = 0; icmp->icmp_cksum = in_cksum((u_short *)icmp, len); sendto(sockfd, sendbuf, len, 0, &out_send_addr, out_addr_len); }

  ok,到此基本上发送端都搞定了,这里面有一个函数in_cksum()用于校验,这个函数网上哪儿哪儿都是,感兴趣的就搜一下。要是能弄明白为啥加来加去

然后各种与还有移位,那就更好了。

  发送结束后,服务器会回复相应的数据包,然后咱们就可以拿着这个数据包计算我们想要的数据了,怎么处理呢?看代码。。

void proc_ipv4(char *recvbuf,ssize_t recv_len, struct msghdr *msg, struct timeval *time_recv)
{
    int ip_hdr_len;
   char recvbuf[1500];
int icmplen; struct ip *ip; struct icmp *icmp; struct timeval *time_send; char char_seq[20]; double rtt; ip = (struct ip*)recvbuf; //收到的是包含IP头部的数据包 ip_hdr_len = ip->ip_hl>>2; //这么做是有道理的,看看IP数据包的头部就知道了。 if(ip->ip_p != IPPROTO_ICMP){ printf("This Packet Is Not A ICMP \n"); return; } icmp = (struct icmp *)(recvbuf + ip_hdr_len); if((icmplen = recv_len - ip_hdr_len) < 8){ printf("Malformed Packet\n"); return; } if(icmp->icmp_type == ICMP_ECHOREPLY){ if(icmp->icmp_id != getpid()){ printf("This Packet belong to other Process"); return; } if(icmplen < 16){ printf("No Enough Data For processing\n"); return; } time_send = (struct timeval*)icmp->icmp_data; tv_sub(time_recv, time_send); rtt =time_recv->tv_sec * 1000.0 + time_recv->tv_usec/1000.0; printf("Recv %d Bytes from %s: Seq: %d ttl = %d, rtt: %.3f ms\n", icmplen, inet_ntop(AF_INET, (void *)msg->msg_name, char_seq,msg->msg_namelen ), icmp->icmp_seq, ip->ip_ttl, rtt); } else { printf("Fail To Process\n"); } }

  这里面用到了一个计算时间差的函数tv_sub(),具体实现是这样的,比较适合重复利用。

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;
}

 

  现在,主要的代码部分都有了,至于怎么循环发送很多包,怎么依次接收,要不要定时器等等,这些都仁者见仁智者见智了,方法很多,就不再赘述了。

上面的代码都是参考<<Unix 网络编程: 卷1>>编写的,没有关注IPV6的部分,因为初始入门,或者为了探究ICMP和原始套接口,例子越简单越好。

    其实从本文可以看出,原始套接字并不复杂,复杂的还是程序设计、协议理解。真正的ICMP协议可不是这么实现的,看看Linux源代码就知道了,那里面的实现

数据结构是基于sk_buff,icmp_hdr,ip_hdr等等。咱们写的这个也就是玩玩而已,这个代码实现了ICMP协议的五分之一功能吧,不会再多了。

 

 

转载于:https://www.cnblogs.com/tju-gsp/p/3780458.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值