在上一篇博文中,我们使用了 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 函数的封装
- 掌握辅助数据