在unp第14章讲了这两个函数,但是只是讲了两个数据结构及参数而已,所以自己想根据介绍来重构udp回射的客户端程序。但是sendmsg和recvmsg都遇到了问题,并且纠结了很久,所以在此记录下。
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成员,目前没有研究。
由于辅助数据涉及内容较多,故分出一节来讲。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); /* 获取辅助数据 */
}
下面是自己写的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和接收数据报所用的接口索引。
但是该程序偶尔会出现获取不到返回的数据的问题,还未弄清楚为什么会出现这种现象。