【C-实践】文件服务器(1.0)

概述

使用了 tcp + epoll + 进程池,实现文件下载服务器


功能


主要功能:客户端连接服务器,然后自动下载文件


次要功能:客户端接收时显示进度条


启动


启动服务器

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: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


客户端

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


检查传输文件是否正确

  1. 查看文件大小 $ du -h file
  2. 查看文件唯一哈希值 $ md5sum file


服务器搭建


1 创建进程池

根据子进程的数量,创建存储子进程信息的数组
根据子进程的数量,循环创建子进程,并初始化子进程的信息(子进程id、是否空闲,通讯管道)


2 主进程分配任务给子进程
建立一个tcp类型的正在监听的套接字
使用epoll管理所有套接字
1. 有新的客户端连接,得到一个客户端套接字,交给一个空闲的子进程处理
2. 等待子进程工作完毕,将其状态设为空闲
3. 等待退出信号,收到后回收进程池资源,退出程序


3 资源进程(子进程)处理具体业务
等待任务(主进程发送过来的客户端套接字)
设置本进程为忙碌状态
工作(发送文件给客户端)
通知主进程任务完成



进程池退出方式

方式一:给主进程发送退出信号,主进程收到信号后,kill所有子进程,然后回收所有子进程的资源,再退出主进程 (本文采用)


方式二:给主进程发送退出信号,主进程收到信号后,通知所有子进程退出

  1. 如果是非忙碌的子进程,直接退出
  2. 如果是忙碌的子进程,就忙完了再退出


传输文件方式

方式一:使用自定义协议传输:先发送本次数据长度,再发送数据内容 (本文使用)


方式二:使用零拷贝的方式传输,比如mmap或者splice



代码实现逻辑


main_server.c 服务器主流程

步骤:

  1. 从配置文件中拿到,本服务器ip地址、port端口号、进程数量
  2. 创建一个子进程数组,用来存储所有子进程的信息
  3. 创建进程池,并用子进程数组记录子进程的信息(根据子进程的数组和子进程的数量)
  4. 建立退出管道,并注册SIGUSR1信号(用于主进程的异步退出)
  5. 创建一个tcp类型的服务器套接字用于监听客户端的连接
  6. 处理来自客户端和进程池的请求,以及退出信号
    1. 将每一个客户端的连接交给空闲子进程,
    2. 将请求的忙碌子进程设为空闲状态
    3. 收到退出信号,依次终止子进程,回收子进程资源,退出主进程
  7. 最后释放子进程数组的空间


init_process_pool.c 创建进程池

输入:子进程数组pChilds,子进程的数量childsNum

输出:一个有childsNum个子进程信息的数组


为什么用socketpair生成一对套接口,而不是用管道等方式在进程间传递套接字(文件描述符)?

每一个进程都会维护一个数字与文件描述符对应的表

每个文件描述符都会在内核中维护一个文件对象数据结构的,不仅仅是一个数字

而用管道传输文件描述符时,只会传送数字,而不会传送文件对象

因此需要特殊的接口,在进程之间,传递文件描述符的数据结构


步骤:

  1. 循环childsNum次,创建子进程

    1. 使用socketpair创建一对用于本地通信的tcp类型的套接口fds[2](全双工管道,用于传递客户端套接字)

    2. fork出一个子进程

    3. 子进程设置

      1. 关闭套接口的写端fds[1](子进程只需要从套接口中读取客户端套接字)
      2. 子进程业务逻辑
      3. 子进程退出
    4. 主进程设置,将新建的子进程信息放入子进程数组内

      1. 关闭套接口的读端fds[0](主进程只需要向套接口中写入客户端套接字)
      2. 记录第i个子进程的进程id
      3. 设置第i个子进程的状态为空闲
      4. 设置第i个子进程的通信管道为fds[0]


child_process.c 子进程业务逻辑

输入:子进程套接口

输出:将目标文件发送给客户端


步骤:

  1. 死循环,处理业务
    1. 等待任务:阻塞在套接口,等待主进程发来的客户端套接字
    2. 干活:将目标文件发送给客户端
    3. 任务结束:通知主进程任务完成


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成员,就是用来传输客户端套接字文件对象的

步骤:

  1. 初始化一个struct msghdr结构体msg,用来传递客户端套接字
  2. 创建一个struct iovec结构体,初始化&赋值,然后设为msg的参数
  3. 创建一个struct cmsghdr结构体cmsg,初始化,然后设为msg的参数
    1. CMSG_LEN宏得到cmsg的大小
    2. cmsg->cmsg_level = SOL_SOCKET; 原始协议
    3. cmsg->cmsg_type = SCM_RIGHTS; 特定协议类型
    4. 传输的客户端套接字 *(int*)CMSG_DATA(cmsg) = cli_fd;
  4. 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成员,就是用来传输客户端套接字文件对象的

