【C-实践】文件服务器(2.0)

概述

使用了 tcp + epoll + 进程池,实现文件下载服务器


与1.0相比,进程池的退出方式不同,服务器的传输文件方式也不同

退出方式:采用温和手段,通知子进程让其自己退出

(在1.0的基础上,修改了 sendFdrecvFd函数,多加了一个退出标志位参数,用来指定子进程是否退出)


传输方式:采用零拷贝的方式完成(文件内容不再经过用户态缓冲区,直接从内核缓冲区发送出去)

(在1.0的基础上,增加了send_file_mmapsend_file_sendfilesend_file_splice函数)


注意


实际传输速度:主要取决于网络传输速度,而不是本地传输速度。网速不高的情况下,零拷贝接口并没有优势。


工作中不推荐使用零拷贝接口,因为这几个零拷贝接口的一致性不是很好,自定义传输协议比较常用。


功能


主要功能:客户端连接服务器,然后自动下载文件



启动


启动服务器

1、在bin目录下生成可执行文件

w@Ubuntu20:bin $ gcc ../src/*.c -o server

2、启动服务器

w@Ubuntu20:bin $ ./server ../conf/server.conf

启动客户端

1、在客户端的目录下生成可执行文件

w@Ubuntu20:client $ gcc main_client.c -o client

2、启动客户端

w@Ubuntu20:client $ ./client client.conf


目录设计

服务器

  • bin:存放二进制文件
  • conf:存放配置文件
  • include:存放头文件
  • resource:存放资源文件
  • src:存放源文件
w@Ubuntu20:src $ tree ..
..
├── bin
│   └── server
├── conf
│   └── server.conf
├── include
│   └── process_pool.h
├── resource
│   └── file
└── src
    ├── child_process.c
    ├── init_process_pool.c
    ├── interact.c
    ├── main_server.c
    ├── send_file_mmap.c
    ├── send_file_sendfile.c
    ├── send_file_splice.c
    ├── send_file_truck.c
    ├── tcp_init.c
    └── transfer_fd.c


客户端

w@Ubuntu20:client $ tree 
.
├── client
├── client.conf
├── main_client.c
├── recv_file_mmap.c
├── recv_file_splice.c
└── recv_file_truck.c


配置文件


服务器配置文件 server.conf

存放服务器ip地址,服务器port端口,进程数量

根据实际情况自行更改

192.168.160.129
2000
5

客户端配置文件 client.conf

存放服务器ip地址,服务器port端口

根据实际情况自行更改

192.168.160.129
2000


检查传输文件是否正确

  1. 查看文件大小 $ du -h file
  2. 查看文件唯一哈希值 $ md5sum file


服务器搭建

  1. 创建进程池

    1. 根据子进程的数量,创建存储子进程信息的数组
    2. 根据子进程的数量,循环创建子进程,并初始化子进程的信息(子进程id、是否空闲,通讯管道)
  2. 主进程分配任务给子进程

    1. 建立一个tcp类型的正在监听的套接字

    2. 使用epoll管理所有套接字

      1. 有新的客户端连接,得到一个客户端套接字,交给一个空闲的子进程处理
      2. 等待子进程工作完毕,将其状态设为空闲
      3. 等待退出信号,收到后回收进程池资源,退出程序
  3. 资源进程(子进程)处理具体业务

    1. 等待任务(主进程发送过来的客户端套接字)
    2. 设置本进程为忙碌状态
    3. 工作(发送文件给客户端)
    4. 通知主进程任务完成


进程池退出方式

方式一:给主进程发送退出信号,主进程收到信号后,kill所有子进程,然后回收所有子进程的资源,再退出主进程


方式二:给主进程发送退出信号,主进程收到信号后,通知所有子进程退出 (本文采用)

  1. 如果是非忙碌的子进程,直接退出
  2. 如果是忙碌的子进程,就忙完了再退出

在1.0版本的基础上,修改sendFd.crecvFd.c文件,主进程和子进程的通信中,多传递一个标志位表示是否结束进程(0-不结束,1-结束)



传输文件方式

方式一:使用自定义协议传输:先发送本次数据长度,再发送数据内容


方式二:使用零拷贝的方式传输 (本文使用)


mmap零拷贝接口

NAME
       mmap, munmap - map or unmap files or devices into memory

SYNOPSIS
       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
       int munmap(void *addr, size_t length);

注意:服务器用mmap发送的文件,客户端也需要用mmap接收,因为不再是先接数据长度再接数据内容的形式了

//服务器用mmap建立文件映射,将文件直接映射到内核发送缓冲区
char *pMap = (char*)mmap(NULL, file_info.st_size, PROT_READ, MAP_SHARED, file_fd, 0); 
ERROR_CHECK(pMap, (char*)-1, "mmap");

send(socket_fd, pMap, file_info.st_size, 0); 
munmap(pMap, file_info.st_size);
//客户端也需要用mmap接收,或者用while+recv接收(直接接收数据,不用先接收数据长度)
ftruncate(fd, filesize);//先固定文件大小
char *pMap = (char*)mmap(NULL, filesize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 
ERROR_CHECK(pMap, (char*)-1, "mmap");
  
recv(sfd, pMap, filesize, MSG_WAITALL);	//要用MSG_WAITALL参数,避免没有全部接收到
munmap(pMap, filesize);



sendfile零拷贝接口

在两个文件描述符之间直接发送文件,将in_fd发给out_fd

NAME
       sendfile - transfer data between file descriptors

SYNOPSIS
       #include <sys/sendfile.h>

       ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);



splice零拷贝接口

splice传输必须要借助管道

将数据写入管道,再从管道传给发送缓冲区(是不经过用户态空间的)

管道最多可以存储65535个字节,如果文件较大则需要循环传输

splice没有字节数限制,比上面两个好用一点,可以控制发送的快慢(第5个参数决定每次传输的字节数量)

NAME
       splice - splice data to/from a pipe

SYNOPSIS
       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <fcntl.h>

       ssize_t splice(int fd_in, loff_t *off_in, int fd_out,
                      loff_t *off_out, size_t len, unsigned int flags);




代码实现逻辑


main_server.c 服务器主流程


步骤:

  1. 从配置文件中拿到,本服务器ip地址、port端口号、进程数量
  2. 创建一个子进程数组,用来存储所有子进程的信息
  3. 创建进程池,并用子进程数组记录子进程的信息(根据子进程的数组和子进程的数量)
  4. 建立退出管道,并注册SIGUSR1信号(用于主进程的异步退出)
  5. 创建一个tcp类型的服务器套接字用于监听客户端的连接
  6. 处理来自客户端和进程池的请求,以及退出信号
    1. 将每一个客户端的连接交给空闲子进程,
    2. 将请求的忙碌子进程设为空闲状态
    3. 收到退出信号,依次终止子进程,回收子进程资源,退出主进程
  7. 最后释放子进程数组的空间


init_process_pool.c 创建进程池


输入:子进程数组pChilds,子进程的数量childsNum

输出:一个有childsNum个子进程信息的数组



为什么用socketpair生成一对套接口,而不是用管道等方式在进程间传递套接字(文件描述符)?

每一个进程都会维护一个数字与文件描述符对应的表

每个文件描述符都会在内核中维护一个文件对象数据结构的,不仅仅是一个数字

而用管道传输文件描述符时,只会传送数字,而不会传送文件对象

因此需要特殊的接口,在进程之间,传递文件描述符的数据结构



步骤:

  1. 循环childsNum次,创建子进程

    1. 使用socketpair创建一对用于本地通信的tcp类型的套接口fds[2](全双工管道,用于传递客户端套接字)

    2. fork出一个子进程

    3. 子进程设置

      1. 关闭套接口的写端fds[1](子进程只需要从套接口中读取客户端套接字)
      2. 子进程业务逻辑
      3. 子进程退出
    4. 主进程设置,将新建的子进程信息放入子进程数组内

      1. 关闭套接口的读端fds[0](主进程只需要向套接口中写入客户端套接字)
      2. 记录第i个子进程的进程id
      3. 设置第i个子进程的状态为空闲
      4. 设置第i个子进程的通信管道为fds[0]


child_process.c 子进程业务逻辑


输入:子进程套接口

输出:将目标文件发送给客户端


步骤:

  1. 死循环,处理业务
    1. 等待任务:阻塞在套接口,等待主进程发来的客户端套接字
    2. 干活:将目标文件发送给客户端
    3. 任务结束:通知主进程任务完成



sendFd.c 主进程向子进程发送客户端套接字


输入:子进程的套接口,客户端套接字

输出:使用sendmsg接口,将客户端套接字发送给子进程



sendmsg接口

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

struct msghdr {
               void         *msg_name;       /* Optional address */  
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags (unused) */
           };

