在 socket 编程中,send()
是用于在已连接的套接字上发送数据的系统调用,主要用于 TCP 协议(也可用于 UDP,但需配合连接操作)。它负责将用户态的数据传递到内核缓冲区,再由内核协议栈(如 TCP/IP)完成实际的网络传输。以下从函数原型、参数、返回值、工作原理、使用细节等方面详细讲解:
一、函数原型与头文件
send()
的函数原型如下,需要包含 <sys/socket.h>
头文件:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
二、参数详解
1. sockfd
(套接字描述符)
- 含义:已建立连接的套接字的文件描述符(由
socket()
创建,并通过connect()
或accept()
完成连接)。 - 注意:
send()
仅用于已连接的套接字(TCP 中通过connect()
连接的客户端套接字,或accept()
返回的服务端连接套接字;UDP 需先通过connect()
绑定目标地址,否则需用sendto()
)。
2. buf
(数据缓冲区)
- 含义:指向用户态内存中待发送数据的缓冲区(如字符串、字节数组等)。
- 要求:
- 缓冲区必须是有效的(不能为
NULL
),且长度至少为len
字节。 - 数据在发送前需由用户确保格式正确(如网络字节序转换、协议封装等)。
- 缓冲区必须是有效的(不能为
3. len
(数据长度)
- 含义:要发送的数据的字节数(
size_t
类型,非负整数)。 - 限制:
- 实际发送的最大长度受限于内核发送缓冲区大小(可通过
SO_SNDBUF
选项查询/设置)。 - UDP 中,
len
不能超过协议最大传输单元(MTU,通常约 1500 字节),否则可能被分片或丢弃;TCP 无此限制(会自动分片)。
- 实际发送的最大长度受限于内核发送缓冲区大小(可通过
4. flags
(标志位)
- 含义:控制发送行为的标志,可取值为 0 或以下宏的组合(通过位或
|
连接):MSG_OOB
:发送带外数据(TCP 紧急数据),用于传递高优先级信息(如中断信号)。带外数据有独立的传输通道,不占用常规数据缓冲区。MSG_DONTWAIT
:非阻塞模式发送(覆盖套接字本身的阻塞属性)。若内核缓冲区已满,不会阻塞等待,而是立即返回-1
并设置errno = EAGAIN
或EWOULDBLOCK
。MSG_NOSIGNAL
:当连接已关闭(如对端断开)时,不产生SIGPIPE
信号(默认会产生,可能导致进程终止)。MSG_MORE
:告知内核“后续还有数据要发送”,用于批量发送时减少 TCP 分段(优化网络效率)。
三、返回值
- 成功:返回实际发送的字节数(
ssize_t
类型,可能小于len
)。
原因:内核发送缓冲区可能不足以容纳全部len
字节数据,此时仅发送部分数据(常见于非阻塞模式或高负载场景)。 - 失败:返回
-1
,并设置errno
标识错误原因,常见错误码:EBADF
:sockfd
不是有效的文件描述符。ENOTCONN
:套接字未连接(如 TCP 未调用connect()
,或 UDP 未绑定目标地址)。EAGAIN
/EWOULDBLOCK
:非阻塞模式下,内核缓冲区已满,暂时无法发送(可重试)。EPIPE
:连接已断开(如对端关闭),且未设置MSG_NOSIGNAL
(此时可能伴随SIGPIPE
信号)。EINTR
:发送过程被信号中断(可重试)。
四、工作原理
send()
的核心是“将用户数据复制到内核缓冲区”,而非直接发送到网络,具体流程如下:
- 参数校验:内核检查
sockfd
有效性、buf
内存可访问性、len
合理性等。 - 缓冲区检查:查看套接字对应的内核发送缓冲区(大小由
SO_SNDBUF
控制)是否有足够空间容纳数据。 - 数据复制:
- 若缓冲区有空间,将
buf
中最多len
字节的数据复制到内核缓冲区(用户态 → 内核态拷贝)。 - 若空间不足:
- 阻塞模式:进程进入睡眠,等待缓冲区有空间后再复制(直到超时或被信号唤醒)。
- 非阻塞模式(
MSG_DONTWAIT
或套接字设为O_NONBLOCK
):立即返回-1
并设置EAGAIN
。
- 若缓冲区有空间,将
- 协议处理:内核协议栈(如 TCP)负责后续工作:
- TCP:将缓冲区数据按 MSS(最大分段大小)分片,添加 TCP 头部,通过 IP 层发送;并维护重传队列,等待对端 ACK 确认后释放缓冲区。
- UDP(已
connect()
):将数据封装成 UDP 数据报,添加头部后直接发送(不保证到达,无确认机制)。
- 返回结果:返回实际复制到内核缓冲区的字节数(即“已提交给内核发送的数据量”,而非“已到达对端的数据量”)。
五、关键特性与使用细节
1. 与 write()
的关系
send()
可视为 write()
的“增强版”:
write(sockfd, buf, len)
等价于send(sockfd, buf, len, 0)
(即flags=0
时功能相同)。send()
的优势是通过flags
支持更多控制(如带外数据、非阻塞等)。
2. 处理“部分发送”
send()
的返回值可能小于 len
(如内核缓冲区不足),因此必须循环发送直到所有数据发送完毕,示例:
// 循环发送完整数据
ssize_t send_all(int sockfd, const void *buf, size_t len, int flags) {size_t total_sent = 0; // 已发送总字节数const char *p = buf; // 指向未发送数据的指针while (total_sent < len) {ssize_t sent = send(sockfd, p + total_sent, len - total_sent, flags);if (sent == -1) {// 若被信号中断,可重试;其他错误返回失败if (errno == EINTR) continue;return -1;}total_sent += sent;}return total_sent; // 全部发送成功
}
3. TCP 与 UDP 中的差异
特性 | TCP 中 send() | UDP 中 send() (需先 connect() ) |
---|---|---|
连接要求 | 必须通过 connect() /accept() 连接 | 需先 connect() 绑定目标地址(否则用 sendto() ) |
可靠性 | 依赖 TCP 重传机制,保证数据有序到达 | 无可靠性保证,数据可能丢失、乱序 |
数据边界 | 无边界(粘包),send(100) 可能对应多次 recv | 有边界(数据报),send(100) 对应一次 recv(100) |
发送缓冲区满时行为 | 阻塞等待或返回 EAGAIN (非阻塞) | 直接丢弃数据并返回 -1 (errno=EAGAIN ) |
4. 带外数据(MSG_OOB
)
- 用途:发送高优先级数据(如“紧急中断”),TCP 会优先传输,对端可通过
SO_OOBINLINE
选项或recv(..., MSG_OOB)
接收。 - 限制:带外数据只有 1 字节有效(TCP 紧急指针指向的字节),通常用于“通知”而非传输大量数据。
5. 非阻塞模式注意事项
- 当
flags=MSG_DONTWAIT
或套接字设为O_NONBLOCK
时,send()
可能返回EAGAIN
(缓冲区满),此时需通过select()
/poll()
/epoll
监听套接字可写事件后再重试。 - 示例:结合
epoll
处理非阻塞发送:// 监听套接字可写事件 struct epoll_event ev, events[10]; int epfd = epoll_create(1); ev.events = EPOLLOUT; // 关注可写事件 ev.data.fd = sockfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);// 等待可写 int nfds = epoll_wait(epfd, events, 10, -1); for (int i = 0; i < nfds; i++) {if (events[i].data.fd == sockfd && events[i].events & EPOLLOUT) {// 此时缓冲区有空间,可尝试发送ssize_t sent = send(sockfd, buf, len, MSG_DONTWAIT);// 处理发送结果...} }
六、常见错误与解决
-
SIGPIPE
信号导致进程终止
原因:对已关闭的连接调用send()
,默认会产生SIGPIPE
信号(默认处理是终止进程)。
解决:发送时设置MSG_NOSIGNAL
标志,或忽略SIGPIPE
信号(signal(SIGPIPE, SIG_IGN)
)。 -
发送数据量远小于
len
原因:内核发送缓冲区过小,或网络拥塞导致缓冲区未及时释放。
解决:- 增大发送缓冲区(
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size))
)。 - 采用非阻塞模式 + I/O 多路复用(
epoll
等),避免阻塞等待。
- 增大发送缓冲区(
-
UDP 发送失败(
errno=EMSGSIZE
)
原因:len
超过 MTU 且未开启分片(或对端不支持分片)。
解决:限制 UDP 数据长度(通常 ≤ 1472 字节,即 MTU 1500 - IP 头部 20 - UDP 头部 8)。
总结
send()
是 socket 编程中发送数据的核心系统调用,其核心功能是将用户数据复制到内核缓冲区,由协议栈完成实际传输。使用时需注意:
- 处理“部分发送”,通过循环确保数据完整发送;
- 根据场景选择
flags
(如非阻塞、带外数据); - 区分 TCP 与 UDP 中的行为差异;
- 处理常见错误(如
EAGAIN
、SIGPIPE
)。
理解 send()
的工作原理(尤其是内核缓冲区的角色)是编写可靠网络程序的关键。