linux 网络编程 ping,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的量,这些量定义在中,比如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协议的头部定义如下:
structicmphdr {
unsignedchartype; //标记当前ICMP数据包的类型 ECHO、ECHOREPLY等等。
unsignedcharcode; //code取值与type协同,或者对type具体化
unsignedshortchecksum; //校验和
union {struct{
unsignedshortid;
unsignedshortsequence;
} echo;
unsignedlonggateway;
} un;
};
这个ICMP的头部可能与最新的版本不太一致,但是基本结构是类似的。从这个结构可以计算出来,对于32位的机器来说,这个头部的长度就是8字节。
IP头部如果不考虑附加信息的话,固定长度应该为20字节。当然在ping代码中,我们并不考虑IP头部的初始化等操作,只是了解一下。
1 structiphdr {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 "
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()更加优秀一些,怎么个优秀法,咱们别处再说,在这里只说怎么获取的。代码丑陋,将就看。
voidGetAddrInfo(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";structaddrinfo 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)
{intlen;
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)
{intip_hdr_len;
char recvbuf[1500];inticmplen;struct ip *ip;struct icmp *icmp;struct timeval *time_send;char char_seq[20];doublertt;
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;
}
现在,主要的代码部分都有了,至于怎么循环发送很多包,怎么依次接收,要不要定时器等等,这些都仁者见仁智者见智了,方法很多,就不再赘述了。
上面的代码都是参考<>编写的,没有关注IPV6的部分,因为初始入门,或者为了探究ICMP和原始套接口,例子越简单越好。
其实从本文可以看出,原始套接字并不复杂,复杂的还是程序设计、协议理解。真正的ICMP协议可不是这么实现的,看看Linux源代码就知道了,那里面的实现
数据结构是基于sk_buff,icmp_hdr,ip_hdr等等。咱们写的这个也就是玩玩而已,这个代码实现了ICMP协议的五分之一功能吧,不会再多了。
linux 网络编程 ping,Linux 网络编程基础(4) -- Ping 的C代码实现相关推荐
- 【socket】从计算机网络基础到socket编程——Windows Linux C语言 + Python实现(TCP+UDP)
一.部分基础知识 1.1 计算机网络的体系结构 1.11 互联网简介 1.12 计算机网络的分类 1.13 协议与网络的分层体系结构 ▶ 协议 ▶ 网络的分层体系结构 1.14 OSI 七层模型(重要 ...
- Linux网络编程之套接字基础
Linux网络编程之套接字基础 1.套接字的基本结构 struct sockaddr 这个结构用来存储套接字地址. 数据定义: struct sockaddr { unsigned short sa_ ...
- Linux网络编程篇之ICMP协议分析及ping程序实现
Linux网络编程系列: Linux网络编程篇之Socket编程预备知识 Linux网络编程篇之TCP协议分析及聊天室功能实现 如果对Linux网络编程,对socket通信不是太清楚的同学,强烈推荐看 ...
- 嵌入式学习笔记-linux应用编程和网络编程-3.8 网络基础
一.再论进程 1.从进程间通信说起 网络域套接字socket,网络通信其实就是位于网络中不同主机上面的2个进程之间的通信. 2.网络通信概述 硬件部分:网卡 操作系统底层:网卡驱动 操作系统API:s ...
- IT视频课程集(包含各类Oracle、DB2、Linux、Mysql、Nosql、Hadoop、BI、云计算、编程开发、网络、大数据、虚拟化
马哥Linux培训视频课程:http://pan.baidu.com/s/1pJwk7dp Oracle.大数据系列课程:http://pan.baidu.com/s/1bnng3yZ 天善智能BI培 ...
- C++教程网之Linux网络编程视频 Unix网络编程视频
教程非常不错,价值280元,绝对是干货 Linux网络编程(总共41集) 讲解Linux网络编程知识,分以下四个篇章. Linux网络编程之TCP/IP基础篇 Linux网络编程之socket编程篇 ...
- 网络编程二-LINUX网络IO模型
目录 前言:网络编程里通用常识 一.同步和异步与阻塞和非阻塞 同步和异步 阻塞和非阻塞 两者的组合 二.五种I/O模型 阻塞I/O模型 非阻塞IO模型 IO复用模型 信号驱动IO 异步IO模型 5个I ...
- 【Linux网络编程】TCP网络编程中connect listen和accept三者之间的关系
00. 目录 文章目录 00. 目录 01. TCP服务端和客户端流程 02. connect函数 03. listen函数 04. 三次握手 05. accept函数 06. 附录 01. TCP服 ...
- 【Linux网络编程】TCP网络编程中connect()、listen()和accept()三者之间的关系
基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下: connect()函数 对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三 ...
最新文章
- 保证计算机网络的稳定运行,厦门大学校园网管理保证网络稳定运行
- 惠普10亿美元锻造Helion云品牌
- 全屏显示的包含webview的页面中弹出的软键盘覆盖输入框的问题
- =空值返回空值_@ParameterizedTest在@CvsSource中具有空值
- [小技巧][JAVA][转换]List, Integer[], int[]的相互转换
- Shell 基础介绍 [1]
- 了解VS2005为你的MFC程序做的一些事
- 有钱人也开始消费降级了!
- 为qt程序添加ico图标
- 解决高并发(数据库锁机制)
- oracle odac 配置,.Net項目中通過ODAC方式鏈接Oracle數據庫相關配置
- BZOJ5336 DP套DP
- 讲讲传奇架设教程跟传奇开区教程,我们首先要明白传奇如何形成
- OpenCV2:特征匹配及其优化
- python看网络电视
- .NET5.0 初始
- 2021-10-13-草稿纸
- SPI 及 NOR Flash 介绍
- 图片在线收款发货系统源码
- 出包王女全集名字和顺序
热门文章
- Django源码分析6:auth认证及登陆保持
- E: Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)E: U
- 规格选项表管理之查询获取规格选项表列表数据
- 创建Django项目和模型(创建工程、子应用、设置pycharm环境、使用Django进行数据库开发的步骤)
- OpenCV中直方图反向投影算法详解与实现
- 卷积神经网络如何处理一维时间序列数据?
- Python加速运行技巧
- HTML5 - Websocket
- Eclipse搭建java分布式商城项目
- Confluence 6 指派和撤销空间权限