《UNIX网络编程 卷1》 笔记: 原始套接字—ping程序
2017-06-15 10:38
351 查看
原始套接字可以提供普通的TCP和UDP套接字不支持的三个能力:
1. 进程可以读写ICMPv4、IGMPv4、ICMPv6分组。
2. 进程可以读写内核不处理其协议字段的IPv4数据报。
3. 进程可以使用IP_HDRINCL套接字选项自行构造IPV4首部。
本节我们使用原始套接字来实现一个常用的程序:ping。为了同时支持ICMPv4和ICMPv6(这里不贴出ICMPv6相关的代码,读者可以在书中查阅),我们定义了一个如下的协议相关的proto结构:
定义ICMP数据的长度为56字节,加上ICMP首部的8字节,整个ICMP报文的长度就是64字节,与实际的ping程序一致。
主函数就是做一些初始化全局变量的工作,注册SIGALRM信号处理函数,然后根据参数host(目标主机名)是IPv4地址还是IPv6地址使用相应版本的proto结构,发送ICMP回显请求的功能是在readloop函数里实现的。
host_serv函数在名字与地址转换一节中实现,sock_ntop_host函数是将sockaddr结构中的IP地址数值格式转换为表达格式,支持IPv4和IPv6,代码如下:
信号处理函数sig_alrm代码如下。它先调用send_v4或send_v6发送相应的ICMP请求回显,然后又设置了1秒的定时器,这样每秒钟都会发送一个ICMP回显请求。
第一个ICMP回显请求报文由readloop函数调用sig_alrm函数发出。在发送报文之前必须先创建一个ICMP类型的原始套接字。readloop函数代码如下:
下面我们就来看看send_v4和proc_v4函数是如何实现的。
send_v4函数发送ICMP回显请求报文,报文的格式如下:
我们通常将标识符字段设置为进程ID号。序号字段从0开始,每发送一个报文递增1。为了计算报文往返时间RTT,我们将数据填充为发送时间戳。send_v4的代码如下:
+ 20 + 14 = 98字节。
在接收到ICMP报文时,我们调用proc_v4处理,打印出发送给本进程的ICMP回显应答。函数最后一个参数是在readloop函数中获取的接收到报文时的时间戳,由此可以计算报文往返时间RTT。
我们实现的ping程序效果如下:
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 1500 char 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; } else err_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; else err_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网络编程代码 第28章 原始套接字
- UNIX网络编程卷一 笔记 第四章 基本TCP套接字编程
- UNIX网络编程——原始套接字(dos攻击)
- UNIX网络编程——原始套接字的魔力【续】
- UNIX网络编程——原始套接字的魔力【下】
- Unix网络编程学习笔记【1】套接字地址结构
- UNIX网络编程——原始套接字的魔力【下】
- UNIX网络编程——原始套接字(dos攻击)
- UNIX网络编程——原始套接字的魔力【续】
- UNIX网络编程——原始套接字的魔力【续】
- UNIX网络编程卷一 笔记 第7章 套接字选项
- UNIX网络编程——原始套接字的魔力【上】
- UNIX网络编程——原始套接字的魔力【上】
- UNIX网络编程——原始套接字(dos攻击)
- UNIX网络编程——原始套接字的魔力【下】
- UNIX网络编程卷一 笔记 第三章 套接字编程简介
- UNIX网络编程——原始套接字SOCK_RAW
- UNIX网络编程——原始套接字的魔力【下】
- UNIX网络编程——原始套接字SOCK_RAW
- UNIX网络编程——原始套接字的魔力【上】