struct iovec {
    void *iov_base;		//缓冲区起始位置
    size_t iov_len;		//传输的字节数
};

struct cmsghdr {
    socklen_t cmsg_len;		//用CMSG_LEN()宏计算,宏里是传输数据的长度
    int cmsg_level;			//原始协议,本程序用SOL_SOCKET
    int cmsg_type;			//特定协议类型,本程序用SCM_RIGHTS
    unsigned char cmsg_data[];	//可变长数组,使用CMSG_DATA()宏存储,要传输的客户端套接字放在这
};


msghdr前两个成员用于udp,不用写
msghdr的iov数组必须写,可以存一些数据,不想存可以随便写一个
msghdr的control成员,就是用来传输客户端套接字文件对象的

步骤:

  1. 初始化一个struct msghdr结构体msg,用来传递客户端套接字
  2. 创建一个struct iovec结构体,初始化&赋值,然后设为msg的参数
  3. 创建一个struct cmsghdr结构体cmsg,初始化,然后设为msg的参数
    1. CMSG_LEN宏得到cmsg的大小
    2. cmsg->cmsg_level = SOL_SOCKET; 原始协议
    3. cmsg->cmsg_type = SCM_RIGHTS; 特定协议类型
    4. 传输的客户端套接字 *(int*)CMSG_DATA(cmsg) = cli_fd;
  4. msg向子进程套接口发送


recvFd.c 子进程从主进程接收客户端套接字


输入:子进程的套接口,客户端套接字地址

输出:使用recvmsg接口,从主进程接收客户端套接字


recvmsg接口

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

struct msghdr {
               void         *msg_name;       /* Optional address */  
               socklen_t     msg_namelen;    /* Size of address */
               struct iovec *msg_iov;        /* Scatter/gather array */
               size_t        msg_iovlen;     /* # elements in msg_iov */
               void         *msg_control;    /* Ancillary data, see below */
               size_t        msg_controllen; /* Ancillary data buffer len */
               int           msg_flags;      /* Flags (unused) */
           };

struct iovec {
    void *iov_base;		//缓冲区起始位置
    size_t iov_len;		//传输的字节数
};

