这两个函数只适用于套接字描述符。read、readv、recv 和 recvfrom 能用的地方,recvmsg 都能使用,而且,recvmsg 能提供更多的功能。同样的,各种 output 类型的函数都可以替换成 sendmsg 函数。
所以,recvmsg 和 sendmsg 是之前我们学过的读写类函数的究极形态。这么强大的函数,使用起来也会相当的复杂,在本文,我们只讨论它的一部分功能,剩下的功能,以后再说。
1. 函数原型
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
这两个函数,把大多数的参数都放进了一个 struct msghdr 类型的结构体。接下来,我们就来看看这个结构体的样子。
2. msghdr{}
struct msghdr { void *msg_name; /* 套接字地址 */ socklen_t msg_namelen; /* 地址长度 */ struct iovec *msg_iov; /* 散布/聚焦数组 */ size_t msg_iovlen; /* msg_iov 的元素个数 */ void *msg_control; /* 辅助数据 */ size_t msg_controllen; /* 辅助数据大小 */ int msg_flags; /* 接收数据的标志,仅用于 recvmsg */ };
图1 msghdr{} 结构体
- msg_name 和 msg_namelen
这两个成员仅用于无连接的场合(比如无连接的 UDP 套接字)。它相当于 recvfrom 和 sendto 的最后两个参数。
msg_name 指向一个套接字地址结构,msg_namelen 则是该套接字地址结构的大小。对于 sendto 来说,msg_name 存入接收者的地址。对于 recv_msg 来说,它用来接收返回值(值-结果参数)。
- msg_iov 和 msg_iovlen
这两个成员和 writev 和 readv 函数没什么两样。
- msg_control 和 msg_controllen
这两个成员表示辅助数据的位置和大小。对于 recvmsg 来说,这个参数也是一个值-结果参数。关于辅助数据的含义,我们先不讨论它,以后我们遇到它的时候,再进行详细学习。因为就目前来说,我们还用不上它。
- msg_flags
这个成员只针对 recvmsg 函数有效。
recvmsg 被调用时,recvmsg 的最后一个参数 flags 的值会被复制到 msg_flags 成员并交由内核使用。当 recvmsg 返回时,msg_flags 会保存返回结果,因此 msg_flags 也是一个值结果参数。msg_flags 的返回结果可能是下面这些值:MSG_EOR、MSG_OOB、MSG_BCAST、MSG_MCAST、MSG_TRUNC、MSG_CTRUNC、MSG_NOTIFCATION. 千万别晕菜,这些东西你目前来说不需要去理解它是干嘛的……所以我们目前忽略掉它。
sendmsg 要想使用 flags,只能使用 sendmsg 函数的最一个参数,而不应该使用 msg_flags 成员。
3. 实验
根据第 2 节中的情况,我们掌握前 4 个 msghdr{} 的前 4 个成员:msg_name, msg_namelen, msg_iov, msg_iovlen 即可。实验中,我们将之前写的 udp 回射程序进行改写,将 sendto 和 recvfrom 改成 sendmsg 和 recvmsg 即可。
程序路径:
git clone https://git.oschina.net/ivan_allen/unp.git
如果你已经 clone 过这个代码了,请使用 git pull
更新一下。本节程序所使用的程序路径是 unp/program/advcio/rwmsg
udp 回射程序主要修改了两个地方:
- 将客户端的 sendto 修改成了 sendmsg
- 将服务器的 recvfrom 修改成了 recvmsg
3.1 程序代码
- 服务器端关键代码
void doServer(int sockfd) { char buf[4096]; int nr, nw; struct sockaddr_in cliaddr; struct msghdr msg; struct iovec iov; socklen_t len; while(1) { // 填充 msghdr{} 结构体 iov.iov_base = buf; iov.iov_len = 4096; msg.msg_name = &cliaddr; msg.msg_namelen = sizeof(cliaddr); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = NULL; msg.msg_controllen = 20; msg.msg_flags = 0; // 接收数据 nr = recvmsg(sockfd, &msg, 0); if (nr < 0) { if (errno == EINTR) continue; ERR_EXIT("recvfrom"); } // 后面的部分是将数据处理后发送给客户端 // ... } } }
- 客户端代码
void doClient(int sockfd) { int ret, len, nr, nw; struct sockaddr_in servaddr; char prompt[64]; char buf[4096]; struct msghdr msg; struct iovec iov[2]; strcpy(prompt, "send: "); ret = resolve(g_option.hostname, g_option.port, &servaddr); if (ret < 0) ERR_EXIT("resolve"); while(1) { nr = iread(STDIN_FILENO, buf, 4096); if (nr < 0) { ERR_EXIT("iread"); } else if (nr == 0) break; // 填充 msghdr{} 结构体 iov[0].iov_base = prompt; iov[0].iov_len = strlen(prompt); iov[1].iov_base = buf; iov[1].iov_len = nr; msg.msg_name = &servaddr; msg.msg_namelen = sizeof(servaddr); msg.msg_iov = iov; msg.msg_iovlen = 2; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_flags = 0; nw = sendmsg(sockfd, &msg, 0); // 从服务器接收数据 // ... } }
3.2 运行结果
- 客户端
图2 客户端运行结果
- 服务器
图3 服务器运行结果
4. 总结
- 掌握 recvmsg 和 sendmsg 的基本用法