概述
使用了 tcp
+ epoll
+ 进程池
,实现文件下载服务器
与1.0相比,进程池的退出方式不同,服务器的传输文件方式也不同
退出方式:采用温和手段,通知子进程让其自己退出
(在1.0的基础上,修改了 sendFd
和recvFd
函数,多加了一个退出标志位参数,用来指定子进程是否退出)
传输方式:采用零拷贝的方式完成(文件内容不再经过用户态缓冲区,直接从内核缓冲区发送出去)
(在1.0的基础上,增加了send_file_mmap
、send_file_sendfile
、send_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
检查传输文件是否正确
- 查看文件大小
$ du -h file
- 查看文件唯一哈希值
$ md5sum file
服务器搭建
-
创建进程池
- 根据子进程的数量,创建存储子进程信息的数组
- 根据子进程的数量,循环创建子进程,并初始化子进程的信息(子进程id、是否空闲,通讯管道)
-
主进程分配任务给子进程
-
建立一个tcp类型的正在监听的套接字
-
使用epoll管理所有套接字
- 有新的客户端连接,得到一个客户端套接字,交给一个空闲的子进程处理
- 等待子进程工作完毕,将其状态设为空闲
- 等待退出信号,收到后回收进程池资源,退出程序
-
-
资源进程(子进程)处理具体业务
- 等待任务(主进程发送过来的客户端套接字)
- 设置本进程为忙碌状态
- 工作(发送文件给客户端)
- 通知主进程任务完成
进程池退出方式
方式一:给主进程发送退出信号,主进程收到信号后,kill
所有子进程,然后回收所有子进程的资源,再退出主进程
方式二:给主进程发送退出信号,主进程收到信号后,通知所有子进程退出 (本文采用)
- 如果是非忙碌的子进程,直接退出
- 如果是忙碌的子进程,就忙完了再退出
在1.0版本的基础上,修改sendFd.c
和recvFd.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 服务器主流程
步骤:
- 从配置文件中拿到,本服务器
ip
地址、port
端口号、进程数量 - 创建一个子进程数组,用来存储所有子进程的信息
- 创建进程池,并用子进程数组记录子进程的信息(根据子进程的数组和子进程的数量)
- 建立退出管道,并注册
SIGUSR1
信号(用于主进程的异步退出) - 创建一个tcp类型的服务器套接字用于监听客户端的连接
- 处理来自客户端和进程池的请求,以及退出信号
- 将每一个客户端的连接交给空闲子进程,
- 将请求的忙碌子进程设为空闲状态
- 收到退出信号,依次终止子进程,回收子进程资源,退出主进程
- 最后释放子进程数组的空间
init_process_pool.c 创建进程池
输入:子进程数组pChilds,子进程的数量childsNum
输出:一个有childsNum个子进程信息的数组
为什么用socketpair
生成一对套接口,而不是用管道等方式在进程间传递套接字(文件描述符)?
每一个进程都会维护一个数字与文件描述符对应的表
每个文件描述符都会在内核中维护一个文件对象数据结构的,不仅仅是一个数字
而用管道传输文件描述符时,只会传送数字,而不会传送文件对象
因此需要特殊的接口,在进程之间,传递文件描述符的数据结构
步骤:
-
循环childsNum次,创建子进程
-
使用
socketpair
创建一对用于本地通信的tcp类型的套接口fds[2]
(全双工管道,用于传递客户端套接字) -
fork
出一个子进程 -
子进程设置
- 关闭套接口的写端
fds[1]
(子进程只需要从套接口中读取客户端套接字) - 子进程业务逻辑
- 子进程退出
- 关闭套接口的写端
-
主进程设置,将新建的子进程信息放入子进程数组内
- 关闭套接口的读端
fds[0]
(主进程只需要向套接口中写入客户端套接字) - 记录第i个子进程的进程id
- 设置第i个子进程的状态为空闲
- 设置第i个子进程的通信管道为
fds[0]
- 关闭套接口的读端
-
child_process.c 子进程业务逻辑
输入:子进程套接口
输出:将目标文件发送给客户端
步骤:
- 死循环,处理业务
- 等待任务:阻塞在套接口,等待主进程发来的客户端套接字
- 干活:将目标文件发送给客户端
- 任务结束:通知主进程任务完成
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成员,就是用来传输客户端套接字文件对象的
步骤:
- 初始化一个
struct msghdr
结构体msg
,用来传递客户端套接字 - 创建一个
struct iovec
结构体,初始化&赋值,然后设为msg
的参数 - 创建一个
struct cmsghdr
结构体cmsg
,初始化,然后设为msg
的参数- 用
CMSG_LEN
宏得到cmsg
的大小 cmsg->cmsg_level = SOL_SOCKET;
原始协议cmsg->cmsg_type = SCM_RIGHTS;
特定协议类型- 传输的客户端套接字
*(int*)CMSG_DATA(cmsg) = cli_fd;
- 用
- 将
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成员,就是用来传输客户端套接字文件对象的
步骤:
- 初始化一个
struct msghdr
结构体msg
,用来接收客户端套接字 - 创建一个
struct iovec
结构体,初始化,然后设为msg
的参数 - 创建一个
struct cmsghdr
结构体cmsg
,初始化,然后设为msg
的参数- 用
CMSG_LEN
宏得到cmsg
的大小 cmsg->cmsg_level = SOL_SOCKET;
原始协议cmsg->cmsg_type = SCM_RIGHTS;
特定协议类型- 传输的客户端套接字
*(int*)CMSG_DATA(cmsg) = cli_fd;
- 用
- 从套接口中用
recvmsg
接收msg
- 从msg中提取客户端套接字
tcp_init.c 生成一个服务器正在监听的tcp套接字
输入:服务器的ip地址,服务器的port端口号
输出:绑定了服务器ip和port,正在监听的tcp类型的套接字
步骤:
- 使用socket生成一个tcp类型的套接字
- 给套接字绑定服务器的ip地址和port端口号
- 开始监听
interact_cli.c 主进程处理客户端和进程池请求,以及退出信号
输入:服务器套接字,子进程数组,子进程数量,退出管道读端
输出:将客户端的请求转发给空闲子进程,将完成任务的子进程设为空闲状态,如果收到退出信号则回收所有子进程资源并退出
步骤:
- 创建epoll管理所有请求
- 将服务器套接字,加入epoll,用于接收客户端请求
- 将子进程数组内的所有通信管道,加入epoll,用于处理子进程的请求
- 将退出管道读端,加入epoll,用于接收退出信号
- epoll循环等待就绪的文件描述符
- 如果服务器套接字就绪,接收客户端套接字并其交给一个空闲子进程处理,然后关闭客户端套接字
- 如果子进程的管道就绪(表示子进程已处理完一个任务),读取管道,然后将该子进程的状态设为空闲
- 如果收到退出信号,依次关闭子进程,回收所有子进程的资源,然后退出主进程
send_file_truck 私有协议发送文件
输入:客户端套接字,待传输文件名
输出:使用私有协议将文件传输给客户端
自定义传输文件协议:小货车
//传输文件协议:小货车
typedef struct {
int _data_len;//货车头,表示数据长度
char _data[1000];//火车车厢,表示数据
}Truck_t;
步骤:
- 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
- 将文件名添加上资源目录的路径,再
open
打开待传文件 - 传输中
- 先发文件名
- 再发文件大小
- 循环发送文件内容(小货车每次最多发1000个字节)
- 给小货车装车,发货
- 如果全部传输完毕之后,通知客户端,并退出循环
- 如果客户端异常断开,则退出循环(此时会收到SIGPIPE信号)
- 传输结束,关闭待传文件
send_file_mmap 零拷贝发送文件
输入:客户端套接字,待传输文件名
输出:用mmap
零拷贝发送文件
自定义传输文件协议:小货车
//传输文件协议:小货车
typedef struct {
int _data_len;//货车头,表示数据长度
char _data[1000];//火车车厢,表示数据
}Truck_t;
步骤:
- 初始化一个小货车(使用自定义协议传输文件,防止
tcp
粘包问题) - 将文件名添加上资源目录的路径,再
open
打开待传文件 - 传输中
- 先发文件名(用小货车发)
- 再发文件大小(用小货车发)
- 对文件建立
mmap
映射,整体发送,然后解除映射
- 传输结束,关闭待传文件
send_file_sendfile 零拷贝发送文件
输入:客户端套接字,待传输文件名
输出:用sendfile零拷贝发送文件
自定义传输文件协议:小货车
//传输文件协议:小货车
typedef struct {
int _data_len;//货车头,表示数据长度
char _data[1000];//火车车厢,表示数据
}Truck_t;
步骤:
- 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
- 将文件名添加上资源目录的路径,再
open
打开待传文件 - 传输中
- 先发文件名(用小货车发)
- 再发文件大小(用小货车发)
- 用
sendfile
接口直接发送文件
- 传输结束,关闭待传文件
send_file_splice 零拷贝发送文件
输入:客户端套接字,待传输文件名
输出:用splice
零拷贝发送文件
自定义传输文件协议:小货车
//传输文件协议:小货车
typedef struct {
int _data_len;//货车头,表示数据长度
char _data[1000];//火车车厢,表示数据
}Truck_t;
步骤:
- 初始化一个小货车
- 将文件名添加上资源目录的路径,再open打开待传文件
- 传输中
- 先发文件名(用小货车发)
- 再发文件大小(用小货车发)
- 创建一对管道
- 循环发送文件内容
- 先将文件发送给管道
- 再从管道放入发送缓冲区
- 传输结束,关闭待传文件
main_client.c 客户端主流程
命令行参数:配置文件(服务器ip地址,服务器端口号)
方式一:私有协议循环下载
步骤:
- 读取配置文件,拿到服务器的
ip
和port
- 生成一个
tcp
类型的套接字,并绑定服务器的ip
和端口 - 申请连接服务器
- 下载文件(根据自定义传输协议小货车接收:先接受数据长度,再根据长度接收数据)
- 先接受文件名,根据文件名
open
一个新文件 - 再接收文件大小(为了打印接收进度条)
- 循环接收文件内容(根据协议,每次最多接收1000个字节)
- 先接收数据长度(如果为空则表示接收完毕,退出循环)
- 根据数据长度,接收数据内容
- 根据当前进度和总大小打印进度条(用
fflush
刷新标准输出,避免光标跳动) - 将数据写入文件
- 关闭文件
- 先接受文件名,根据文件名
- 关闭服务器套接字
方式二:mmap零拷贝下载
步骤:
- 读取配置文件,拿到服务器的
ip
和port
- 生成一个
tcp
类型的套接字,并绑定服务器的ip
和端口 - 申请连接服务器
- 下载文件
- 先接受文件名,根据文件名
open
一个新文件 - 再接收文件大小
- 通过文件映射接收数据
- 根据文件大小,固定文件的空间
- 对新文件建立映射,整体接收,解除映射
- 关闭文件
- 先接受文件名,根据文件名
- 关闭服务器套接字
方式三:用splice零拷贝下载
步骤:
- 读取配置文件,拿到服务器的
ip
和port
- 生成一个
tcp
类型的套接字,并绑定服务器的ip
和端口 - 申请连接服务器
- 下载文件
- 先接受文件名,根据文件名
open
一个新文件 - 再接收文件大小
- 创建一对管道,用于splice接口
- 循环接收文件内容
- 从服务器下载到管道
- 从管道写入文件
- 关闭文件
- 先接受文件名,根据文件名
- 关闭服务器套接字
具体代码
服务器代码
prcess_pool.h 服务器头文件
比1.0多个三个个接口 send_file_mmap
、 send_file_sendfile
、 send_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;
}