8、发送数据:send()函数详解、sendto()函数详解、阻塞发送与非阻塞发送
网络编程里,发送数据是基本功。但说实话,很多人写了几年代码,对 send() 和 sendto() 的理解还停留在「能用就行」的阶段。我见过不少线上事故,就是因为对这两个函数的阻塞行为判断失误导致的。今天咱们就把它们彻底讲透。
8.1 send() 函数:TCP 专属的发送接口
先看原型:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数不多,但每个都有讲究:
- sockfd:已连接的 TCP socket 描述符。注意,必须是
connect()之后的 socket。 - buf:要发送的数据缓冲区。我习惯用栈上数组或 malloc 分配的内存。
- len:期望发送的字节数。不是缓冲区大小,是你想发多少。
- flags:标志位,一般填 0。常用的有
MSG_DONTWAIT(非阻塞)和MSG_NOSIGNAL(防止 SIGPIPE)。
返回值:成功返回实际发送的字节数,失败返回 -1。
⚠️ 重要:send() 返回的字节数可能小于 len。不要假设一次 send 就能发完所有数据。我在项目中遇到过新手直接写
send(fd, buf, 10000, 0),然后发现只发了 4000 字节,后续数据全丢了。必须用循环发送。
8.2 sendto() 函数:UDP 的发送利器
UDP 是无连接的,所以发送时需要指定目标地址:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
相比 send(),多了两个参数:
- dest_addr:目标地址结构体指针。可以是 IPv4 的
struct sockaddr_in,也可以是 IPv6 的struct sockaddr_in6。 - addrlen:地址结构体的长度。
UDP 发送有个特点:sendto() 一次调用就是一个数据报。如果数据报大小超过 MTU(通常 1500 字节),IP 层会分片,但应用层感知不到。不过我个人建议 UDP 单次发送不要超过 1400 字节,避免分片带来的丢包风险。
💡 经验之谈:我曾经调试过一个 UDP 丢包问题,发现 sendto() 每次发 2000 字节,网络稍微拥塞就丢包。改成 1400 字节后,问题消失了。原因就是 IP 分片后,只要一个分片丢了,整个数据报就废了。
8.3 阻塞发送 vs 非阻塞发送
这是最容易踩坑的地方。先看一张图,理解核心区别:
8.3.1 阻塞发送
默认情况下,TCP socket 是阻塞的。当你调用 send() 时:
- 如果内核发送缓冲区有足够空间,数据直接拷贝进去,立即返回。
- 如果缓冲区满了,进程会挂起等待,直到缓冲区有空闲位置。
你想想看,这有什么问题?如果对端接收太慢,或者网络拥堵,你的程序就会卡在 send() 上。我曾经维护过一个网关程序,就是因为某个客户端不读数据,导致 send() 阻塞,整个单线程网关全部卡死。
🔑 关键点:阻塞发送的返回值一定是 > 0 吗?不一定。如果被信号中断,返回 -1,errno 为 EINTR。所以正确的写法是:
while (sent < total_len) {
n = send(fd, buf + sent, total_len - sent, 0);
if (n == -1) {
if (errno == EINTR) continue; // 被信号打断,重试
perror("send");
break;
}
sent += n;
}
8.3.2 非阻塞发送
设置非阻塞有两种方式:
- 创建 socket 时指定
SOCK_NONBLOCK(Linux 2.6.27+) - 运行时调用
fcntl(sockfd, F_SETFL, O_NONBLOCK) - 调用 send() 时传入
MSG_DONTWAIT标志
非阻塞发送的核心行为:能发多少发多少,发不完就返回。如果缓冲区满了,直接返回 -1,errno 设为 EAGAIN 或 EWOULDBLOCK。
⚠️ 避坑指南:我曾经在压测时发现,非阻塞 send() 返回 -1 后,如果直接重试,CPU 会飙升到 100%。正确的做法是配合 epoll 或 select 使用,等待 socket 可写事件再发送。不要忙等。
8.4 实际项目中的选择策略
我根据多年经验,总结了一个选择表:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 高并发服务器(如 HTTP 服务) | 非阻塞 + epoll | 避免一个连接阻塞影响其他连接 |
| 低吞吐、短连接(如 DNS 查询) | 阻塞 | 代码简单,性能足够 |
| 大文件传输 | 非阻塞 + 异步 I/O | 防止大块数据发送阻塞主流程 |
| UDP 广播/组播 | 阻塞(默认即可) | UDP 发送几乎不阻塞,缓冲区很大 |
8.5 一个完整的非阻塞发送示例
最后,给一个我项目中常用的非阻塞发送封装:
int nonblock_send(int fd, const char *data, size_t len) {
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, data + sent, len - sent, MSG_DONTWAIT);
if (n > 0) {
sent += n;
} else if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 缓冲区满了,等待可写事件
struct pollfd pfd = {.fd = fd, .events = POLLOUT};
int ret = poll(&pfd, 1, 5000); // 超时5秒
if (ret <= 0) return -1; // 超时或出错
continue;
} else if (errno == EINTR) {
continue; // 被信号打断
} else {
return -1; // 真正的错误
}
}
}
return 0;
}
这段代码我用了好几年,线上跑得很稳。核心思路就是:能发就发,发不了就等,等不到就报错。你想想看,网络编程的本质不就是处理这些不确定性吗?
💡 最后提醒:send() 和 sendto() 的 flags 参数还有
MSG_MORE(TCP 攒包发送)和 MSG_CONFIRM(ARP 缓存确认),这些属于高级用法,等后面讲到性能优化时再细聊。
公众号:蓝海资料掘金营,微信deep3321