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 非阻塞发送

这是最容易踩坑的地方。先看一张图,理解核心区别:

阻塞发送 vs 非阻塞发送流程对比 阻塞发送 调用 send() 内核缓冲区满? 进程休眠等待 直到缓冲区有空闲才返回 非阻塞发送 调用 send() + MSG_DONTWAIT 内核缓冲区满? 立即返回 -1,errno = EAGAIN 应用层需要重试或等待 对比

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 非阻塞发送

设置非阻塞有两种方式:

  1. 创建 socket 时指定 SOCK_NONBLOCK(Linux 2.6.27+)
  2. 运行时调用 fcntl(sockfd, F_SETFL, O_NONBLOCK)
  3. 调用 send() 时传入 MSG_DONTWAIT 标志

非阻塞发送的核心行为:能发多少发多少,发不完就返回。如果缓冲区满了,直接返回 -1,errno 设为 EAGAINEWOULDBLOCK

⚠️ 避坑指南:我曾经在压测时发现,非阻塞 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