《UNIX网络编程 卷1》 笔记: 原始套接字—ping程序

来源:互联网 发布:手机上能编程的软件 编辑:程序博客网 时间:2024/06/06 05:05
原始套接字可以提供普通的TCP和UDP套接字不支持的三个能力:
    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_v4proto_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_v4send_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_v4proc_v6处理。

下面我们就来看看send_v4proc_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程序效果如下:


阅读全文
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 捡到苹果6s手机怎么办才能自己用 苹果6s玩游戏卡住了怎么办 苹果6s进水换屏后指纹失灵怎么办 苹果手机6s声音按键失灵怎么办 苹果手机触屏失灵怎么办5s 苹果六摔了一下屏幕失灵怎么办 荣耀5c进水屏幕触摸屏失灵怎么办 生活玩家打不了风云蝙蝠岛怎么办啊 企鹅号在注册的时候选错领域怎么办 博士超期学信网的信息被删除怎么办 电脑账号删除后电脑开不了怎么办 lol不小心融错了皮肤怎么办 农村医保交费了没有录入系统怎么办 电动三轮车在泗阳被交警扣了怎么办 环评证实际的设备少报了怎么办 别人盗用了自己的社保卡怎么办 如果有人盗用社保卡信息住院怎么办 丈夫出轨生下孩子妻子该怎么怎么办 l老公偷着儿子消失了怎么办 老婆出轨怀孕现在流了他跑了怎么办 交通银行贷款资金户里面的钱怎么办 新车2个月出现好多小毛病怎么办 地上终末之日尸潮破坏房子怎么办 初一数学期未考试考了77分怎么办 生育服务单再婚婚史情况怎么办 我孩子的数学一直很差怎么办呀! 扶桑花的叶子出斑点发黄怎么办 高二的学生成绩下降特别大怎么办 六个月大的宝宝咳嗽伴有气喘怎么办 昨天奶用力吸今天好疼怎么办 律师把医院的医药费单子丢了怎么办 母亲是个不明事理的人怎么办 华西医院的就诊卡掉了怎么办 整形医院把我脸上疤痕被大了怎么办 牙齿缝里的东西很臭怎么办 五岁宝宝牙齿有空洞斑点怎么办 给别人担保贷款被起诉了怎么办 不小心在微信里登录江西移动怎么办 住院出院后医保卡在医院怎么办 医院门诊做完检查医生下班了怎么办 退税的发票勾选为抵扣的发票怎么办