struct cmsghdr {
    socklen_t cmsg_len;		//用CMSG_LEN()宏计算,宏里是传输数据的长度
    int cmsg_level;			//原始协议,本程序用SOL_SOCKET
    int cmsg_type;			//特定协议类型,本程序用SCM_RIGHTS
    unsigned char cmsg_data[];	//可变长数组,使用CMSG_DATA()宏存储,要传输的客户端套接字放在这
};


msghdr前两个成员用于udp,不用写
msghdr的iov数组必须写,可以存一些数据,不想存可以随便写一个
msghdr的control成员,就是用来传输客户端套接字文件对象的

步骤:

  1. 初始化一个struct msghdr结构体msg,用来接收客户端套接字
  2. 创建一个struct iovec结构体,初始化,然后设为msg的参数
  3. 创建一个struct cmsghdr结构体cmsg,初始化,然后设为msg的参数
    1. CMSG_LEN宏得到cmsg的大小
    2. cmsg->cmsg_level = SOL_SOCKET; 原始协议
    3. cmsg->cmsg_type = SCM_RIGHTS; 特定协议类型
    4. 传输的客户端套接字 *(int*)CMSG_DATA(cmsg) = cli_fd;
  4. 从套接口中用recvmsg接收msg
  5. 从msg中提取客户端套接字


tcp_init.c 生成一个服务器正在监听的tcp套接字


输入:服务器的ip地址,服务器的port端口号

输出:绑定了服务器ip和port,正在监听的tcp类型的套接字


步骤:

  1. 使用socket生成一个tcp类型的套接字
  2. 给套接字绑定服务器的ip地址和port端口号
  3. 开始监听



interact_cli.c 主进程处理客户端和进程池请求,以及退出信号


输入:服务器套接字,子进程数组,子进程数量,退出管道读端

输出:将客户端的请求转发给空闲子进程,将完成任务的子进程设为空闲状态,如果收到退出信号则回收所有子进程资源并退出


步骤:

  1. 创建epoll管理所有请求
    1. 将服务器套接字,加入epoll,用于接收客户端请求
    2. 将子进程数组内的所有通信管道,加入epoll,用于处理子进程的请求
    3. 将退出管道读端,加入epoll,用于接收退出信号
  2. epoll循环等待就绪的文件描述符
    1. 如果服务器套接字就绪,接收客户端套接字并其交给一个空闲子进程处理,然后关闭客户端套接字
    2. 如果子进程的管道就绪(表示子进程已处理完一个任务),读取管道,然后将该子进程的状态设为空闲
    3. 如果收到退出信号,依次关闭子进程,回收所有子进程的资源,然后退出主进程



send_file_truck 私有协议发送文件


输入:客户端套接字,待传输文件名

输出:使用私有协议将文件传输给客户端


自定义传输文件协议:小货车

//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

步骤:

  1. 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
  2. 将文件名添加上资源目录的路径,再open打开待传文件
  3. 传输中
    1. 先发文件名
    2. 再发文件大小
    3. 循环发送文件内容(小货车每次最多发1000个字节)
      1. 给小货车装车,发货
      2. 如果全部传输完毕之后,通知客户端,并退出循环
      3. 如果客户端异常断开,则退出循环(此时会收到SIGPIPE信号)
  4. 传输结束,关闭待传文件



send_file_mmap 零拷贝发送文件


输入:客户端套接字,待传输文件名

输出:用mmap零拷贝发送文件


自定义传输文件协议:小货车

//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

步骤:

  1. 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
  2. 将文件名添加上资源目录的路径,再open打开待传文件
  3. 传输中
    1. 先发文件名(用小货车发)
    2. 再发文件大小(用小货车发)
    3. 对文件建立mmap映射,整体发送,然后解除映射
  4. 传输结束,关闭待传文件


send_file_sendfile 零拷贝发送文件


输入:客户端套接字,待传输文件名

输出:用sendfile零拷贝发送文件


自定义传输文件协议:小货车

//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

步骤:

  1. 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
  2. 将文件名添加上资源目录的路径,再open打开待传文件
  3. 传输中
    1. 先发文件名(用小货车发)
    2. 再发文件大小(用小货车发)
    3. sendfile接口直接发送文件
  4. 传输结束,关闭待传文件


send_file_splice 零拷贝发送文件


输入:客户端套接字,待传输文件名

输出:用splice零拷贝发送文件


自定义传输文件协议:小货车

//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

步骤:

  1. 初始化一个小货车
  2. 将文件名添加上资源目录的路径,再open打开待传文件
  3. 传输中
    1. 先发文件名(用小货车发)
    2. 再发文件大小(用小货车发)
    3. 创建一对管道
    4. 循环发送文件内容
      1. 先将文件发送给管道
      2. 再从管道放入发送缓冲区
  4. 传输结束,关闭待传文件


main_client.c 客户端主流程


命令行参数:配置文件(服务器ip地址,服务器端口号)


方式一:私有协议循环下载

步骤:

  1. 读取配置文件,拿到服务器的ipport
  2. 生成一个tcp类型的套接字,并绑定服务器的ip和端口
  3. 申请连接服务器
  4. 下载文件(根据自定义传输协议小货车接收:先接受数据长度,再根据长度接收数据)
    1. 先接受文件名,根据文件名open一个新文件
    2. 再接收文件大小(为了打印接收进度条)
    3. 循环接收文件内容(根据协议,每次最多接收1000个字节)
      1. 先接收数据长度(如果为空则表示接收完毕,退出循环)
      2. 根据数据长度,接收数据内容
      3. 根据当前进度和总大小打印进度条(用fflush刷新标准输出,避免光标跳动)
      4. 将数据写入文件
    4. 关闭文件
  5. 关闭服务器套接字

