第9章 接收数据:recv()函数详解、recvfrom()函数详解、接收缓冲区管理
好,咱们继续往下走。上一章我们把数据发送出去了,那数据怎么收回来呢?这章我就跟你好好聊聊接收数据的那些事儿。
说实话,接收数据比发送数据要复杂那么一丢丢。为啥?因为你永远不知道对方什么时候发数据,发多少数据。这就好比你在等快递,不知道快递员几点来,也不知道包裹有多大。嗯,咱们得学会怎么优雅地收包裹。
9.1 recv()函数:TCP专用接收器
先看recv(),这个函数是专门给TCP用的。我个人习惯在TCP编程中只用recv(),不用read(),虽然它们底层差不多,但recv()多了个flags参数,灵活得多。
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
- sockfd:套接字描述符,就是你创建的那个socket
- buf:接收缓冲区指针,数据放这儿
- len:缓冲区大小,告诉内核最多收多少字节
- flags:标志位,一般传0,但有些场景很有用
返回值:成功返回接收到的字节数,返回0表示对方关闭连接,返回-1表示出错。
核心要点:recv()不保证一次收完所有数据。TCP是流协议,没有消息边界。你发100字节,对方可能分两次收,一次50字节。这是新手最容易踩的坑。
9.2 flags参数:那些你可能没用过的选项
flags参数很多人直接传0,其实有几个标志位挺实用的。我在项目中用过MSG_PEEK,那感觉就像开了上帝视角。
| 标志位 | 作用 | 我什么时候用过 |
|---|---|---|
| MSG_PEEK | 偷看数据,但不从缓冲区移除 | 需要先检查数据头再决定怎么收 |
| MSG_WAITALL | 阻塞直到收满len字节 | 收固定长度的协议头时 |
| MSG_DONTWAIT | 非阻塞模式,没有数据直接返回 | 配合select/epoll使用 |
小技巧:MSG_PEEK + recv()的组合,可以用来实现"先看一眼再决定"的逻辑。比如你先看4字节判断消息类型,再决定后续怎么收。不过要注意,这玩意儿性能一般,别在循环里频繁用。
9.3 recvfrom()函数:UDP的接收方式
UDP就不一样了。UDP是数据报协议,一次recvfrom()收一个完整的数据报。你发100字节,我recvfrom()一次就收100字节,不多不少。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
多出来的两个参数:
- src_addr:发送方的地址信息,传NULL表示不关心
- addrlen:地址结构体长度,传入传出参数
为什么要多这两个参数?因为UDP是无连接的,你不知道数据是谁发的。recvfrom()能告诉你"这包数据是哪个IP哪个端口发来的"。这在实现服务器时特别重要,你得知道往哪回数据。
避坑指南:我曾经在UDP服务器中犯过一个低级错误——recvfrom()的addrlen参数忘记初始化。这个参数必须先设置成sizeof(struct sockaddr_in),否则函数调用会失败。嗯,这个坑我踩过,你别再踩了。
9.4 接收缓冲区管理:内核里的那个"邮箱"
每个socket在内核里都有一个接收缓冲区。你可以把它想象成一个邮箱。数据到了,内核帮你放进邮箱。你调用recv()/recvfrom(),就是从邮箱里取信。
接收缓冲区的大小直接影响程序性能。太小了,数据频繁溢出丢包;太大了,浪费内存。我一般根据业务场景来调。
// 获取接收缓冲区大小
int rcvbuf;
socklen_t optlen = sizeof(rcvbuf);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &optlen);
printf("接收缓冲区大小: %d 字节\n", rcvbuf);
// 设置接收缓冲区大小
int new_rcvbuf = 262144; // 256KB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &new_rcvbuf, sizeof(new_rcvbuf));
注意:内核实际分配的缓冲区大小会是设置值的两倍。你设128KB,内核可能给你256KB。这是内核的"潜规则",为了管理方便。我记得第一次发现这个时还以为是bug,查了半天文档才明白。
9.5 阻塞与非阻塞:两种接收模式
默认情况下,recv()是阻塞的。没有数据时,线程就挂在那等。这其实挺好的,省CPU。但如果你要做高并发,就得用非阻塞模式。
非阻塞模式下,recv()没有数据直接返回-1,errno设为EAGAIN或EWOULDBLOCK。这时候你不能慌,这不是错误,只是告诉你"现在没数据,你过会儿再来"。
// 设置非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// 非阻塞接收
ssize_t n = recv(sockfd, buf, sizeof(buf), 0);
if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 没数据,正常情况,继续干别的
printf("暂时没数据,我先忙别的\n");
} else {
// 真正的错误
perror("recv error");
}
}
9.6 接收数据的常见陷阱
做网络编程这么多年,我总结了几条接收数据的"血泪教训":
- TCP粘包问题:TCP是流,没有边界。你发两次"hello",对方可能一次收到"hellohello"。解决方案:自定义协议,加长度字段或分隔符。
- 缓冲区溢出:recv()的len参数一定要小于等于buf的实际大小。我见过有人传了1024,但buf只分配了100字节,结果数据一多就崩了。
- 信号中断:recv()被信号中断时会返回-1,errno是EINTR。这时候应该重试,而不是直接报错。
- UDP数据报截断:如果buf太小,装不下一个完整的UDP数据报,多余的数据会被丢弃。UDP不会像TCP那样分多次给你。
我的习惯:写TCP接收逻辑时,我一般会写一个循环,直到收满期望的字节数才退出。这样能有效应对TCP的流特性。代码大概长这样:
ssize_t recv_all(int sockfd, void *buf, size_t len) {
size_t total = 0;
while (total < len) {
ssize_t n = recv(sockfd, (char*)buf + total, len - total, 0);
if (n <= 0) {
if (n == 0) break; // 对方关闭
if (errno == EINTR) continue; // 信号中断,重试
return -1; // 真错误
}
total += n;
}
return total;
}
9.7 本章知识体系
下面这张图帮你理清recv()和recvfrom()的核心脉络,以及接收缓冲区管理的要点:
9.8 实战建议
最后给你几条实在的建议:
- TCP程序一定要写循环接收,别指望一次recv()搞定所有数据
- UDP程序要注意缓冲区大小,至少要比最大数据报大,我一般设65535字节
- 生产环境建议用非阻塞模式 + epoll/poll,别用阻塞模式开多线程
- 接收缓冲区大小要根据你的数据吞吐量来调,别用默认值
好了,接收数据这块就聊到这儿。记住一句话:收数据比发数据更需要细心。你想想看,发数据主动权在你手里,收数据主动权在对方手里。把接收逻辑写稳了,你的网络程序就成功了一大半。