recvmsg和sendmsg函数

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

1. 基础介绍

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

#include 
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 
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_level cmsg_type 说明
IPv4 IPPROTO_IP IP_RECVDSTADDR 随UDP数据报接收目的的地址
IP_RECVIF 随UDP数据报接收接口的索引
IPv6 IPPROTO_IPV6 IPV6_DSTOPTS 指定/接收目的地选项
IPV6_HOPLIMIT 指定/接收跳限
IPV6_HOPOPTS 指定/接收步跳选项
IPV6_NEXTHOP 指定下一跳地址
IPV6_PKTINFO 指定/接收分组信息
IPV6_PTHDR 指定/接收路由首部
IPV6_TCLASS 指定/接收分组流通类别
Unix域 SOL_SOCKET SCM_RIGHTS 发送/接收描述符
SCM_CREDS 发送/接收用户凭证

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

#include 
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_len
cmsg_level
cmsg_type
填充字节
数据

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

#include 
#include 
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. 琐碎

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

4. 例子

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

/* unpudpsendmsg.c */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#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 ");
    }

    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和接收数据报所用的接口索引。
但是该程序偶尔会出现获取不到返回的数据的问题,还未弄清楚为什么会出现这种现象。

你可能感兴趣的:(网络编程)