方式二:mmap零拷贝下载

步骤:

  1. 读取配置文件,拿到服务器的ipport
  2. 生成一个tcp类型的套接字,并绑定服务器的ip和端口
  3. 申请连接服务器
  4. 下载文件
    1. 先接受文件名,根据文件名open一个新文件
    2. 再接收文件大小
    3. 通过文件映射接收数据
      1. 根据文件大小,固定文件的空间
      2. 对新文件建立映射,整体接收,解除映射
    4. 关闭文件
  5. 关闭服务器套接字

方式三:用splice零拷贝下载

步骤:

  1. 读取配置文件,拿到服务器的ipport
  2. 生成一个tcp类型的套接字,并绑定服务器的ip和端口
  3. 申请连接服务器
  4. 下载文件
    1. 先接受文件名,根据文件名open一个新文件
    2. 再接收文件大小
    3. 创建一对管道,用于splice接口
    4. 循环接收文件内容
      1. 从服务器下载到管道
      2. 从管道写入文件
    5. 关闭文件
  5. 关闭服务器套接字


具体代码

服务器代码


prcess_pool.h 服务器头文件

比1.0多个三个个接口 send_file_mmapsend_file_sendfilesend_file_splice

#ifndef __PROCESSPOOL_H__
#define __PROCESSPOOL_H__

#include <stdlib.h>

//检查命令行参数个数
#define ARGS_CHECK(argc, num) { if (argc != num) {\
    fprintf(stderr, "Args error!\n"); return -1; }}

//检查系统调用返回值是否合法,非法报错退出
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror("msg");  return -1;  } }

//输入:服务器的ip地址,端口号
//输出:绑定了服务器ip和端口的,正在监听的套接字
int tcp_init(char *ip, int port);


//记录进程信息的结构体
typedef struct 
{
    short _flag;//进程是否空闲 0-是  1-不是
    int _pipefd;//父子进程通信的管道
    pid_t _pid;//进程id
}ProcInfo_t, *pProcInfo_t;

//功能:创建进程池
//参数:子进程数组,子进程数量
int init_process_pool(pProcInfo_t, int);

//功能:服务器主进程处理来自客户端的请求
//参数:服务器套接字,子进程数组,子进程数量,退出管道读端
int interact_cli(int sfd, pProcInfo_t pChilds, int childsNum, int exitpipe);

//功能:将客户端套接字发送给子进程
//参数:子进程套接口,客户端套接字,退出标志位
int sendFd(int pipefd, int cli_fd, int exit_flag);

//功能:从主进程接收客户端套接字
//参数:子进程套接口,客户端套接字地址,退出标志位地址
int recvFd(int pipefd, int *cli_fd, int *exit_flag);

//功能:资源进程的配置
//参数:与主进程通信的套接字
int child_process(int pipefd);

//功能:给客户端套接字发送文件,用私有传输协议
//参数:客户端套接字,文件名
int send_file_truck(int socket_fd, char *filename);

//功能:给客户端套接字发送文件,用mmap零拷贝接口
//参数:客户端套接字,文件名
int send_file_mmap(int socket_fd, char *filename);

//功能:给客户端套接字发送文件,用sendfile零拷贝接口
//参数:客户端套接字,文件名
int send_file_sendfile(int socket_fd, char *filename);

//功能:给客户端套接字发送文件,用splice零拷贝接口
//参数:客户端套接字,文件名
int send_file_splice(int socket_fd, char *filename);

#endif


main_server.c

#include "../include/process_pool.h"
#include "../include/process_pool.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

//与主进程通信的管道,用来传递退出信号
int exitpipe[2];

//退出信号处理,通知主进程退出
void sigFunc(int sigNum)
{
    /* printf("%d is coming!\n", sigNum); */ 
    write(exitpipe[1], &sigNum, 4);
}

int main(int argc, char *argv[]) 
{
    //命令行参数:配置文件(ip地址,port端口号,子进程数量)
    ARGS_CHECK(argc, 2);

    //从配置文件中拿到ip,port,子进程数
    char ip[64] = {0};
    int port = 0;
    int childsNum = 0;
    FILE *fp = fopen(argv[1], "r");
    ERROR_CHECK(fp, NULL, "fopen");
    fscanf(fp, "%s%d%d", ip, &port, &childsNum);
    fclose(fp);

    //创建一个数组,存储子进程信息
    pProcInfo_t pChilds = (pProcInfo_t)calloc(childsNum, sizeof(ProcInfo_t));

    //创建进程池(参数:子进程数组,子进程数量)
    init_process_pool(pChilds, childsNum);

    //注册退出信号, SIGUSR1默认行为是终止进程
    pipe(exitpipe);
    signal(SIGUSR1, sigFunc);

    //建立一个tcp类型正在监听的套接字
    int sfd = tcp_init(ip, port);

    //处理来自客户端,进程池,退出信号的请求
    if (-1 != sfd) {
        interact_cli(sfd, pChilds, childsNum, exitpipe[0]);
    }

    //回收子进程数组
    free(pChilds);
    pChilds = NULL;
    return 0;
}


init_process_pool.c

#include "../include/process_pool.h"
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

