在上一篇博文中,我们使用了 recvmsg 函数来获取标志位,但是每次填充 struct msg 结构体都相当费事,因此我们希望将这个过程封装成一个函数 recvFromFlags,一劳永逸。
除了获取标志位之外,我们还希望得到数据包是从哪个接口进来的,以及数据包的目的地址。这个任务需要使用辅助数据结构来完成,如果你不记得辅助数据结构,请参考《辅助数据》。
本文所用到的代码托管在 gitos 上:http://git.oschina.net/ivan_allen/unp
本文使用的程序路径为 unp/program/advcudp/recvfromflags
1. recvFromFlags 函数声明
int recvFromFlags(int sockfd, char* buf, int len, int *flags, struct sockaddr *addr, socklen_t *addrlen, struct in_pktinfo *pkt);
先不论此函数实现,先解释下参数的含义。前 3 个就不解释了,大家都明白。从 flags 开始。
- flags: 这是一个传出参数,用来接收 recv 可能产生的错误或其它信息,常见值如下:
- MSG_EOR
- MSG_TRUNC
- MSG_CTRUNC
- MSG_OOB
- MSG_ERRQUEUE
- MSG_BCAST (使用时需先检查有没有定义)
- MSG_MCAST(使用时需先检查有没有定义)
- addr 和 addrlen 和 recvfrom 函数的一样,没有区别
- pkt:这是一个传出参数,定义在后面给出。
recvFromFlags 的返回值表示接收的字节数。返回 -1 失败。
1.1 struct in_pktinfo 结构体(man 7 ip)
struct in_pktinfo { unsigned int ipi_ifindex; /* 接口索引号 */ struct in_addr ipi_spec_dst; /* 路由地址 */ struct in_addr ipi_addr; /* 目标地址*/ };
该数据结构,通过 struct msghdr 的成员 msg_control 辅助数据来传递。如果想要接收此数据,接收方需要打开 socket 选项 IP_PKTINFO,具体如下:
int on = 1; setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on));
2. 示例
在 udp server 中,我们通过 recvFromFlags 来演示它的用法,下面是部分程序(已删除无关部分)。
- 部分代码
void doServer(int sockfd) { char buf[4096]; char ifname[IF_NAMESIZE]; int nr, nw, flags; struct sockaddr_in cliaddr; socklen_t len; struct in_pktinfo pkt; while(1) { len = sizeof(cliaddr); flags = 0; bzero(&pkt, sizeof(pkt)); // 接收数据,同时接收标志位,struct in_pktinfo 信息 recvFromFlags(sockfd, buf, 20, &flags, (struct sockaddr*)&cliaddr, &len, &pkt); printf("%d byte datagram from %s", nr, inet_ntoa(cliaddr.sin_addr)); if (pkt.ipi_addr.s_addr != 0) { printf(", to %s", inet_ntoa(pkt.ipi_addr)); } if (pkt.ipi_spec_dst.s_addr != 0) { printf("(local ip %s)", inet_ntoa(pkt.ipi_spec_dst)); } if (pkt.ipi_ifindex > 0) { printf(", recv i/f = %s", if_indextoname(pkt.ipi_ifindex, ifname)); } if (flags & MSG_TRUNC) { printf(" (datagram truncated)"); } if (flags & MSG_CTRUNC) { printf(" (control info truncated)"); } #ifdef MSG_BCAST if (flags & MSG_BCAST) { printf(" (broadcast)"); } #endif #ifdef MSG_MCAST if (flags & MSG_MCAST) { printf(" (multicast)"); } #endif printf("\n"); sendto(sockfd, buf, nr, 0, (struct sockaddr*)&cliaddr, len); } }
- 运行结果
图1 使用 recvFromFlags 的 udp 服务器
3. recvFromFlags 函数实现
因为要接收标志位和辅助数据,因此内部实现只能使用 recvmsg 函数来完成。标志位的接收在上一篇博文里已经演示过,非常简单。这里重点和难点在于 struct in_pktinfo 数据的接收。实际上,如果你已经熟练掌握了辅助数据,这也不是什么难事。
下面我只贴上接收辅助数据的关键代码。recvFromFlags 函数的具体实现定义在 unp/program/util/common.cc
中。
int recvFromFlags(int sockfd, char* buf, int len, int *flags, struct sockaddr *addr, socklen_t *addrlen, struct in_pktinfo *pkt) { int nr; struct msghdr msg; // 其它局部变量定义... // 填充 msg // ... msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); nr = recvmsg(sockfd, &msg, *flags); if (nr < 0) { return nr; } // 返回的标志位 *flags = msg.msg_flags; if (msg.msg_controllen < sizeof(struct cmsghdr) // 辅助数据长度不够 || (msg.msg_flags & MSG_CTRUNC) // 辅助数据被截断 || pkt == NULL) { // 用户并不想知道接口信息 return nr; } // 遍历辅助数据 // Linux 采用 IP_PKTINFO 而不是 IP_RECVDSTADDR 和 IP_RECVIF // 该套接字选项对应结构体 struct in_pktinfo // 成员 ipi_ifindex 表示数据包从哪个接口进来的 // 成员 ipi_spec_dst 表示数据包的本地地址(路由地址) // 成员 ipi_addr 表示数据包的目的地址 for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) { memcpy(pkt, CMSG_DATA(cmsg), sizeof(struct in_pktinfo)); break; } ERR_QUIT("unknown ancillary data, len = %d, level = %d, type = %d", cmsg->cmsg_len, cmsg->cmsg_level, cmsg->cmsg_type); } return nr; }
4. 总结
- 掌握 recvFromFlags 函数的封装
- 掌握辅助数据