【Linux C | 网络编程】进程池零拷贝传输的实现详解(四)
上一篇解决了进程池中进行大文件传输的问题,通过循环接收和发送指定大小的内容实现大文件的可靠传输。
【Linux C | 网络编程】进程池大文件传输的实现详解(三)
但是其中不可避免的在循环中使用多次的send和recv,这就涉及到多次从内核缓冲区到用户缓冲区的切换,这其中就会增加文件传输的耗时。为了减少这些不必要的系统开销,可以使用一些零拷贝的技术,提升文件传输的效率。
1.使用mmap开辟一块内存映射区
![](https://i-blog.csdnimg.cn/direct/ec2aa0f8a85e4478b979e69995e06e5e.png)
![](https://i-blog.csdnimg.cn/direct/18d6c2b5d0f5454fa2aa19331b562b80.png)
客户端
#include "process_pool.h"
#define FILENAME "bigfile.avi"
//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
int left = len;
const char* pbuf = buff;
int ret = -1;
while(left > 0) {
ret = send(sockfd, pbuf, left, 0);
if(ret < 0) {
perror("send");
return -1;
}
left -= ret;
pbuf += ret;
}
return len - left;
}
int transferFile(int peerfd)
{
//读取本地文件
int fd = open(FILENAME, O_RDWR);
ERROR_CHECK(fd, -1, "open");
//获取文件的长度
struct stat st;
memset(&st, 0, sizeof(st));
fstat(fd, &st);
char buff[100] = {0};
int filelength = st.st_size;
printf("filelength: %d\n", filelength);
//进行发送操作
//1. 发送文件名
train_t t;
memset(&t, 0, sizeof(t));
t.len = strlen(FILENAME);
strcpy(t.buf, FILENAME);
sendn(peerfd, &t, 4 + t.len);
//2. 再发送文件内容
//2.1 发送文件的长度
sendn(peerfd, &filelength, sizeof(filelength));
//服务器这一边采用的是零拷贝的技术
//当mmap映射成功时,pMap指向的就是内核中文件缓冲区,大小为filelength的内存地址空间,和打开的文件对象建立映射关系
char * pMap = mmap(NULL, filelength, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
int ret = send(peerfd, pMap, filelength, 0); //等于直接发送内存数据
printf("send ret: %d\n", ret);
return 0;
}
#include <func.h>
#include <unistd.h>
//接收确定的字节数的数据
int recvn(int sockfd, void * buff, int len)
{
int left = len;
char * pbuf = buff;
int ret = -1;
while(left > 0) {
ret = recv(sockfd, pbuf, left, 0);
if(ret == 0) {
break;
} else if(ret < 0) {
perror("recv");
return -1;
}
left -= ret;
pbuf += ret;
}
return len - left;
}
int main()
{
//创建客户端的套接字
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(clientfd, -1, "socket");
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
//指定使用的是IPv4的地址类型 AF_INET
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(8080);
serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//连接服务器
int ret = connect(clientfd, (struct sockaddr*)&serveraddr,
sizeof(serveraddr));
ERROR_CHECK(ret, -1, "connect");
printf("connect success.\n");
//进行文件的接收
//1. 先接收文件的名字
//1.1 先接收文件名的长度
int length = 0;
ret = recvn(clientfd, &length, sizeof(length));
printf("filename length: %d\n", length);
//1.2 再接收文件名本身
char buff[1000] = {0};
ret = recvn(clientfd, buff, length);
printf("1 recv ret: %d\n", ret);
int fd = open(buff, O_CREAT|O_RDWR, 0644);
ERROR_CHECK(fd, -1, "open");
//2. 再接收文件的内容
//2.1 先接收文件内容的长度
ret = recvn(clientfd, &length, sizeof(length));
printf("fileconent length: %d\n", length);
//对于客户端来说,当写入数据时,需要先制造一个文件空洞
ftruncate(fd, length);
//再将用户态映射到内核态, pMap指向的就是内核文件缓冲区,建立和接收文件和内存映射区
char * pMap = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
//当调用recv函数就已经完成了文件的写入操作
ret = recv(clientfd, pMap, length, MSG_WAITALL); //客户端等于直接从内存接收文件内容
printf("recv ret: %d\n", ret);
close(fd);
close(clientfd);
return 0;
}
2.sendfile
![](https://i-blog.csdnimg.cn/direct/c35a9b082c524695a0d9d51cea400e91.png)
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
参数解释
out_fd:目标文件描述符,通常是一个套接字文件描述符,用于接收数据。
in_fd:源文件描述符,通常是一个打开的文件描述符,用于读取数据。
offset:
指向一个 off_t 类型的指针,表示从源文件的哪个位置开始读取。如果 offset 不为 NULL,传输完毕后会更新此偏移量。如果 offset 为 NULL,则从当前文件偏移位置开始读取,且不会改变文件的偏移位置。
count:要传输的数据的字节数。
返回值
成功时,返回传输的字节数。
失败时,返回 -1,并设置 errno 表示错误原因。
使用 sendfile 的时候要特别注意, out_fd一般只能填写网络套接字的描述符,表示写入的文件描述
符, in_fd一般是一个磁盘文件,表示读取的文件描述符。从上述的需求可以得知, sendfile只能用
于发送文件方的零拷贝实现,无法用于接收方,并且发送文件的大小上限是2GB。
#include "process_pool.h"
#define FILENAME "bigfile.avi"
//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
int left = len;
const char* pbuf = buff;
int ret = -1;
while(left > 0) {
ret = send(sockfd, pbuf, left, 0);
if(ret < 0) {
perror("send");
return -1;
}
left -= ret;
pbuf += ret;
}
return len - left;
}
int transferFile(int peerfd)
{
//读取本地文件
int fd = open(FILENAME, O_RDWR);
ERROR_CHECK(fd, -1, "open");
//获取文件的长度
struct stat st;
memset(&st, 0, sizeof(st));
fstat(fd, &st);
char buff[100] = {0};
int filelength = st.st_size;
printf("filelength: %d\n", filelength);
//进行发送操作
//1. 发送文件名
train_t t;
memset(&t, 0, sizeof(t));
t.len = strlen(FILENAME);
strcpy(t.buf, FILENAME);
sendn(peerfd, &t, 4 + t.len);
//2. 再发送文件内容
//2.1 发送文件的长度
sendn(peerfd, &filelength, sizeof(filelength));
//零拷贝技术之sendfile
int ret = sendfile(peerfd, fd, NULL, filelength);
printf("sendfile ret: %d\n", ret);
return 0;
}
3.splice
splice函数在服务器和客户端都可以使用
splice
函数在Linux系统中用于实现文件描述符之间的零拷贝数据传输。它允许直接在内核空间中进行数据传输,避免了用户空间和内核空间之间的数据拷贝,因此在性能上有显著的优势。
#include <fcntl.h>
#include <unistd.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
loff_t *off_out, size_t len, unsigned int flags);
参数解释
fd_in:源文件描述符,从该文件描述符中读取数据。
off_in:指向 loff_t 类型的指针,表示从源文件的哪个位置开始读取数据。如果 off_in 为 NULL,则从当前文件偏移位置开始读取。
fd_out:目标文件描述符,向该文件描述符写入数据。
off_out:指向 loff_t 类型的指针,表示从目标文件的哪个位置开始写入数据。如果 off_out 为 NULL,则从当前文件偏移位置开始写入。
len:要传输的数据的长度,以字节为单位。
flags:控制传输行为的标志位,可以是以下值的组合:
SPLICE_F_MOVE:使用移动数据模式,将数据从 fd_in 移动到 fd_out,而不是复制。
SPLICE_F_NONBLOCK:非阻塞模式,使得 splice 调用不会阻塞,立即返回。
SPLICE_F_MORE:提示内核还有更多数据要传输,对性能有优化作用。
返回值
成功时,返回实际传输的字节数。
失败时,返回 -1,并设置 errno 表示错误原因。
服务端:
#include "process_pool.h"
#define FILENAME "bigfile.avi"
//sendn函数可以发送确定的字节数
int sendn(int sockfd, const void * buff, int len)
{
int left = len;
const char* pbuf = buff;
int ret = -1;
while(left > 0) {
ret = send(sockfd, pbuf, left, 0);
if(ret < 0) {
perror("send");
return -1;
}
left -= ret;
pbuf += ret;
}
return len - left;
}
int transferFile(int peerfd)
{
//读取本地文件
int fd = open(FILENAME, O_RDWR);
ERROR_CHECK(fd, -1, "open");
//获取文件的长度
struct stat st;
memset(&st, 0, sizeof(st));
fstat(fd, &st);
char buff[100] = {0};
int filelength = st.st_size;
printf("filelength: %d\n", filelength);
//进行发送操作
//1. 发送文件名
train_t t;
memset(&t, 0, sizeof(t));
t.len = strlen(FILENAME);
strcpy(t.buf, FILENAME);
sendn(peerfd, &t, 4 + t.len);
//2. 再发送文件内容
//2.1 发送文件的长度
sendn(peerfd, &filelength, sizeof(filelength));
int fds[2];
pipe(fds);//创建一条匿名管道
//零拷贝技术之splice
int sendSize = 0;
int ret = 0;
while(sendSize < filelength) {
//从文件描述符读数据,发送到splice管道写端,每次4k
ret = splice(fd, NULL, fds[1], NULL, 4096, SPLICE_F_MORE);
//从splice管道读端读数据,发送到通信套接字,然后通过网络(网络缓冲区)到客户端
ret = splice(fds[0], NULL, peerfd, NULL, ret, SPLICE_F_MORE);
sendSize += ret;
}
printf("splice ret: %d\n", sendSize);
return 0;
}
原文地址:https://blog.csdn.net/qq_42037383/article/details/140738961
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!