C/C++中如何直接从套接字读取数据写入文件
问题解答
存在这类方案,核心是通过操作系统提供的零拷贝系统调用,实现套接字到文件的直接数据传输,无需在用户态手动分配/管理char类型缓冲区,和已知的sendfile()(文件到套接字零拷贝)逻辑对称。注意不存在完全不经过任何内存的传输——所有网络、磁盘IO都必然经过内核态内存缓冲区,这类方案的价值是省去用户态缓冲区分配、以及内核态和用户态之间的两次数据拷贝,性能远高于常规的recv()/read() + write()流程。
不同平台的原生实现方案
- Linux平台:
splice()系统调用
Linux没有提供和sendfile()完全对称的套接字转文件专用接口,但可以用splice()实现等价效果。splice()支持在两个文件描述符之间直接传输数据,全程不需要数据拷贝到用户态。
由于早期内核版本要求splice()的两个操作fd中必须有一个是管道类型,通用实现流程是先创建一个管道做中转:先把套接字数据splice到管道写端,再把管道读端的数据splice到目标文件fd,全程不需要用户态定义缓冲区。
极简实现示例:
高版本内核中如果两个fd都支持零拷贝操作,也可以跳过管道直接在套接字和文件fd之间调用#include <fcntl.h> #include <unistd.h> ssize_t recv_to_file(int sock_fd, int out_file_fd, size_t expect_len) { int pipe_fd[2]; if (pipe(pipe_fd) == -1) return -1; ssize_t total_trans = 0; while (expect_len > 0) { ssize_t recv_n = splice(sock_fd, NULL, pipe_fd[1], NULL, expect_len, SPLICE_F_MOVE); if (recv_n <= 0) break; // 连接断开或出错 ssize_t write_n = splice(pipe_fd[0], NULL, out_file_fd, NULL, recv_n, SPLICE_F_MOVE); if (write_n <= 0) break; total_trans += write_n; expect_len -= write_n; } close(pipe_fd[0]); close(pipe_fd[1]); return total_trans; }splice(),但管道中转的写法兼容性最好。 - FreeBSD/macOS平台:
recvfile()系统调用
这两个系统提供了和sendfile()完全对称的recvfile()接口,专门用于套接字到文件的零拷贝接收,不需要管道中转,直接传入套接字fd、文件fd、要接收的长度参数即可完成传输,接口比Linux的splice()更简洁。 - Windows平台
可通过重叠IO(Overlapped I/O)配合IOCP实现无用户态缓冲区的套接字到文件传输,从Windows 10 1809版本开始也提供了封装好的CopyFileFromSocket接口实现等价零拷贝能力。
注意事项
- 不要混淆零拷贝的边界:这类方案省去的是用户态的缓冲区和拷贝开销,数据在内核中依然会经过网络协议栈缓冲区、文件系统页缓存,不存在完全绕开内存的IO操作,这是计算机硬件和TCP协议栈的基本设计决定的。
- 常规
read()+write()流程的数据路径是:网卡 -> 内核socket缓冲区 -> 拷贝到用户态char缓冲区 -> 拷贝到内核文件页缓存 -> 写入磁盘;零拷贝方案的路径是:网卡 -> 内核socket缓冲区 -> 内核文件页缓存 -> 写入磁盘,少了两次跨态拷贝,大文件传输时性能提升非常明显。 - 如果需要跨平台兼容,可以使用封装了各平台零拷贝逻辑的网络库,不需要自己针对每个平台做适配。
内容的提问来源于stack exchange,提问作者user19260951
相关产品推荐
相关产品推荐