//功能:创建进程池
//参数:子进程数组,子进程数量
int init_process_pool(pProcInfo_t pChilds, int childsNum)
{
    pid_t pid = 0;
    int fds[2];//存储socketpair创建的一对套接口

    //创建childsNum个子进程
    int i;
    for (i = 0; i < childsNum; ++i) {
        //通过socketpair创建一对本地的tcp类型的套接口,这对套接口是相连的,只能在本机使用
        //用于传递客户端套接字
        socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
        pid = fork();

        //启动子进程
        if (0 == pid) {
            close(fds[1]);	//关闭套接口的写端
            child_process(fds[0]);
            exit(0);
        }

        //主进程记录子进程的信息
        close(fds[0]);	//关闭套接口的读端
        pChilds[i]._pid = pid;
        pChilds[i]._flag = 0;
        pChilds[i]._pipefd = fds[1];
    }

    return 0;
}


child_process.c

#include "../include/process_pool.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>

int child_process(int pipefd)
{
    printf("create child_process , child_pid = %d\n", getpid());
    int cli_fd;//客户端套接字
    int exit_flag;//子进程退出标志位

    while (1) {
        //阻塞,等待主进程发送客户端套接字
        recvFd(pipefd, &cli_fd, &exit_flag);
        if (1 == exit_flag) {
            printf("child_process exit!\n");
            exit(10);//退出码范围 0-255
        }

        //开始干活,4中干活方式,后三种是零拷贝发送文件
        char filename[] = "file";
        /* send_file_truck(cli_fd, filename); */
        /* send_file_mmap(cli_fd, filename); */
        /* send_file_sendfile(cli_fd, filename); */
        send_file_splice(cli_fd, filename);
        

        //干完通知主进程
        write(pipefd, "a", 1);
    }

    return 0;
}


tcp_init.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <arpa/inet.h>

#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror("msg");  return -1;} }

//输入:服务器的ip地址,端口号
//输出:绑定了服务器ip和端口的,正在监听的套接字
int tcp_init(char *ip, int port)
{
    //生成一个tcp类型的套接字
    int sfd = socket(AF_INET, SOCK_STREAM, 0);
    ERROR_CHECK(sfd, -1, "ser_socket");

    //将端口号设置为可重用, 不用再等待重启时的TIME_WAIT时间
    int reuse = 1;
    setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

    //给套接字绑定服务端ip和port
    struct sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(struct sockaddr_in));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(ip);
    serverAddr.sin_port = htons(port);

    int ret = bind(sfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    ERROR_CHECK(ret, -1, "ser_bind");

    //将套接字设为监听模式,并指定最大监听数(全连接队列的大小)
    ret = listen(sfd, 10); 
    ERROR_CHECK(ret, -1, "ser_listen");

    printf("[ip:%s, port:%d] is listening...\n", ip, port);

    return sfd;
}


interact_cli.c

#include "../include/process_pool.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>

#include <sys/socket.h>
#include <sys/epoll.h>



//功能:服务器主进程处理来自客户端和进程池的请求,以及退出信号
//参数:服务器套接字,子进程数组,子进程数量, 退出管道读端
int interact_cli(int sfd, pProcInfo_t pChilds, int childsNum, int exitpipe)
{
    //接受所有客户端的连接,将客户端套接字转发给空闲子进程处理
    //将工作完的子进程状态设为空闲
    //收到退出信号,实现进程池的退出
    

    //使用epoll管理所有文件描述符
    int epfd = epoll_create(1);

    //定义读事件
    struct epoll_event event;
    memset(&event, 0, sizeof(event));
    event.events = EPOLLIN;

    //将sfd添加进epfd
    event.data.fd = sfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &event);

    //将子进程的管道fd,加入epfd
    int i;
    for (i = 0; i < childsNum; ++i) {
        event.data.fd = pChilds[i]._pipefd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, pChilds[i]._pipefd, &event);
    }

    //将接收退出信号的管道加入epfd
    event.data.fd = exitpipe;
    epoll_ctl(epfd, EPOLL_CTL_ADD, exitpipe, &event);

    char buf[128] = {0};//读写缓冲区
    int readyFdNum = 0;//就绪的文件描述符数量
    struct epoll_event evs[2]; //epoll_wait等待数组的大小
    int newfd = 0;//客户端的套接字

    //epoll等待就绪的文件描述符
    while (1) {
        readyFdNum = epoll_wait(epfd, evs, 2, -1);
        //ERROR_CHECK(readyFdNum, -1, "epoll_wait");
        //这里不能检查epoll_wait的返回值                                
        //epoll_wait等待时可能会收到终止信号,这将导致调用被中断

        for (i = 0; i < readyFdNum; ++i) {

            //服务端套接字就绪,有新的客户端申请连接,将其发送给空闲子进程
            if (evs[i].data.fd == sfd) {
                //newfd指向最后一个客户端套接字
                //每次accept都会更新newfd
                newfd = accept(sfd, NULL, NULL);

                //将newfd交给空闲子进程
                int j;
                for (j = 0; j < childsNum; ++j) {
                    if (0 == pChilds[j]._flag) {
                        sendFd(pChilds[j]._pipefd, newfd, 0);
                        pChilds[j]._flag = 1;   //将子进程状态设为忙碌
                        printf("the child_pid %d is working...\n", pChilds[j]._pid);
                        break;
                    }
                }

                //任务已传给空闲子进程,关掉客户端套接字
                //主进程只管调度任务,不管具体实现
                close(newfd);
            }

            //收到退出信号
            else if (evs[i].data.fd == exitpipe) {
                int j;
                //通知所有子进程退出,将退出标志位设为1
                for (j = 0; j < childsNum; ++j) {
                    sendFd(pChilds[j]._pipefd, 0, 1);
                }

                //回收所有子进程资源
                for (j = 0; j < childsNum; ++j) {
                    wait(NULL);
                }

                //服务器退出
                printf("Server exit!\n");
                exit(0);
            }

            //子进程套接口就绪,将就绪的子进程状态设为空闲
            else  {
		        int j;
                for (j = 0; j < childsNum; ++j) {
                    if (evs[i].data.fd == pChilds[j]._pipefd) {
                        read(pChilds[j]._pipefd, buf, sizeof(buf) - 1);//读取子进程套接口
                        pChilds[j]._flag = 0;
                        printf("the child_pid %d finished work!\n", pChilds[j]._pid);
                    }
                }
            }
        }
    }
    return 0;
}


