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;
}

演示效果

在這裏插入圖片描述

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