Linux环境:C编程实战——实现文件下载

设计目标

  • 采用C/S模式,服务端发送文件,多个客户端可以同时下载文件
  • 服务端的进程不会因为客户端的操作(意外退出)崩溃
  • 关闭服务端时各个子进程可以有序退出
  • 需要设计协议保证文件传输不出差错
  • 客户端可以动态查看文件下载进度

设计思路

服务端请求响应
  • 服务端采用进程池模式,父进程监听服务端口,创建多个子进程负责传输文件,有客户端请求时,建立连接,然后唤醒一个阻塞的子进程处理。
  • 父进程通过维护一个子进程状态队列来实时更新各个子进程的状态。
  • 采用传递socket描述符的方式使子进程可以直接与客户端通信。
  • 父子进程之间通过一对匿名套接字通信,有新请求时父进程写套接字,通知子进程开始工作,子进程传输完毕后,写套接字通知父进程已经工作完毕进入阻塞状态
  • 父进程采用epoll模型监控各个子进程的通信套接字和监听端口的套接字:
    • 监听端口套接字可读,代表有新的下载请求,查找空闲子进程交付任务,更新子进程状态队列
    • 子进程套接字可读,代表有子进程完成工作,更新子进程状态队列
服务端进程退出
  • 注册信号处理函数实现有序退出,收到信号后查找子进程状态队列,杀死第一个空闲子进程,标记该进程已注销,将其监控事件从epoll监控实例中删除,关闭套接字,打印提示。同时处理epoll_wait函数由于监控对象变化而返回的errno == 4错误,直接忽略即可。
文件传输
  • 通过tcp连接进行文件传输,每次传输数据之前应先发送本次传输的字节数,使得客户端不至于多读或少读数据。
  • 正式传输之前需要传输文件名和文件大小,方便客户端创建文件和计算下载进度。
  • 服务端子进程每次send后需要检查返回值来确定客户端的状态,如果返回-1则打印提示客户端断开连接,重新阻塞。
  • 文件传输结束后发送一个0代表传输结束
  • 客户端每次先读一个int型的控制数据代表接下来要读的字节数,然后再读数据。如果控制数据为0代表传输结束。
  • 需要注意的是,recv的第四个参数要设置为阻塞,MSG_WAITALL,即不满足所读长度时就一直阻塞;
  • 客户端每次读完数据都要和本次数据的控制数据对比,查看长度是否一致,不一致证明传输出差错,打印提示信息,退出客户端,重新进行下载。
客户端信息显示
  • 客户端每成功接收一组数据,显示当前的下载进度百分比,通过\r回到行首来覆盖重写。
  • 客户端根据下载进度打印若个标志字符来显示动态进度条。
  • 可以 通过获取当前窗口的winsize结构体来确定需要打印的进度条长度,从而适应不同大小的窗口。
  • 参考链接:linux获取终端窗口的大小方法

代码实现

头文件
#pragma once
#include <fun.h> //包含各种需要用到的标准库和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;
    //get filename and create file
    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;
    }
    //get size of file 
    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;

    //创建tcp端口等待客户端连接
    int sfd = tcp_init(argv[1],atoi(argv[2]));//自己封装的,需要自行实现,相当于socket,bind,listen三连
    
    //create epoll instance
    efd = epoll_create(process_number+1);
    struct epoll_event event,evs[11];
    event.events = EPOLLIN;
    event.data.fd = sfd;
    
    //监听服务端的listen端口
    int ret = epoll_ctl(efd,EPOLL_CTL_ADD,sfd,&event);
    RET_CHECK(ret,-1,"epoll_ctl");
    
    //添加子进程监听socket
    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")
    }
    
    //begin task
    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);
        
        //epoll实例状态改变,子进程退出删除监控事件导致的错误码,忽略处理
        if(count == -1&&errno == 4)
        {
            continue;
        }

        RET_CHECK(count,-1,"epoll_wait");
        
        for(i=0;i<count;i++)
        {
            //sfd is ready, a new client is connecting
            if(evs[i].data.fd == sfd)
            {
                flag = 0;
                newfd = tcp_accept(sfd);
                RET_CHECK(newfd,-1,"accept");
               
               //find free child 查找空闲进程
				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;
                    }
                }
                //no child is free 没有空闲进程
                if(!flag)
                {
                    close(newfd);
                    printf("no child is free,the task is rejected.\n");
                }
            }
            //check if a child is free 查看是否有子进程完成任务
            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;
}

子进程创建和工作
//create children processes,创建子进程,子进程循环等待父进程唤醒
int fork_child(int num,pro_data* p)
{
    int fds[2];
    int pid;
    int ret;
    for(int i=0;i<num;i++)
    {
        //apply a pair of anonymous sockets 申请两个套接字,第一个用于子进程读写,第二个用于父进程读写
        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");
    
    //transfer filename
    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;
    }
    
    //transfer the size of file 
    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;//指向epoll实例
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;
}

演示效果

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章