transfer_fd.c

比1.0版本,多了个退出标志位参数,告诉子进程是否退出程序

#include "../include/process_pool.h"
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
/* #include <sys/uio.h>    //writev & readv */


//功能:将客户端套接字发送给子进程
//参数:子进程套接口,客户端套接字,退出标志位
int sendFd(int pipefd, int cli_fd, int exit_flag)
{
    //使用sendmsg接口发送fd
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    
    //设置iovec结构体数组,不想传数据就随意写一个
    struct iovec iov;
    memset(&iov, 0, sizeof(iov));

    iov.iov_base = &exit_flag; //推出标志位放在这
    iov.iov_len = sizeof(int);

    msg.msg_iov = &iov;//iovec结构体数组指针
    msg.msg_iovlen = 1;//iovec结构体数组大小

    //设置cmsghdr结构体,最后一个成员就是要传输的fd
    struct cmsghdr *cmsg = (struct cmsghdr*)calloc(1, sizeof(struct cmsghdr));
    //计算cmsg结构体的长度, 使用CMSG_LEN()宏,其中已经有cmsg前三个成员的大小,只需传入最后一个成员大小即可(客户端套接字)
    int len = CMSG_LEN(sizeof(cli_fd));
    cmsg->cmsg_len = len;
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    *(int*)CMSG_DATA(cmsg) = cli_fd;

    msg.msg_control = cmsg;//cmsghdr结构体指针
    msg.msg_controllen = len;//cmsghdr结构体长度

    //将fd写入套接口
    int ret = sendmsg(pipefd, &msg, 0);
    ERROR_CHECK(ret, -1, "sendmsg");
    return 0;
}

//功能:从主进程接收客户端套接字
//参数:子进程套接口,客户端套接字地址,推出标志位地址
int recvFd(int pipefd, int *cli_fd, int *exit_flag)
{
    //使用recvmsg接口接收fd
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
        
    //设置iovec结构体数组,不想传数据就随意写一个
    struct iovec iov;
    memset(&iov, 0, sizeof(iov));

    iov.iov_base = exit_flag;
    iov.iov_len = sizeof(int);

    msg.msg_iov = &iov;//iovec结构体数组指针
    msg.msg_iovlen = 1;//iovec结构体数组大小

    //设置cmsghdr结构体,最后一个成员就是要接收的fd
    struct cmsghdr *cmsg = (struct cmsghdr*)calloc(1, sizeof(struct cmsghdr));
    //计算cmsg结构体的长度, 使用CMSG_LEN()宏,其中已经有cmsg前三个成员的大小,只需传入最后一个成员大小即可(客户端套接字)
    int len = CMSG_LEN(sizeof(cli_fd));
    cmsg->cmsg_len = len;
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;

    msg.msg_control = cmsg;//cmsghdr结构体指针
    msg.msg_controllen = len;//cmsghdr结构体长度

    //从套接口中接收fd
    recvmsg(pipefd, &msg, 0);
    *cli_fd = *(int*)CMSG_DATA(cmsg);
    return 0;
}


send_file_truck.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>

//open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//send
#include <sys/socket.h>


#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg); return -1;} }


//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

//使用私有协议传输数据,给另一个进程传输文件
int send_file_truck(int socket_fd, char *filename)
{
    int ret = -1;
    //定义一个小货车,用来传输文件
    Truck_t truck;
    memset(&truck, 0, sizeof(Truck_t));

    //将文件名扩展为文件路径
    char filepath[128] = {0};
    sprintf(filepath, "../resource/%s", filename);

    //根据文件路径打开传输文件
    int file_fd = open(filepath, O_RDONLY);
    ERROR_CHECK(file_fd, -1, "open");

    //先发文件名
    truck._data_len = strlen(filename);
    strcpy(truck._data, filename);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_title");

    //再发文件大小
    struct stat file_info;
    memset(&file_info, 0, sizeof(file_info));
    fstat(file_fd, &file_info);

    truck._data_len = sizeof(file_info.st_size);
    memcpy(truck._data, &file_info.st_size, truck._data_len);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_filesize");


    //再发文件内容
    while (1) {
        memset(truck._data, 0, sizeof(truck._data));
        truck._data_len = read(file_fd, truck._data, sizeof(truck._data));
        ERROR_CHECK(truck._data_len, -1, "read");
        if (0 == truck._data_len) {
            //传输完成,通知客户端,然后退出循环
            ret = send(socket_fd, &truck._data_len, 4, 0);
            ERROR_CHECK(ret, -1, "send");
            break;
        }

        ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
        if (-1 == ret) {
            //客户端异常断开,退出循环
            printf("client already break!\n");
            break;
        }
    }
    

    //关闭传输文件
    close(file_fd);
    return 0;
}


send_file_mmap.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>

//open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//send
#include <sys/socket.h>
//mmap
#include <sys/mman.h>


