recvmsg和sendmsg函数

xiaoxiao2021-02-28  88

在unp第14章讲了这两个函数,但是只是讲了两个数据结构及参数而已,所以自己想根据介绍来重构udp回射的客户端程序。但是sendmsg和recvmsg都遇到了问题,并且纠结了很久,所以在此记录下。

1. 基础介绍

recvmsg和sendmsg是最通用的I/O函数,只要设置好参数,read、readv、recv、recvfrom和write、writev、send、sendto等函数都可以对应换成这两个函数来调用。

#include <sys/socket.h> ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); ssizt_t sendmsg(int sockfd, struct msghdr *msg, int flags);

函数的参数少,说明msghdr参数就比较复杂了,因为需要的参数都被封装到这个参数了。msghdr数据结构如下:

struct msghdr { void *msg_name; /* protocol address */ socklen_t msg_namelen; /* sieze of protocol address */ struct iovec *msg_iov; /* scatter/gather array */ int msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* ancillary data ( cmsghdr struct) */ socklen_t msg_conntrollen; /* length of ancillary data */ int msg_flags; /* flags returned by recvmsg() */ }

msg_name和msg_namelen用于套接字未连接的时候(主要是未连接的UDP套接字),用来指定接收来源或者发送目的的地址。两个成员分别是套接字地址及其大小,类似recvfrom和sendto的第二和第三个参数。对于已连接套接字,则可直接将两个参数设置为NULL和0。而对于recvmsg,msg_name是一个值-结果参数,会返回发送端的套接字地址。 msg_iov和msg_iovlen两个成员用于指定数据缓冲区数组,即iovec结构数组。iovec结构如下:

#include <sys/uio.h> struct iovec { void *iov_base; /* starting address of buffer */ size_t iov_len; /* size of buffer */ }

其中iov_base就是一个缓冲区元素,事实上也是一个数组,而iov_len则是指定该数据的大小。也就是说,缓冲区是一个二维数组,并且每一维长度不是固定的。猜测这样子设置应该是方便传递多个结构类型不同,并且长度也是不固定的数据吧,这样子客户端就可以直接对每个位置的数据进行转换获取就行了。如果只是当存传送一个字符串,那只需要将msg_iovlen设置成1,然后将数据赋给iov[0].iov_base就行了。无论是sendmsg和recvmsg,都需要提前设置好这两项并且分配好内存。 msg_control和msg_controllen是用来设置辅助数据的位置和大小的,辅助数据(ancillary data)也叫作控制信息(control infomation)。这两个成员可以用来返回关于数据报文的其他指定信息,不过需要通过setsockopt函数指定要返回的辅助信息。对于sendmsg,这两项需要都设置成0,否则会导致发送数据失败。还未研究过sendmsg的辅助数据能够做什么。 关于两个函数的flags参数和msghdr的msghdr的msg_flags成员,目前没有研究。

2. 辅助数据

由于辅助数据涉及内容较多,故分出一节来讲。unp中给出了下面各种辅助数据的用途:

协议cmsg_levelcmsg_type说明IPv4IPPROTO_IPIP_RECVDSTADDR随UDP数据报接收目的的地址IP_RECVIF随UDP数据报接收接口的索引IPv6IPPROTO_IPV6IPV6_DSTOPTS指定/接收目的地选项IPV6_HOPLIMIT指定/接收跳限IPV6_HOPOPTS指定/接收步跳选项IPV6_NEXTHOP指定下一跳地址IPV6_PKTINFO指定/接收分组信息IPV6_PTHDR指定/接收路由首部IPV6_TCLASS指定/接收分组流通类别Unix域SOL_SOCKETSCM_RIGHTS发送/接收描述符SCM_CREDS发送/接收用户凭证

其中cmsg_level和cmsg_type应该和调用setsockopt函数时传递的level和optname参数是一样的。那么我们怎么获取辅助数据呢,在msg_control辅助数据是通过一个或多个辅助数据对象保存的,辅助数据对象cmsghdr结构如下:

