《UNIX网络编程 卷1》 笔记: 原始套接字—ping程序
来源:互联网 发布:手机上能编程的软件 编辑:程序博客网 时间:2024/06/06 05:05
1. 进程可以读写ICMPv4、IGMPv4、ICMPv6分组。
2. 进程可以读写内核不处理其协议字段的IPv4数据报。
3. 进程可以使用IP_HDRINCL套接字选项自行构造IPV4首部。
本节我们使用原始套接字来实现一个常用的程序:ping。为了同时支持ICMPv4和ICMPv6(这里不贴出ICMPv6相关的代码,读者可以在书中查阅),我们定义了一个如下的协议相关的proto结构:
struct proto {void (*fproc)(char *, ssize_t, struct msghdr *, struct timeval *); /*接收处理函数*/void (*fsend)(void); /*发送函数*/void (*finit)(void); /*初始化函数*/struct sockaddr *sasend; /*发送端套接字地址结构*/struct sockaddr *sarecv; /*接收端套接字地址结构*/socklen_t salen; /*套接字地址结构长度*/int icmpproto; /*ICMP协议版本*/} *pr;并定义了两个proto结构的变量proto_v4和proto_v6。
struct proto proto_v4 = {proc_v4, send_v4, NULL, NULL, NULL, 0, IPPROTO_ICMP};struct proto proto_v6 = {proc_v6, send_v6, NULL, NULL, NULL, 0, IPPROTO_ICMPV6};为发送ICMP回显请求报文我们定义了一些全局变量,意义如下:
#define BUFSIZE 1500char sendbuf[BUFSIZE]; //ICMP报文缓冲区int datalen = 56; //ICMP报文数据长度(不包含ICMP首部)char *host; //目的主机IP地址int nsent; //序列号pid_t pid; //进程号int sockfd; //套接字描述符int verbose;
定义ICMP数据的长度为56字节,加上ICMP首部的8字节,整个ICMP报文的长度就是64字节,与实际的ping程序一致。
主函数就是做一些初始化全局变量的工作,注册SIGALRM信号处理函数,然后根据参数host(目标主机名)是IPv4地址还是IPv6地址使用相应版本的proto结构,发送ICMP回显请求的功能是在readloop函数里实现的。
int main(int argc, char **argv){int c;struct addrinfo *ai;char *h;opterr = 0;while ((c = getopt(argc, argv, "v")) != -1) {switch (c) {case 'v':verbose++;break;case '?':err_quit("unrecognized option: %c", c);}}if (optind != argc - 1)err_quit("usage: ping [ -v ] <hostname>");host = argv[optind]; /*目标主机名*/pid = getpid() & 0xffff; /*进程号*/ Signal(SIGALRM, sig_alrm);ai = Host_serv(host, NULL, 0, 0); /*获取主机名相关的addrinfo结构*/h = Sock_ntop_host(ai->ai_addr, ai->ai_addrlen); /*返回套接字关联的IP地址*/printf("PING %s (%s): %d data bytes\n", ai->ai_canonname ? ai->ai_canonname : h, h, datalen);if (ai->ai_family == AF_INET) { /*根据IP协议版本号指定处理函数*/pr = &proto_v4;} else if (ai->ai_family == AF_INET6) {pr = &proto_v6;} elseerr_quit("unknown address family %d", ai->ai_family);pr->sasend = ai->ai_addr;pr->sarecv = Calloc(1, ai->ai_addrlen);pr->salen = ai->ai_addrlen;readloop();exit(0);}
host_serv函数在名字与地址转换一节中实现,sock_ntop_host函数是将sockaddr结构中的IP地址数值格式转换为表达格式,支持IPv4和IPv6,代码如下:
char* sock_ntop_host(const struct sockaddr *sa, socklen_t salen){ static char str[128];/* Unix domain is largest */switch (sa->sa_family) {case AF_INET: {struct sockaddr_in*sin = (struct sockaddr_in *) sa;/*IP地址数值格式转表达格式*/if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)return(NULL);return str;}case AF_INET6: {struct sockaddr_in6*sin6 = (struct sockaddr_in6 *) sa;if (inet_ntop(AF_INET6, &sin6->sin6_addr, str, sizeof(str)) == NULL)return(NULL);return(str);}default:snprintf(str, sizeof(str), "sock_ntop_host: unknown AF_xxx: %d, len %d", sa->sa_family, salen);return str;} return NULL;}
信号处理函数sig_alrm代码如下。它先调用send_v4或send_v6发送相应的ICMP请求回显,然后又设置了1秒的定时器,这样每秒钟都会发送一个ICMP回显请求。
void sig_alrm(int signo){ (*pr->fsend)(); alarm(1); return;}
第一个ICMP回显请求报文由readloop函数调用sig_alrm函数发出。在发送报文之前必须先创建一个ICMP类型的原始套接字。readloop函数代码如下:
void readloop(void){char recvbuf[BUFSIZE];char controlbuf[BUFSIZE];struct msghdr msg;struct iovec iov;ssize_t n;struct timeval tval;/*创建原始套接字,需要超级用户权限*/sockfd = Socket(pr->sasend->sa_family, SOCK_RAW, pr->icmpproto);setuid(getuid());if (pr->finit)(*pr->finit)();/*发送ICMP回显请求*/sig_alrm(SIGALRM);iov.iov_base = recvbuf;iov.iov_len = sizeof(recvbuf);msg.msg_name = pr->sarecv;msg.msg_iov = &iov;msg.msg_iovlen = 1;msg.msg_control = controlbuf;for ( ; ; ) {msg.msg_namelen = pr->salen;msg.msg_controllen = sizeof(controlbuf);n = recvmsg(sockfd, &msg, 0); /*接收到达接口的ICMP报文*/if (n < 0) { if (errno == EINTR)continue;elseerr_sys("recvmsg error");}Gettimeofday(&tval, NULL); /*获取报文到达时间*/(*pr->fproc)(recvbuf, n, &msg, &tval); /*处理接收的报文*/}}在发送第一个ICMP回显请求后,它循环调用recvmsg接收ICMP报文,然后调用proc_v4或proc_v6处理。
下面我们就来看看send_v4和proc_v4函数是如何实现的。
send_v4函数发送ICMP回显请求报文,报文的格式如下:
我们通常将标识符字段设置为进程ID号。序号字段从0开始,每发送一个报文递增1。为了计算报文往返时间RTT,我们将数据填充为发送时间戳。send_v4的代码如下:
void send_v4(void){int len;struct icmp *icmp;icmp = (struct icmp *)sendbuf;icmp->icmp_type = ICMP_ECHO; /*类型 = 8, 代码 = 0 请求回显*/icmp->icmp_code = 0;icmp->icmp_id = pid; /*标识符字段设置为发送进程的pid*/icmp->icmp_seq = nsent++; /*序列号*/memset(icmp->icmp_data, 0x0, datalen); /*数据长度58字节*/Gettimeofday((struct timeval *)icmp->icmp_data, NULL); /*填充发送时间戳*/len = 8 + datalen; /*ICMP报文长度64字节*/icmp->icmp_cksum = 0;icmp->icmp_cksum = in_cksum((u_short *)icmp, len); /*计算校验和*/Sendto(sockfd, sendbuf, len, 0, pr->sasend, pr->salen);}由于readloop函数创建原始套接字时IP_HDRINCL套接字选项未开启,因此我们构造的数据(sendbuf)是指IP首部之后的数据,IP首部由内核构造并添加到我们的数据之前。在这个例子中我们发送的以太网帧长是64 + 20 + 14 = 98字节。
在接收到ICMP报文时,我们调用proc_v4处理,打印出发送给本进程的ICMP回显应答。函数最后一个参数是在readloop函数中获取的接收到报文时的时间戳,由此可以计算报文往返时间RTT。
void proc_v4(char *ptr, ssize_t len, struct msghdr *msg, struct timeval *tvrecv){int hlen1, icmplen;double rtt;struct ip *ip;struct icmp *icmp;struct timeval *tvsend;/*验证报文合法性*/ip = (struct ip *)ptr;hlen1 = ip->ip_hl << 2;icmp = (struct icmp *)(ptr + hlen1);if ((icmplen = len - hlen1) < 8)return;if (icmp->icmp_type == ICMP_ECHOREPLY) { /*ICMP回显应答*/if (icmp->icmp_id != pid) /*只处理发送给本进程的回显应答*/return;if (icmplen < 16)return;/*获取报文发送时间*/tvsend = (struct timeval *)icmp->icmp_data;/*计算RTT*/tv_sub(tvrecv, tvsend);rtt = tvrecv->tv_sec * 1000.0 + tvrecv->tv_usec / 1000.0;/*打印出回显应答报文的数据长度,序列号ttl,报文往返时间TTL*/printf("%d bytes from %s: seq = %u, ttl = %d, rtt = %.3f ms\n", icmplen, Sock_ntop_host(pr->sarecv, pr->salen), icmp->icmp_seq, ip->ip_ttl, rtt);} else if (verbose) { /*打印其他类型的ICMP报文*/printf(" %d bytes from %s: type = %d, code = %d\n", icmplen, Sock_ntop_host(pr->sarecv, pr->salen), icmp->icmp_type, icmp->icmp_code);}}我们只关心发送给本进程的ICMP回显应答,如果开启了-v选项,那么我们打印其他类型的ICMP报文。
我们实现的ping程序效果如下:
- 《UNIX网络编程 卷1》 笔记: 原始套接字—ping程序
- 《网络编程》原始套接字 ---ping程序实现
- 学习 UNIX网络编程卷1:套接字 笔记1-实现一个简单的回射客户服务器程序
- UNIX网络编程——原始套接字SOCK_RAW
- UNIX网络编程——原始套接字SOCK_RAW
- UNIX网络编程——原始套接字SOCK_RAW
- 《UNIX网络编程 卷1》 笔记: 基本UDP套接字编程
- UNIX网络编程卷1:套接字联网-第5章:TCP客户/服务器程序示例
- 学习《UNIX网络编程 卷1:套接字联网API》
- UNIX网络编程 卷1:套接字联网API
- UNIX网络编程卷1:套接字联网API
- unix网络编程卷1:套接字联网 源码编译
- 《Unix网络编程卷1-套接字联网API》linux 学习笔记一
- Unix网络编程(卷1)—笔记
- Linux网络编程之原始套接字-ping协议实现
- Linux网络编程之原始套接字-ping协议实现
- Linux网络编程之原始套接字-ping协议实现
- 使用原始套接字编程实现简单的ping程序
- ubuntu查看搜索可安装包名
- R语言写简单线性回归
- 设计模式
- table列表中不换行处理(适应不同分辨率)
- Android GridView的用法总结
- 《UNIX网络编程 卷1》 笔记: 原始套接字—ping程序
- 时间复杂度和空间复杂度详解
- Sqoop 1.99.7 客户端操作实践
- linux ftp 文件修改时间 ModifiedDate 与本地相差 8小时
- spring配置文件详解
- ubantu网卡驱动安装过程
- 引用计数的原理和实例
- mysql 存储引擎 索引数据结构
- 分针网—IT教育:关于php 高并发解决的一点思路