连接建立的根本目的是实现数据的可靠传输。本文详细阐述如何通过套接字接口进行数据收发,以及操作系统内核缓冲区(Buffer)在数据传输过程中的核心作用。
TCP 套接字上发送数据常用的有三个函数:
ssize_t write(int socketfd, const void *buffer, size_t size);
ssize_t send(int socketfd, const void *buffer, size_t size, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);三个函数的使用场景各不相同:
write:通用文件写函数,将 socketfd 视为文件描述符进行写入操作send:在 write 基础上增加 flags 参数,支持发送带外数据(Out-of-band Data,OOB)等选项sendmsg:支持多重缓冲区(Scatter/Gather I/O)传输数据,通过 msghdr 结构体指定多个缓冲区内核版本注记: 自 Linux 4.14 起,
sendmsg支持MSG_ZEROCOPY标志,可实现零拷贝发送。自 Linux 5.1 起,io_uring接口提供了异步发送机制。
套接字描述符与普通文件描述符均遵循 UNIX"万物皆文件"(Everything is a File)的设计哲学,可以传递给通用的 I/O 函数。然而,二者在写入行为上存在本质差异:
write 调用返回的写入字节数通常等于请求的 size,否则表示出错write 调用返回的写入字节数 可能小于 请求的 size,这是正常行为而非错误产生这一差异的根本原因在于操作系统内核为套接字通信引入了发送缓冲区(Send Buffer)机制。
TCP 连接建立后,操作系统内核为每个连接创建配套的基础设施,其中最重要的就是发送缓冲区。
发送缓冲区的大小可通过 SO_SNDBUF 套接字选项查询和设置:
int sndbuf_size;
socklen_t optlen = sizeof(sndbuf_size);
getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf_size, &optlen);内核版本注记: Linux 内核中发送缓冲区的默认值由
/proc/sys/net/ipv4/tcp_wmem控制,该参数包含三个值:最小值、默认值和最大值。默认发送缓冲区大小为 16,384 字节(16 KB),最大值受/proc/sys/net/core/wmem_max限制。
当应用程序调用 write 函数时,实际操作是将数据 从应用程序的用户空间拷贝到操作系统内核的发送缓冲区中,而非直接通过网络发送。具体行为分为以下几种情况:
情况一: 发送缓冲区有足够空间容纳应用程序数据,write 调用立即返回,返回值等于请求写入的字节数。
情况二: 发送缓冲区空间不足以容纳全部应用程序数据(可能因为缓冲区中尚有未发送的数据,或缓冲区本身容量不足)。此时,对于阻塞式套接字,应用程序在 write 调用处阻塞,直到数据完全拷贝到发送缓冲区后才返回。
内核在 TCP 连接建立后持续运作:将发送缓冲区中的数据按照 TCP/IP 协议语义封装为 MSS(Maximum Segment Size)段和 MTU(Maximum Transmission Unit)帧,通过数据链路层发送出去。缓冲区空间因此逐步释放,应用程序的数据逐步被拷贝到缓冲区中,直至全部数据写入完成。
关键结论: write 返回成功仅表示数据已拷贝到内核发送缓冲区,并不意味着数据已发送到对端,更不意味着对端已接收。
与发送数据对应,读取数据也遵循"万物皆文件"的设计原则,可以将套接字描述符传递给标准的文件读取函数。
read 函数的原型如下:
ssize_t read(int socketfd, void *buffer, size_t size);该函数请求操作系统内核从套接字描述符读取最多 size 个字节的数据,并将结果存储到 buffer 中。返回值含义如下:
errno 确定具体错误类型内核版本注记: 自 Linux 2.6.33 起,
recvmmsg()系统调用可用,支持一次调用接收多个消息,显著提升高并发场景下的接收性能。自 Linux 5.1 起,io_uring提供了异步读取机制。
read 函数返回的是"最多"读取 size 个字节,实际读取的字节数可能小于请求值。若应用程序需要确保每次读取恰好 size 个字节,必须编写循环读取逻辑:
/* 从 socketfd 描述符中读取 size 个字节 */
size_t readn(int fd, void *buffer, size_t size) {
char *buffer_pointer = buffer;
int length = size;
while (length > 0) {
int result = read(fd, buffer_pointer, length);
if (result < 0) {
if (errno == EINTR)
continue; /* 被信号中断,重新调用 read */
else
return (-1); /* 真正的错误 */
} else if (result == 0)
break; /* EOF,对端关闭连接 */
length -= result;
buffer_pointer += result;
}
return (size - length); /* 返回实际读取的字节数 */
}该函数的关键处理逻辑:
size 个字节之前持续循环errno == EINTR 表示读取被信号中断,需重新调用 read以下通过客户端-服务器端的实际程序,演示发送缓冲区与接收缓冲区的工作机制。客户端持续发送大量数据,服务器端每读取一段数据后休眠,模拟业务处理时延。
#include "lib/common.h"
void read_data(int sockfd) {
ssize_t n;
char buf[1024];
int time = 0;
for (;;) {
fprintf(stdout, "block in read\n");
if ((n = readn(sockfd, buf, 1024)) == 0)
return;
time++;
fprintf(stdout, "1K read for %d \n", time);
usleep(1000);
}
}
int main(int argc, char **argv) {
int listenfd, connfd;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(12345);
/* 绑定到本地地址,端口为 12345 */
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
/* listen 的 backlog 为 1024 */
listen(listenfd, 1024);
/* 循环处理客户端请求 */
for (;;) {
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
read_data(connfd); /* 读取数据 */
close(connfd); /* 关闭已连接套接字(非监听套接字) */
}
}程序逻辑说明:
accept 获取已连接套接字usleep(1000)),模拟服务器端业务处理时延#include "lib/common.h"
#define MESSAGE_SIZE 102400
void send_data(int sockfd) {
char *query;
query = malloc(MESSAGE_SIZE + 1);
for (int i = 0; i < MESSAGE_SIZE; i++) {
query[i] = 'a';
}
query[MESSAGE_SIZE] = '\0';
const char *cp;
cp = query;
size_t remaining = strlen(query);
while (remaining) {
int n_written = send(sockfd, cp, remaining, 0);
fprintf(stdout, "send into buffer %d \n", n_written);
if (n_written <= 0) {
error(1, errno, "send failed");
return;
}
remaining -= n_written;
cp += n_written;
}
return;
}
int main(int argc, char **argv) {
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
error(1, 0, "usage: tcpclient <IPaddress>");
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
int connect_rt = connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
if (connect_rt < 0) {
error(1, errno, "connect failed");
}
send_data(sockfd);
exit(0);
}程序逻辑说明:
connect 向服务器端发起连接请求send_data 发送数据MESSAGE_SIZE 的字符串流send 函数将全部数据发送出去客户端程序发送大量字节流,运行后可观察到服务器端持续打印读取进度信息。客户端的全部字节流发送完毕后 send 调用才会全部返回,在此之前 send 一直处于阻塞状态。
结论: 阻塞式套接字最终发送返回的实际写入字节数与请求字节数相等。
将服务器端休眠时间调大,同时调大客户端发送数据量(如将 MESSAGE_SIZE 由 102400 调整为 1024000),再次运行实验。可观察到客户端很快打印完成信息,而服务器端仍在持续读取缓冲区中的数据。
结论: 发送成功仅表示数据已从应用程序拷贝到内核发送缓冲区,并不意味着对端已收到数据。数据何时到达对端接收缓冲区、何时被对端应用程序读取,对发送端而言完全透明。
从应用程序发送端到应用程序接收端,一段数据流经过的完整路径如下:
在此路径中,数据至少经历了以下拷贝操作:
write 系统调用触发)read 系统调用触发)若使用 MSG_ZEROCOPY 标志(自 Linux 4.14 起可用),步骤 1 可避免数据拷贝,直接将应用程序内存页注册给内核使用。
本文重点阐述了套接字数据读写与缓冲区机制,关键要点如下:
write/send 返回成功仅表示数据已拷贝到内核发送缓冲区,并不表示对端已成功接收。read 函数返回的实际字节数可能小于请求值,需要通过循环读取确保读取完整数据,同时需正确处理 EOF(对端关闭连接)和 EINTR(被信号中断)等异常条件。