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头部的初始化等操作,只是了解一下。
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代码实现的更多相关文章
- 服务器编程入门(4)Linux网络编程基础API
问题聚焦: 这节介绍的不仅是网络编程的几个API 更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系. 这节主要介绍三个方面的内容:套接字( ...
- Linux 高性能服务器编程——Linux网络编程基础API
问题聚焦: 这节介绍的不仅是网络编程的几个API 更重要的是,探讨了Linux网络编程基础API与内核中TCP/IP协议族之间的关系. 这节主要介绍三个方面的内容:套接字(so ...
- 第5章 Linux网络编程基础
第5章 Linux网络编程基础 5.1 socket地址与API 一.理解字节序 主机字节序一般为小端字节序.网络字节序一般为大端字节序.当格式化的数据在两台使用了不同字节序的主机之间直接传递时,接收 ...
- Linux网络编程基础API
第5章 Linux网络编程基础API 探讨Linux网络编程基础API与内核中TCP/IP协议族之间的关系,并未后续章节提供编程基础.从3个方面讨论Linux网络API. socket地址API.so ...
- linux高性能服务器编程 (五) --Linux网络编程基础api
第五章 Linux网络编程基础api 1.主机字节序和网络字节序 字节序是指整数在内存中保存的顺序.字节序分为大端字节序.小端字节序. 大端字节序:一个整数的高位字节数据存放在内存的低地址处.低位字节 ...
- linux 网络编程 基础
网络编程基础 套接字编程需要指定套接字地址作为参数,不同的协议族有不同的地址结构,比如以太网其结构为sockaddr_in. 通用套接字: struct sockaddr { sa_family_t ...
- linux网络编程基础--(转自网络)
转自 http://www.cnblogs.com/MyLove-Summer/p/5215287.html Linux下的网络编程指的是socket套接字编程,入门比较简单. 1. socket套接 ...
- Linux网络编程基础
1. Linux网络模型 ① OSI七层模型和Linux四层模型 ② 各种协议之间的关系及在Linux模型中的位置 ③ 协议封装:各种协议处于一种层层封装的关系 (1)Ethernet (2)IP * ...
- Linux 网络编程基础(1)--网络相关的数据结构及转化函数
在Linux下进行网络编程,使用的语言一般为C.就个人感受而言,在Linux下进行网络程序的编写,重要的不是代码能力要多强,而是对Linux的网络编程思想的理解和对Linux网络数据结构的掌握.如果想 ...
随机推荐
- iOS中有关配置 Apache 服务器的详细步骤
配置 Apache 服务器 目的: 能够有一个测试的服务器,Apache 服务器是免费的! 为什么是 Apache 使用最广的 Web 服务器 Mac自带,只需要修改几个配置就可以,简单,快捷 有些特 ...
- 安装ConEmu
ConEmu可以提供比Windows自带控制台(cmd.exe)程序更多的功能,从其官网下载安装程序时选择预览版安装包(Preview, Installer)即可.
- .Net之垃圾回收算法
垃圾回收器检测托管堆中是否有应用程序不在使用的任何对象,如果一次垃圾回收之后,堆栈没有可用的内存,new操作符将会抛出OutOfMemoryException(内存溢出). 每一个应用程序都包含一组根 ...
- mysql主从同步从库同步报错
1.在从库上设置master_info信息时出错 mysql> change master to master_host='192.168.157.143',master_port=3306,m ...
- Android Intent的花样启动
刚开始看郭大神的<>,实现以下里面的一些例子.Intent的花样启动 显示Intent的使用. 实例化一个Intent,并且制定当前的activity和要跳转到的activity Inte ...
- Android之 compileSdkVersion, minSdkVersion, and targetSdkVersion
在一年之中,也有可能只是几个月后,你会发布你的Android应用程序.但是这中间有可能会发布新的版本的Android系统,比如:6.0+. 你肯定希望你的应用可以向前兼容,当用户升级到新的Androi ...
- centos 添加epel、remi仓库和ELRepo仓库
centos使用yum安装软件非常方便,yum会自动安装软件的相关依赖.但是centos自带的源仓库,软件相对老旧并且不太全,所以我们可以添加第三方仓库,可以安装较新的软件版本. epel是fedor ...
- 修改默认的undo_retention参数设置
昨天,一个朋友的数据库数据被误操作删除掉了,请求我帮忙进行恢复. 数据库版本是Oracle10g Release 2的,我首先想到的是使用Flashback Query进行闪回恢复,不幸的是ORA-0 ...
- Web模板
http://www.iteye.com/news/26229 http://designmodo.com/admin-html-website-templates/#ixzz1mj36E4kN ht ...
- delphi 实现微信开发
大体思路: 1.用户向服务号发消息,(这里可以是个菜单项,也可以是一个关键词,如:注册会员.) 2.kbmmw web server收到消息,生成一个图文消息给微信,在图文消息中做好自己的url,在u ...