目录
- 设计目标
- 设计思路
- 服务端请求响应
- 服务端进程退出
- 文件传输
- 客户端信息显示
- 代码实现
- 服务端父进程
- 子进程创建和工作
- 文件传输函数
- 子进程退出的信号处理函数
- 传递文件描述符
- 演示效果
设计目标
- 采用C/S模式,服务端发送文件,多个客户端可以同时下载文件
- 服务端的进程不会因为客户端的操作(意外退出)崩溃
- 关闭服务端时各个子进程可以有序退出
- 需要设计协议保证文件传输不出差错
- 客户端可以动态查看文件下载进度
设计思路
服务端请求响应
- 服务端采用进程池模式,父进程监听服务端口,创建多个子进程负责传输文件,有客户端请求时,建立连接,然后唤醒一个阻塞的子进程处理。
- 父进程通过维护一个子进程状态队列来实时更新各个子进程的状态。
- 采用传递socket描述符的方式使子进程可以直接与客户端通信。
- 父子进程之间通过一对匿名套接字通信,有新请求时父进程写套接字,通知子进程开始工作,子进程传输完毕后,写套接字通知父进程已经工作完毕进入阻塞状态
- 父进程采用epoll模型监控各个子进程的通信套接字和监听端口的套接字:
- 监听端口套接字可读,代表有新的下载请求,查找空闲子进程交付任务,更新子进程状态队列
- 子进程套接字可读,代表有子进程完成工作,更新子进程状态队列
服务端进程退出
- 注册信号处理函数实现有序退出,收到信号后查找子进程状态队列,杀死第一个空闲子进程,标记该进程已注销,将其监控事件从epoll监控实例中删除,关闭套接字,打印提示。同时处理epoll_wait函数由于监控对象变化而返回的
errno == 4
错误,直接忽略即可。
文件传输
- 通过tcp连接进行文件传输,每次传输数据之前应先发送本次传输的字节数,使得客户端不至于多读或少读数据。
- 正式传输之前需要传输文件名和文件大小,方便客户端创建文件和计算下载进度。
- 服务端子进程每次
send
后需要检查返回值来确定客户端的状态,如果返回-1则打印提示客户端断开连接,重新阻塞。
- 文件传输结束后发送一个0代表传输结束
- 客户端每次先读一个int型的控制数据代表接下来要读的字节数,然后再读数据。如果控制数据为0代表传输结束。
- 需要注意的是,recv的第四个参数要设置为阻塞,MSG_WAITALL,即不满足所读长度时就一直阻塞;
- 客户端每次读完数据都要和本次数据的控制数据对比,查看长度是否一致,不一致证明传输出差错,打印提示信息,退出客户端,重新进行下载。
客户端信息显示
- 客户端每成功接收一组数据,显示当前的下载进度百分比,通过\r回到行首来覆盖重写。
- 客户端根据下载进度打印若个标志字符来显示动态进度条。
- 可以 通过获取当前窗口的winsize结构体来确定需要打印的进度条长度,从而适应不同大小的窗口。
- 参考链接:linux获取终端窗口的大小方法
代码实现
头文件
#pragma once
#include //包含各种需要用到的标准库和Linux库和一个检查返回值的宏定义,省略
#define FILENAME "test.txt"
//子进程状态信息数据结构
typedef struct{
int pid;//进程号
int fd; //通信套接字
int busy;//进程状态,0代表阻塞,1代表运行,-1代表死亡
}pro_data;
// 用来传输文件的数据结构
typedef struct{
int len;//控制信息,提示接下来的字节长度
char p[1024];//真正的数据信息
}Train_t;
//创建num子进程,传出参数p保存各个子进程的状态信息队列
int fork_child(int num,pro_data * p);
//向传入的套接字传输文件
int transp(int);
//子进程工作函数,传入参数是其与父进程通信的套接字接口
int work(int);
//传送文件描述符,第一个参数是目的描述符,第二个参数是要传送的文件描述符
int sendFD(int,int);
//接收文件描述符,第一个参数是接收信息的套接字描述符,第二个参数是传出参数,传出接收的文件描述符
int recvFD(int,int *);
客户端
#include <fun.h>
#include "pool.h"
int main(int args,char *argv[])
{
struct winsize wsize;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &wsize);
ARG_CHECK(args,3);
int sfd = tcp_connect(argv[1],atoi(argv[2]));
RET_CHECK(sfd,-1,"client tcp connetc");
printf("successfully connetc server.\n");
Train_t train;
int ret;
int len;
ret = recv(sfd,&len,4,0);
RET_CHECK(ret,-1,"服务器已满负荷,请稍候再试");
ret = recv(sfd,train.p,len,0);
RET_CHECK(ret,-1,"getfilename");
train.p[ret]='\0';
int fd = open(train.p,O_RDWR|O_CREAT,0666);
if(fd==-1)
{
printf("文件创建失败,可能存在同名文件!client exit.\n");
close(sfd);
return 0;
}
int size;
ret = recv(sfd,&size,sizeof(int),0);
RET_CHECK(fd,-1,"recv");
printf("the size of file is %d\n",size);
printf("start downloading file %s\n",train.p);
int getsize = 0;
while(1)
{
ret = recv(sfd,&len,4,MSG_WAITALL);
RET_CHECK(ret,-1,"getlen");
if(len == 0 && ret == 4)
{
printf("\r");
printf(">进度:%5.2f%s 已下载:%d",(double)getsize/size*100,"%",getsize);
for(int i = 0;i<(getsize/size)*(wsize.ws_row-35);i++)
{
printf("-");
}
printf(">\ndownload is finished\n");
close(fd);
close(sfd);
return 0;
}
ret = recv(sfd,&train.p,len,MSG_WAITALL);
RET_CHECK(ret,-1,"recv");
if(ret!=len)
{
printf("download error!\n");
close(fd);
close(sfd);
return 0;
}
getsize += ret;
write(fd,train.p,len);
printf("\r");
printf(">进度:%5.2f%s 已下载:%d",(double)getsize/size*100,"%",getsize);
for(int i = 0;i<(double)getsize/size*(wsize.ws_col-35);i++)
{
printf("-");
}
printf(">");
}
}
服务端父进程
int main(int args,char *argv[])
{
signal(2,clean);
ARG_CHECK(args,4);
int process_number = atoi(argv[3]);
num =&process_number;
pro_data *p = (pro_data *)calloc(process_number,sizeof(pro_data));
fork_child(process_number,p);
tp = p;
int sfd = tcp_init(argv[1],atoi(argv[2]));
efd = epoll_create(process_number+1);
struct epoll_event event,evs[11];
event.events = EPOLLIN;
event.data.fd = sfd;
int ret = epoll_ctl(efd,EPOLL_CTL_ADD,sfd,&event);
RET_CHECK(ret,-1,"epoll_ctl");
for(int i =0;i<process_number;i++)
{
event.data.fd = p[i].fd;
ret = epoll_ctl(efd,EPOLL_CTL_ADD,p[i].fd,&event);
RET_CHECK(ret,-1,"epoll_ctl")
}
int newfd;
int count;
int i,j;
int flag;
printf("server is ready for download task\n");
while(1)
{
count = epoll_wait(efd,evs,process_number+1,-1);
if(count == -1&&errno == 4)
{
continue;
}
RET_CHECK(count,-1,"epoll_wait");
for(i=0;i<count;i++)
{
if(evs[i].data.fd == sfd)
{
flag = 0;
newfd = tcp_accept(sfd);
RET_CHECK(newfd,-1,"accept");
for(j=0;j<process_number;j++)
{
if(!p[j].busy)
{
flag = 1;
p[j].busy = 1;
printf("child process %d get the task.\n",j+1);
sendFD(p[j].fd,newfd);
close(newfd);
break;
}
}
if(!flag)
{
close(newfd);
printf("no child is free,the task is rejected.\n");
}
}
for(j = 0;j < process_number; j++)
{
if(evs[i].data.fd == p[j].fd&&p[j].busy == 1)
{
read(p[j].fd,&ret,1);
p[j].busy = 0;
printf("child process %d completed his task.\n",j+1);
break;
}
}
}
}
return 0;
}
子进程创建和工作
int fork_child(int num,pro_data* p)
{
int fds[2];
int pid;
int ret;
for(int i=0;i<num;i++)
{
ret = socketpair(AF_LOCAL,SOCK_STREAM,0,fds);
RET_CHECK(ret,-1,"socketpair");
pid = fork();
if(0 == pid)
{
#ifdef debug
printf("子进程%d创建成功\n",i);
#endif
close(fds[1]);
work(fds[0]);
}
p[i].pid = pid;
p[i].busy = 0;
p[i].fd = fds[1];
close(fds[0]);
}
return 0;
}
int work(int fd)
{
int sfd=0;
while(1)
{
recvFD(fd,&sfd);
#ifdef debug
printf("pid= %d,sfd=%d\n",getpid(),sfd);
#endif
printf("pid =%d begin working.\n",getpid());
transp(sfd);
close(sfd);
printf("pid =%d stop workinng.\n",getpid());
write(fd,&sfd,1);
}
}
文件传输函数
int transp(int sfd)
{
Train_t train;
memset(&train,0,sizeof(train));
int fd = open(FILENAME,O_RDONLY);
RET_CHECK(fd,-1,"open");
train.len = strlen(FILENAME);
strcpy(train.p,FILENAME);
int ret = send(sfd,&train,4+train.len,0);
if(-1 ==ret)
{
printf("client is closed\n");
close(sfd);
close(fd);
return 0;
}
struct stat statbuf;
fstat(fd,&statbuf);
train.len=statbuf.st_size;
ret = send(sfd,&train.len,sizeof(int),0);
if(-1 ==ret)
{
printf("client is closed\n");
close(sfd);
close(fd);
return 0;
}
printf("pid = %d begin to transfer file %s\n",getpid(),FILENAME);
while((train.len = read(fd,train.p,sizeof(train.p)))>0)
{
ret = send(sfd,&train,4+train.len,0);
if(-1 ==ret)
{
printf("client is closed\n");
close(sfd);
close(fd);
return 0;
}
}
train.len = 0;
send(sfd,&train,4,0);
printf("pid = %d completed transfer of file %s\n",getpid(),FILENAME);
close(fd);
close(sfd);
return 0;
}
子进程退出的信号处理函数
pro_data *tp;
int *num;
int efd;
void clean(int sig)
{
int no = 0;
static int flag = 0;
struct epoll_event event;
event.events = EPOLLIN;
if(flag == *num)
{
printf("all child processes are exit!\n");
}
for(int i = 0;i<*num;i++)
{
if(tp[i].busy!=1 && tp[i].busy != -1)
{
tp[i].busy = -1;
event.data.fd = tp[i].fd;
epoll_ctl(efd,EPOLL_CTL_DEL,tp[i].fd,&event);
close(tp[i].fd);
kill(tp[i].pid,9);
printf("child process pid= %d is exit.\n",tp[i].pid);
flag ++;
no = 1;
break;
}
}
if(!no)
printf("no free child process!\n");
}
传递文件描述符
int sendFD(int fd,int newfd)
{
struct msghdr msg;
memset(&msg,0,sizeof(msg));
char buf[10]= "lalalala!";
struct iovec iov[1];
iov[0].iov_base = buf;
iov[0].iov_len = 10;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
struct cmsghdr *cmsg;
int len = CMSG_LEN(sizeof(int));
cmsg = (struct cmsghdr *)calloc(1,len);
cmsg->cmsg_len = len;
cmsg->cmsg_level=SOL_SOCKET;
cmsg->cmsg_type=SCM_RIGHTS;
*(int*)CMSG_DATA(cmsg)=newfd;
msg.msg_control = cmsg;
msg.msg_controllen = len;
int ret;
ret=sendmsg(fd,&msg,0);
RET_CHECK(ret,-1,"sendmsg");
return 0;
}
int recvFD(int fd,int *newfd)
{
struct msghdr msg;
memset(&msg,0,sizeof(msg));
struct iovec iov[1];
char buf[10];
iov[0].iov_base=buf;
iov[0].iov_len=10;
msg.msg_iov=iov;
msg.msg_iovlen=1;
struct cmsghdr *cmsg;
int len=CMSG_LEN(sizeof(int));
cmsg=(struct cmsghdr *)calloc(1,len);
cmsg->cmsg_len=len;
cmsg->cmsg_level=SOL_SOCKET;
cmsg->cmsg_type=SCM_RIGHTS;
msg.msg_control=cmsg;
msg.msg_controllen=len;
int ret;
ret=recvmsg(fd,&msg,0);
RET_CHECK(ret,-1,"sendmsg");
#ifdef debug
printf("recvFD:%s\n",(char *)msg.msg_iov[0].iov_base);
#endif
*newfd=*(int*)CMSG_DATA(cmsg);
return 0;
}
演示效果