#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg); return -1;} }


//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

//使用私有协议传输数据,给另一个进程传输文件
int send_file_mmap(int socket_fd, char *filename)
{
    int ret = -1;
    //定义一个小货车,用来传输数据
    Truck_t truck;
    memset(&truck, 0, sizeof(Truck_t));

    //将文件名扩展为文件路径
    char filepath[128] = {0};
    sprintf(filepath, "../resource/%s", filename);

    //根据文件路径打开传输文件
    int file_fd = open(filepath, O_RDONLY);
    ERROR_CHECK(file_fd, -1, "open");

    //先发文件名
    truck._data_len = strlen(filename);
    strcpy(truck._data, filename);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_title");

    //再发文件大小
    struct stat file_info;
    memset(&file_info, 0, sizeof(file_info));
    fstat(file_fd, &file_info);

    truck._data_len = sizeof(file_info.st_size);
    memcpy(truck._data, &file_info.st_size, truck._data_len);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_filesize");
    printf("filesize = %ld\n", file_info.st_size);


    //再发文件内容
    //用mmap建立文件映射,将文件直接映射到内核发送缓冲区
    char *pMap = (char*)mmap(NULL, file_info.st_size, PROT_READ, MAP_SHARED, file_fd, 0);
    ERROR_CHECK(pMap, (char*)-1, "mmap");

    send(socket_fd, pMap, file_info.st_size, 0);
    munmap(pMap, file_info.st_size);

    //关闭传输文件
    close(file_fd);
    return 0;
}

send_file_sendfile.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>

//open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//send
#include <sys/socket.h>
//sendfile
#include <sys/sendfile.h>


#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg); return -1;} }


//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

//使用私有协议传输数据,给另一个进程传输文件
int send_file_sendfile(int socket_fd, char *filename)
{
    int ret = -1;
    //定义一个小货车
    Truck_t truck;
    memset(&truck, 0, sizeof(Truck_t));

    //将文件名扩展为文件路径
    char filepath[128] = {0};
    sprintf(filepath, "../resource/%s", filename);

    //根据文件路径打开传输文件
    int file_fd = open(filepath, O_RDONLY);
    ERROR_CHECK(file_fd, -1, "open");

    //先发文件名
    truck._data_len = strlen(filename);
    strcpy(truck._data, filename);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_title");

    //再发文件大小
    struct stat file_info;
    memset(&file_info, 0, sizeof(file_info));
    fstat(file_fd, &file_info);

    truck._data_len = sizeof(file_info.st_size);
    memcpy(truck._data, &file_info.st_size, truck._data_len);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_filesize");
    printf("filesize = %ld\n", file_info.st_size);


    //再发文件内容
    //用sendfile接口,整体传输
    sendfile(socket_fd, file_fd, 0, file_info.st_size);

    //关闭传输文件
    close(file_fd);
    return 0;
}

send_file_splice.c

#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>

//open
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//send
#include <sys/socket.h>


#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg); return -1;} }


//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

//使用私有协议传输数据,给另一个进程传输文件
int send_file_splice(int socket_fd, char *filename)
{
    int ret = -1;
    //定义一个小货车
    Truck_t truck;
    memset(&truck, 0, sizeof(Truck_t));

    //将文件名扩展为文件路径
    char filepath[128] = {0};
    sprintf(filepath, "../resource/%s", filename);

    //根据文件路径打开传输文件
    int file_fd = open(filepath, O_RDONLY);
    ERROR_CHECK(file_fd, -1, "open");

    //先发文件名
    truck._data_len = strlen(filename);
    strcpy(truck._data, filename);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_title");

    //再发文件大小
    struct stat file_info;
    memset(&file_info, 0, sizeof(file_info));
    fstat(file_fd, &file_info);

    truck._data_len = sizeof(file_info.st_size);
    memcpy(truck._data, &file_info.st_size, truck._data_len);
    ret = send(socket_fd, &truck, sizeof(int) + truck._data_len, 0);
    ERROR_CHECK(ret, -1, "send_filesize");
    printf("filesize = %ld\n", file_info.st_size);


    //再发文件内容
    //用splice接口,需要借助管道
    int fds[2];
    pipe(fds);
    int cur_len = 0; //已经发送的字节数
    int maxsize = 4096; //每次最多发送的字节数,修改这个参数来控制传输的速度

    //先读文件到管道,再从管道放入发送缓冲区
    while (cur_len < file_info.st_size) {
        ret = splice(file_fd, 0, fds[1], 0, maxsize, 0);
        ret = splice(fds[0], 0, socket_fd, 0, ret, 0);
        cur_len += ret;
    }


    //关闭传输文件
    close(file_fd);
    return 0;
}


客户端代码

main_client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/mman.h>


//检查命令行参数个数
#define ARGS_CHECK(argc, num) { if (argc != num) {\
    fprintf(stderr, "Argc error!\n");\
    return -1;}}

//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg);\
    return -1;}}

//从服务器下载文件,用while+recv+小货车的方式
int recv_file_truck(int sfd);

//从服务器下载文件,用mmap的方式
int recv_file_mmap(int sfd);

//从服务器下载文件,用splice的方式
int recv_file_splice(int sfd);