步骤:

  1. 初始化一个struct msghdr结构体msg,用来接收客户端套接字
  2. 创建一个struct iovec结构体,初始化,然后设为msg的参数
  3. 创建一个struct cmsghdr结构体cmsg,初始化,然后设为msg的参数
    1. CMSG_LEN宏得到cmsg的大小
    2. cmsg->cmsg_level = SOL_SOCKET; 原始协议
    3. cmsg->cmsg_type = SCM_RIGHTS; 特定协议类型
    4. 传输的客户端套接字 *(int*)CMSG_DATA(cmsg) = cli_fd;
  4. 从套接口中用recvmsg接收msg
  5. 从msg中提取客户端套接字


tcp_init.c 生成一个服务器正在监听的tcp套接字

输入:服务器的ip地址,服务器的port端口号

输出:绑定了服务器ip和port,正在监听的tcp类型的套接字


步骤:

  1. 使用socket生成一个tcp类型的套接字
  2. 给套接字绑定服务器的ip地址和port端口号
  3. 开始监听


interact_cli.c 主进程处理客户端和进程池请求,以及退出信号

输入:服务器套接字,子进程数组,子进程数量,退出管道读端

输出:将客户端的请求转发给空闲子进程,将完成任务的子进程设为空闲状态,如果收到退出信号则回收所有子进程资源并退出


步骤:

  1. 创建epoll管理所有请求
    1. 将服务器套接字,加入epoll,用于接收客户端请求
    2. 将子进程数组内的所有通信管道,加入epoll,用于处理子进程的请求
    3. 将退出管道读端,加入epoll,用于接收退出信号
  2. epoll循环等待就绪的文件描述符
    1. 如果服务器套接字就绪,接收客户端套接字并其交给一个空闲子进程处理,然后关闭客户端套接字
    2. 如果子进程的管道就绪(表示子进程已处理完一个任务),读取管道,然后将该子进程的状态设为空闲
    3. 如果收到退出信号,依次关闭子进程,回收所有子进程的资源,然后退出主进程


send_file 服务器发送文件

输入:客户端套接字,待传输文件名

输出:使用私有协议将文件传输给客户端


自定义传输文件协议:小货车

//传输文件协议:小货车
typedef struct {
    int _data_len;//货车头,表示数据长度
    char _data[1000];//火车车厢,表示数据
}Truck_t;

步骤:

  1. 初始化一个小货车(使用自定义协议传输文件,防止tcp粘包问题)
  2. 将文件名添加上资源目录的路径,再open打开待传文件
  3. 传输中
    1. 先发文件名
    2. 再发文件大小
    3. 循环发送文件内容(小货车每次最多发1000个字节)
      1. 给小货车装车,发货
      2. 如果全部传输完毕之后,通知客户端,并退出循环
      3. 如果客户端异常断开,则退出循环(此时会收到SIGPIPE信号)
  4. 传输结束,关闭待传文件



main_client.c 客户端主流程

命令行参数:配置文件(服务器ip地址,服务器端口号)


步骤:

  1. 读取配置文件,拿到服务器的ip和port
  2. 生成一个tcp类型的套接字,并绑定服务器的ip和端口
  3. 申请连接服务器
  4. 接收文件(根据自定义传输协议小货车接收:先接受数据长度,再根据长度接收数据)
    1. 先接受文件名,根据文件名open一个新文件
    2. 再接收文件大小(为了打印接收进度条)
    3. 循环接收文件内容(根据协议,每次最多接收1000个字节)
      1. 先接收数据长度(如果为空则表示接收完毕,退出循环)
      2. 根据数据长度,接收数据内容
      3. 根据当前进度和总大小打印进度条(用fflush刷新标准输出,避免光标跳动)
      4. 将数据写入文件
    4. 关闭文件
  5. 关闭服务器套接字


具体代码

服务器代码


服务器头文件 prcess_pool.h

#ifndef __PROCESSPOOL_H__
#define __PROCESSPOOL_H__

#include 

//检查命令行参数个数
#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

#include "../include/process_pool.h"
#include "../include/process_pool.h"
#include 
#include 
#include 
#include 

//与主进程通信的管道,用来传递退出信号
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 
#include 
#include 
#include 

//功能:创建进程池
//参数:子进程数组,子进程数量
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 
#include 
#include 
#include 
#include 

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

#include 
#include 
#include 

#include 

#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 
#include 
#include 
#include 

#include 
#include 



//功能:服务器主进程处理来自客户端和进程池的请求,以及退出信号
//参数:服务器套接字,子进程数组,子进程数量, 退出管道读端
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

#include "../include/process_pool.h"
#include 
#include 
#include 
#include 
/* #include     //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

#include 
#include 
#include 

//open
#include 
#include 
#include 
//send
#include 


#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

#include 
#include 
#include 
#include 

#include 
#include 
#include 
#include 


//检查命令行参数个数
#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;
}

你可能感兴趣的:(C语言实践,c语言,服务器,tcp,小程序)