#include <sys/socket.h> struct cmsghdr { socklen_t cmsg_len; /* length in bytes, including this structure */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-specific type */ /* followed by unsigned char cmsg_data[] */ }

而辅助数据对象在实际的存储中是如下分布的(因为不知道在markdown中设置表格宽度,所以有点长):

cmsg_lencmsg_levelcmsg_type填充字节数据

cmsghdr中实际上只有三个元素,而cmsg_data成员实际上并不存在,只是用来表明接下来都是数据,并且实际上数据和结构中还存在着填充数据。填充数据可能是为了对齐(unp中讲到msg_control指向的辅助数据必须为cmsghdr结构适当的对齐),在两个cmsghdr之间也存在着填充数据。 看到这里的时候我是很郁闷的,那我要怎么获取到辅助数据呢?一开始以为要自己手动给cms_data分配内存,但是我连cmsg_data成员都获取不到啊!然后仔细看了unp中的内容才发现可以通过下面5个CMSG_XXX宏来获取和设置辅助数据。

#include <sys/socket.h> #include <sys/param.h> struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdrptr); //返回:指向第一个cmsghdr结构的指针,若无辅助数据则为NULL struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsghdr); //返回:指向下一个cmsghdr结构的指针,若不再有辅助数据对象则为NULL unsigned char *CMSG_DATA(struct cmsghdr *cmsgptr); //返回:指向与cmsghdr结构关联的数据的第一个字节的指针 unsigned char *CMSG_LEN(unsigned int length); //返回:给定数据量下存放到cmsg_len中的值 unsigned char *CMSG_SPACE(unsigned int length); //返回:给定数据量下一个辅助数据对象总的大小。

通过上面五个宏我们可以很方便的为msg_control分配内存和遍历辅助对象、获取辅助数据。不过对于分配内存一般需要预先知道要获取的辅助数据结构的大小。而CMSG_LEN和CMSG_SPACE的区别在于后者会包含两个辅助数据之间的填充字节。

char contorl[CMSG_SPACE(size_of_struct1) + CMSG_SPACE(size_of_struct2)]; struct msghdr msg; struct cmsghdr *cmsgptr; for ( cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL; cmsgptr = CMSG_NXTHDR(&msg, cmsgptr) ) { /* 判断是否是自己需要的msg_level和msg_type */ u_char *ptr; ptr = CMSG_DATA(cmsgptr); /* 获取辅助数据 */ }

3. 琐碎

对于已连接的套接字,msghdr的msg_name直接设置为NULL,对于recvmsg,该成员会返回对端的套接字地址。对于sendmsg,msghdr的msg_control和msg_controllen需要设置为0,不设置为似乎无法发送成功。处理辅助数据可以直接用5个宏,并且需要根据msg_level和msg_type判断辅助数据的类型再进行相应的转换。unp中讲到的很多cmsg_type可能自己的系统中并没有移植,这点需要注意。比如我使用kubuntu,就没有移植IP_RECVDSTADDR和IP_RECVIF。最后我是参考网上的例子,改用IP_PKTINFO才完成了例子,也是在这里纠结和浪费了很多时间。实际上unp第7章的函数就可以用来判断这些设置项是否存在,也可以在调用setsockopt和判断msg_level、msg_type之前用#if defined语句来判断本系统是否兼容该项,如果不兼容的话会直接跳过接下来的处理(见例子)。msg_level和msg_type需要注意支持的协议。

4. 例子

下面是自己写的udp回射客户端程序,代码可能有点凌乱。但基本包含了上面所讲的知识点,可以直接与unp中的udp回射服务器端程序配合使用。

