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头部的初始化等操作,只是了解一下。

 struct iphdr {
#if defined(LITTLE_ENDIAN_BITFIELD)
__u8 ihl:,
version:;
#elif defined (BIG_ENDIAN_BITFIELD)
__u8 version:,
ihl:;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos;
__u16 tot_len;
__u16 id;
__u16 frag_off;
__u8 ttl;
__u8 protocol;
__u16 check;
__u32 saddr;
__u32 daddr;
/*The options start here. */
};

基本的头部信息就介绍到这里,现在说一下,发送和接收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 = ;
int retval = ;
char IPParse[];
char *service = "domain";
struct addrinfo hint ;
struct addrinfo *result, *p, *q; memset(&hint, , sizeof(hint));
hint.ai_flags = AI_CANONNAME;
hint.ai_family = AF_INET; retval = getaddrinfo(host, service, &hint, &result); if(retval != )
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 > )
*addrlen = result->ai_addrlen; i=; //虽然函数回调后会自动释放内存,但是显示的释放还是可取的
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 = ;
icmp->icmp_id = getpid();
icmp->icmp_seq = nsent++; memset(icmp->icmp_data, 0xa5, datalen);
gettimeofday((struct timeval *)icmp->icmp_data, NULL); //用这个计算RTT len = +datalen; //这里为啥要+8 ? 因为ICMP头部长度是 8 字节
icmp->icmp_cksum = ;
icmp->icmp_cksum = in_cksum((u_short *)icmp, len); sendto(sockfd, sendbuf, len, , &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[];
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) < ){
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 < ){
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) < ) {
--out->tv_sec;
out->tv_usec+=;
} out->tv_sec -= in->tv_sec;
}

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

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

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

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

Linux 网络编程基础(4) -- Ping 的C代码实现的更多相关文章

  1. 服务器编程入门(4)Linux网络编程基础API

      问题聚焦:     这节介绍的不仅是网络编程的几个API     更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系.     这节主要介绍三个方面的内容:套接字( ...

  2. Linux 高性能服务器编程——Linux网络编程基础API

    问题聚焦:     这节介绍的不仅是网络编程的几个API     更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系.     这节主要介绍三个方面的内容:套接字(so ...

  3. 第5章 Linux网络编程基础

    第5章 Linux网络编程基础 5.1 socket地址与API 一.理解字节序 主机字节序一般为小端字节序.网络字节序一般为大端字节序.当格式化的数据在两台使用了不同字节序的主机之间直接传递时,接收 ...

  4. Linux网络编程基础API

    第5章 Linux网络编程基础API 探讨Linux网络编程基础API与内核中TCP/IP协议族之间的关系,并未后续章节提供编程基础.从3个方面讨论Linux网络API. socket地址API.so ...

  5. linux高性能服务器编程 (五) --Linux网络编程基础api

    第五章 Linux网络编程基础api 1.主机字节序和网络字节序 字节序是指整数在内存中保存的顺序.字节序分为大端字节序.小端字节序. 大端字节序:一个整数的高位字节数据存放在内存的低地址处.低位字节 ...

  6. linux 网络编程 基础

    网络编程基础 套接字编程需要指定套接字地址作为参数,不同的协议族有不同的地址结构,比如以太网其结构为sockaddr_in. 通用套接字: struct sockaddr { sa_family_t ...

  7. linux网络编程基础--(转自网络)

    转自 http://www.cnblogs.com/MyLove-Summer/p/5215287.html Linux下的网络编程指的是socket套接字编程,入门比较简单. 1. socket套接 ...

  8. Linux网络编程基础

    1. Linux网络模型 ① OSI七层模型和Linux四层模型 ② 各种协议之间的关系及在Linux模型中的位置 ③ 协议封装:各种协议处于一种层层封装的关系 (1)Ethernet (2)IP * ...

  9. Linux 网络编程基础(1)--网络相关的数据结构及转化函数

    在Linux下进行网络编程,使用的语言一般为C.就个人感受而言,在Linux下进行网络程序的编写,重要的不是代码能力要多强,而是对Linux的网络编程思想的理解和对Linux网络数据结构的掌握.如果想 ...

随机推荐

  1. Oracle触发器Trigger2行级

    create table trigger_t2( id int, name ), age int ); /* --创建一个before update的触发器-控制每一行,行级 --只有行级的才会有:n ...

  2. windows 安装paramiko模块

    首先需要安装pycrypto这个模块,这个下源码编译安装的不能用报错warning: GMP or MPIR library not found; Not building这个用编译好的模块安装已编译 ...

  3. 初学DIV+CSS要记住的

    初学DIV+CSS?有六个问题需要您关注一下!作为DIV+CSS初学者,如果在动手写代码之前对网页整体结构由一个清晰认识的话,写起来会事半功倍!但是,写的过程中总是有这样那样的问题,使得我们不得不停下 ...

  4. Google搜索的配置方法

    在百度慢慢沦落为广告商的搜索引擎之后,对于一个追求技术的程序员,他所要追求的搜索引擎永远都应该是google. 下面保存一下我使用的能够FQ实现google搜索的方法和一些面试的测试账号. 小飞机sh ...

  5. Android 关于操作UI线程

    在非UI线程里访问 Android UI toolkit—这个在一个worker线程修改了 View .这会导致不可预期的结果,而且还难以调试. 为了修复这个问题,Android提供了几个方法从非UI ...

  6. 有了bootstrap,为什么还要做amaze ui

    1.Bootstrap介绍Bootstrap,来自 Twitter,是目前很受欢迎的前端框架.Bootstrap 是基于 HTML.CSS.JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加 ...

  7. Oracle 11g R2安装手册(图文教程)For Windows

    1.Oracle 11g R2安装手册(图文教程)For Windows 1.下载Oracle 11g R2 for Windows版本,下载地址如下 官方网站: http://download.or ...

  8. linux如何ARP嗅探 Linux下嗅探工具Dsniff安装记录

      先来下载依赖包 和一些必须要用到的工具 我这里用的是 dsniff-2.3 的版本 wget http://www.monkey.org/~dugsong/dsniff/dsniff-2.3.ta ...

  9. display属性值

    display属性值:none 此元素不会被显示. block 此元素将显示为块级元素,此元素前后会带有换行符. inline 默认.此元素会被显示为内联元素,元素前后没有换行符. inline-bl ...

  10. EditPlus自动执行出结果设置