概述
使用了 tcp
+ epoll
+ 进程池
,实现文件下载服务器
功能
主要功能:客户端连接服务器,然后自动下载文件
次要功能:客户端接收时显示进度条
启动
启动服务器
1、在bin目录下生成可执行文件
bash
w@Ubuntu20:bin $ gcc ../src/*.c -o server
2、启动服务器
bash
w@Ubuntu20:bin $ ./server ../conf/server.conf
启动客户端
1、在客户端的目录下生成可执行文件
bash
w@Ubuntu20:client $ gcc main_client.c -o client
2、启动客户端
bash
w@Ubuntu20:client $ ./client client.conf
目录设计
服务器
- bin:存放二进制文件
- conf:存放配置文件
- include:存放头文件
- resource:存放资源文件
- src:存放源文件
bash
w@Ubuntu20:bin $ 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
├── tcp_init.c
├── transfer_fd.c
└── transfer_file.c
客户端
bash
w@Ubuntu20:client $ tree
.
├── client
├── client.conf
└── main_client.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
服务器搭建
1 创建进程池
根据子进程的数量,创建存储子进程信息的数组
根据子进程的数量,循环创建子进程,并初始化子进程的信息(子进程id、是否空闲,通讯管道)
2 主进程分配任务给子进程
建立一个tcp类型的正在监听的套接字
使用epoll管理所有套接字
-
有新的客户端连接,得到一个客户端套接字,交给一个空闲的子进程处理
-
等待子进程工作完毕,将其状态设为空闲
-
等待退出信号,收到后回收进程池资源,退出程序
3 资源进程(子进程)处理具体业务
等待任务(主进程发送过来的客户端套接字)
设置本进程为忙碌状态
工作(发送文件给客户端)
通知主进程任务完成
进程池退出方式
方式一:给主进程发送退出信号,主进程收到信号后,kill
所有子进程,然后回收所有子进程的资源,再退出主进程 (本文采用)
方式二:给主进程发送退出信号,主进程收到信号后,通知所有子进程退出
- 如果是非忙碌的子进程,直接退出
- 如果是忙碌的子进程,就忙完了再退出
传输文件方式
方式一:使用自定义协议传输:先发送本次数据长度,再发送数据内容 (本文使用)
方式二:使用零拷贝的方式传输,比如mmap或者splice
代码实现逻辑
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
接口
c
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
接口
c
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 服务器发送文件
输入:客户端套接字,待传输文件名
输出:使用私有协议将文件传输给客户端
自定义传输文件协议:小货车
c
//传输文件协议:小货车
typedef struct {
int _data_len;//货车头,表示数据长度
char _data[1000];//火车车厢,表示数据
}Truck_t;
步骤:
- 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
- 将文件名添加上资源目录的路径,再open打开待传文件
- 传输中
- 先发文件名
- 再发文件大小
- 循环发送文件内容(小货车每次最多发1000个字节)
- 给小货车装车,发货
- 如果全部传输完毕之后,通知客户端,并退出循环
- 如果客户端异常断开,则退出循环(此时会收到SIGPIPE信号)
- 传输结束,关闭待传文件
main_client.c 客户端主流程
命令行参数:配置文件(服务器ip地址,服务器端口号)
步骤:
- 读取配置文件,拿到服务器的ip和port
- 生成一个tcp类型的套接字,并绑定服务器的ip和端口
- 申请连接服务器
- 接收文件(根据自定义传输协议小货车接收:先接受数据长度,再根据长度接收数据)
- 先接受文件名,根据文件名
open
一个新文件 - 再接收文件大小(为了打印接收进度条)
- 循环接收文件内容(根据协议,每次最多接收1000个字节)
- 先接收数据长度(如果为空则表示接收完毕,退出循环)
- 根据数据长度,接收数据内容
- 根据当前进度和总大小打印进度条(用fflush刷新标准输出,避免光标跳动)
- 将数据写入文件
- 关闭文件
- 先接受文件名,根据文件名
- 关闭服务器套接字
具体代码
服务器代码
prcess_pool.h
c
#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 recvFd(int pipefd, int *cli_fd);
//功能:资源进程的配置
//参数:套接口
int child_process(int pipefd);
//功能:给客户端套接字发送文件
//参数:客户端套接字,文件名
int send_file(int socket_fd, char *filename);
#endif
main_server.c
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
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
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;//客户端套接字
while (1) {
//阻塞,等待主进程发送客户端套接字
recvFd(pipefd, &cli_fd);
//开始干活
char filename[] = "file";
send_file(cli_fd, filename);
//干完通知主进程
write(pipefd, "a", 1);
}
return 0;
}
tcp_init.c
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
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);
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;
//杀掉所有子进程
for (j = 0; j < childsNum; ++j) {
kill(pChilds[j]._pid, SIGUSR1);
}
//回收所有子进程资源
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
c
#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)
{
//使用sendmsg接口发送fd
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
//设置iovec结构体数组,不想传数据就随意写一个
struct iovec iov;
memset(&iov, 0, sizeof(iov));
char buf[6] = "hi"; //要传输的数据,不想传就随意写
iov.iov_base = buf;
iov.iov_len = strlen(buf);
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)
{
//使用recvmsg接口接收fd
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
//设置iovec结构体数组,不想传数据就随意写一个
struct iovec iov;
memset(&iov, 0, sizeof(iov));
char buf[6] = "hi"; //要传输的数据,不想传就随意写
iov.iov_base = buf;
iov.iov_len = strlen(buf);
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;
}
transfer_file.c
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(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));
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;
}
客户端代码
main_client.c
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>
//检查命令行参数个数
#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;}}
//接收协议
typedef struct {
int _data_len;//先接数据长度
char _data[1000];//再接数据内容
}Truck_t;
int main(int argc, char *argv[])
{
//从配置文件中拿到服务器的ip和port
ARGS_CHECK(argc, 2);
FILE *fp = fopen(argv[1], "r");
char ip[128] = {0};
int port = 0;
fscanf(fp, "%s%d", ip, &port);
fclose(fp);
//生成一个tcp类型的套接字,用于连接服务器
int 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");
//接收文件
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_RDWR|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 (1) {
//重置小货车
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, truck._data_len, MSG_WAITALL);
//打印进度条
cur_size += ret;
rate = (float)cur_size / total_size;
printf("--------------------------%5.2f%%\r", rate * 100);
fflush(stdout);//防止光标抖动
//将接收数据写入文件
write(file_fd, truck._data, truck._data_len);
}
//关闭文件
close(file_fd);
//关闭服务器套接字
close(sfd);
return 0;
}