/* unpudpsendmsg.c */ #include <stdio.h> #include <arpa/inet.h> #include <sys/socket.h> #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <sys/time.h> #define SERV_PORT 51002 #define MAXLINE 256 #define SA struct sockaddr void err_quit(const char *sstr) { printf("%s\n", sstr); exit(0); } int main(int argc, char **argv) { int sockfd, n; struct sockaddr_in servaddr, dstaddr; char buff[MAXLINE], buff2[MAXLINE]; struct msghdr msgsent, msgrecvd; struct cmsghdr cmsg, *cmsgtmp; struct iovec iov, iov2; const int on = 1; char control[CMSG_SPACE(64)]; // 使用CMSG_DATA分配cmsg_control内存,实际应该根据已知的结构分配。 if ( argc < 2 ) { err_quit("usage: unpudpsendmsg <IPaddress>"); } sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; inet_pton(AF_INET, argv[1], &servaddr.sin_addr); servaddr.sin_port = htons(SERV_PORT); // 处理sendmsg的msghdr结构 msgsent.msg_name = NULL; msgsent.msg_namelen = 0; msgsent.msg_iovlen = 1; iov.iov_base = buff; // 为iov[0]分配内存 iov.iov_len = MAXLINE; msgsent.msg_iov = &iov; msgsent.msg_control = 0; // 对sendmsg,msg_control要设置为0。 msgsent.msg_controllen = 0; // 处理recvmsg的msghdr结构 msgrecvd.msg_name = &dstaddr; msgrecvd.msg_control = control; msgrecvd.msg_controllen = sizeof(control); iov2.iov_base = (void *)buff2; iov2.iov_len = MAXLINE; msgrecvd.msg_iov = &iov2; msgrecvd.msg_iovlen = 1; msgrecvd.msg_flags = 0; connect(sockfd, (SA *)&servaddr, sizeof(servaddr)); #if defined(IP_PKTINFO) setsockopt(sockfd, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on)); #elif defined(IP_RECVDSTADDR) setsockopt(sockfd, IPPROTO_IP, IP_RECVORIGDSTADDR, &on, sizeof(on)); #endif while ( 1 ) { fgets(buff, MAXLINE, stdin); n = sendmsg(sockfd, &msgsent, 0); if ( n <= 0 ) { continue; } n = recvmsg(sockfd, &msgrecvd, 0); printf("recvmsg: %s", (char *)msgrecvd.msg_iov[0].iov_base); // 通过缓冲数据组获取服务器端返回的数据。 printf("msg_controllen: %d\n", (int)msgrecvd.msg_controllen); for ( cmsgtmp = CMSG_FIRSTHDR(&msgrecvd); cmsgtmp != NULL; cmsgtmp = CMSG_NXTHDR(&msgrecvd, cmsgtmp) ) { #if defined(IP_RECVDSTADDR) if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_RECVDSTADDR ) { // 判断msg_level和msg_type再进行相应的处理。 struct sockaddr_in *addrtmp; char ip[14]; addrtmp = (struct sockaddr_in *)CMSG_DATA(cmsgtmp); inet_ntop(AF_INET, addrtmp, ip, sizeof(ip)); printf("recv ip: %s, port: %d\n", ip, ntohs(addrtmp->sin_port)); } #elif defined(IP_PKTINFO) if ( cmsgtmp->cmsg_level == IPPROTO_IP && cmsgtmp->cmsg_type == IP_PKTINFO ) { struct in_pktinfo *pktinfo; pktinfo = (struct in_pktinfo*)CMSG_DATA(cmsgtmp); printf("recv ip: %s, ifindex: %d\n", inet_ntoa(pktinfo->ipi_addr), pktinfo->ipi_ifindex); } #endif } } }

运行服务器程序,再运行unpudpsendmsg 127.0.0.1后,输入字符串,可以看到类似下面的输出:

walker@Walker-s $ ./unpudpsendmsg 127.0.0.1 11111 recvmsg: 11111 msg_controllen: 32 recv ip: 127.0.0.1, ifindex: 1 2222 recvmsg: 2222 msg_controllen: 32 recv ip: 127.0.0.1, ifindex: 1 333 recvmsg: 333 msg_controllen: 32

根据辅助数据我们得到了对端的IP和接收数据报所用的接口索引。 但是该程序偶尔会出现获取不到返回的数据的问题,还未弄清楚为什么会出现这种现象。

转载请注明原文地址: https://www.6miu.com/read-71224.html

最新回复(0)