int main(int argc, char *argv[])
{
    ARGS_CHECK(argc, 2);

    //从配置文件中拿到服务器的ip和port
    FILE *fp = fopen(argv[1], "r");
    char ip[128] = {0};
    int port = 0;
    fscanf(fp, "%s%d", ip, &port);
    fclose(fp);

    //生成一个tcp类型的套接字
    int sfd = 0;
    sfd = socket(AF_INET, SOCK_STREAM, 0);

    //连接服务器
    struct sockaddr_in serAddr;
    memset(&serAddr, 0, sizeof(serAddr));
    serAddr.sin_family = AF_INET;
    serAddr.sin_addr.s_addr = inet_addr(ip);
    serAddr.sin_port = htons(port);
    int ret = -1;

    ret = connect(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
    ERROR_CHECK(ret, -1, "connect");
    printf("connect server!\n");

    //下载文件
    /* recv_file_truck(sfd); */
    /* recv_file_mmap(sfd); */
    recv_file_splice(sfd);

    //关闭服务器套接字
    close(sfd);
    return 0;
}

recv_file_truck.c

#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>

//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg);\
    return -1;}}

//接收协议:小货车
typedef struct {
    int _data_len;//车头,先接数据长度
    char _data[1000];//车厢,再接数据内容
}Truck_t;

//先接数据长度,再根据数据长度接收数据内容
int recv_file_truck(int sfd)
{
    int ret = -1;

    //接收文件
    Truck_t truck;
    memset(&truck, 0, sizeof(truck));
    //先接收文件名,打开一个新文件
    recv(sfd, &truck._data_len, sizeof(int), 0);
    recv(sfd, truck._data, truck._data_len, 0);

    int file_fd = open(truck._data, O_WRONLY|O_CREAT, 0666);
    ERROR_CHECK(file_fd, -1, "open");
    printf("filename: %s\n", truck._data);

    //再接收文件大小,用来打印进度条
    int total_size = 0;//文件总大小
    recv(sfd, &truck._data_len, sizeof(int), 0);
    recv(sfd, &total_size, truck._data_len, 0);
    printf("filesize: %d\n", total_size);

    float rate = 0;//当前接收百分比
    int cur_size = 0;//文件已接收大小

    //循环接收文件内容
    while (cur_size < total_size) {
        //重置小货车
        memset(&truck, 0, sizeof(truck));
        //先接数据长度
        recv(sfd, &truck._data_len, sizeof(int), 0);
        if (0 == truck._data_len) {
            //传输完毕
            printf("Transfer Finish!\n");
            break;
        }

        //防止发送方发的慢,导致接收缓冲区将车厢当成车头,设置recv参数为MSG_WAITALL
        ret = recv(sfd, truck._data, 1000, MSG_WAITALL);
        if (0 == ret) {
            printf("Download finish!\n");
            break;
        }

        //将接收数据写入文件
        write(file_fd, truck._data, ret);
        cur_size += ret;

        //打印进度条
        rate = (float)cur_size / total_size;
        printf("--------------------------%5.2f%%\r", rate * 100);
        fflush(stdout);//防止光标抖动
    }

    //关闭文件
    close(file_fd);

    return 0;
}


recv_file_mmap.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg);\
    return -1;}}

//从服务器下载文件,用mmap的方式
int recv_file_mmap(int sfd)
{
    //先接收文件名字
    int data_len = 0;
    char filename[1000] = {0};
    recv(sfd, &data_len, sizeof(int), 0);
    recv(sfd, filename, data_len, 0);

    //创建一个新文件
    int file_fd = open(filename, O_RDWR | O_CREAT, 0600);
    ERROR_CHECK(file_fd, -1, "open");


    //再接收文件大小
    off_t filesize = 0;//此处类型必须为off_t,否则ftrucate会失败
    recv(sfd, &data_len, sizeof(int), 0);
    recv(sfd, &filesize, data_len, 0);
    printf("filesize: %ld\n", filesize);

    //用mmap接收文件
    int ret = ftruncate(file_fd, filesize);
    ERROR_CHECK(ret, -1, "ftruncate");
    
    printf("recv start...\n");
    char *pMap = (char*)mmap(NULL, filesize, PROT_READ|PROT_WRITE, MAP_SHARED, file_fd, 0);
    ERROR_CHECK(pMap, (char*)-1, "mmap");
    recv(sfd, pMap, filesize, MSG_WAITALL);
    munmap(pMap, filesize);
    printf("recv finish!\n");

    //关闭文件
    close(file_fd);
    return 0;
}

recv_file_splice.c

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>

//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\
    perror(msg);\
    return -1;}}

//从服务器下载文件,用splice的方式
int recv_file_splice(int sfd)
{
    int ret = -1;
    //先接收文件名字
    int data_len = 0;
    char filename[1000] = {0};
    recv(sfd, &data_len, sizeof(int), 0);
    recv(sfd, filename, data_len, 0);

    //创建一个新文件
    int file_fd = open(filename, O_RDWR | O_CREAT, 0600);
    ERROR_CHECK(file_fd, -1, "open");


    //再接收文件大小
    off_t filesize = 0;//此处类型必须为off_t,否则ftrucate会失败
    recv(sfd, &data_len, sizeof(int), 0);
    recv(sfd, &filesize, data_len, 0);
    printf("filesize: %ld\n", filesize);

    //用splice接收文件
    int fds[2];
    pipe(fds);
    int cur_len = 0; //当前接收长度
    int maxsize = 4096;//每次传输的最大字节数
    
    printf("recv start...\n");
    while (cur_len < filesize) {
        ret = splice(sfd, 0, fds[1], 0, maxsize, 0);
        ret = splice(fds[0], 0, file_fd, 0, ret, 0);
        cur_len += ret;
    }
    printf("recv finish!\n");

    //关闭文件
    close(file_fd);
    return 0;
}
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值