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

devtools/2024/12/22 22:22:00/


概述

使用了 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_SOCKETint cmsg_type;			//特定协议类型,本程序用SCM_RIGHTSunsigned 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_SOCKETint cmsg_type;			//特定协议类型,本程序用SCM_RIGHTSunsigned 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_initc_tcp_453">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 <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

#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

#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

#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_initc_807">tcp_init.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和portstruct 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 <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添加进epfdevent.data.fd = sfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &event);//将子进程的管道fd,加入epfdint i;for (i = 0; i < childsNum; ++i) {event.data.fd = pChilds[i]._pipefd;epoll_ctl(epfd, EPOLL_CTL_ADD, pChilds[i]._pipefd, &event);}//将接收退出信号的管道加入epfdevent.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都会更新newfdnewfd = 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 <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接口发送fdstruct 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结构体,最后一个成员就是要传输的fdstruct 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接口接收fdstruct 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结构体,最后一个成员就是要接收的fdstruct 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结构体长度//从套接口中接收fdrecvmsg(pipefd, &msg, 0);*cli_fd = *(int*)CMSG_DATA(cmsg);return 0;
}


transfer_file.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

#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和portARGS_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_WAITALLret = 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;
}

http://www.ppmy.cn/devtools/110293.html

相关文章

SpringBoot学习(18)使用spring-boot-admin监控SpringBoot

什么是 Spring Boot Admin? Spring Boot Admin 是一个管理和监控 Spring Boot 应用程序的开源软件。每个应用都认为是一个客户端&#xff0c;通过 HTTP 或者使用 Eureka 注册到 admin server 中进行展示&#xff0c;Spring Boot Admin UI 部分使用 VueJs 将数据展示在前端。 …

【爬虫软件】小红书笔记批量采集工具,含正文内容、IP属地、转评赞藏等

一、背景介绍 1.1 爬取目标 众所周知&#xff0c;小红书是国内最火热的种草社交平台&#xff0c;拥有海量的高品质用户&#xff0c;尤其以女性用户居多&#xff0c;相对于其他平台更具有消费能力。平台上的爆火笔记也成为众多媒体从业者的分析对象。于是&#xff0c;我用pytho…

如何使用elementui实现一个根据页面进度实时增长/前进的进度条

如何使用elementui实现一个根据页面进度实时增长/前进的进度条&#xff0c;当用户点击已完成进度条部分的任何一个值时&#xff0c;例如已完成70%点击35%可以跳到35%时对应的页面呢&#xff1f; <template><div><el-progress :percentage"progressPercent…

CSS之我不会

非常推荐html-css学习视频&#xff1a;尚硅谷html-css 一、选择器 作用&#xff1a;选择页面上的某一个后者某一类元素 基本选择器 1.标签选择器 格式&#xff1a;标签{} <h1>666</h1><style>h1{css语法} </style>2.类选择器 格式&#xff1a;.类…

Java超详细知识点——I/O流(字节流和字符流)

File类&#xff1a; Java API&#xff1a;java.io.File 类 是用来操作文件或文件夹的&#xff0c;无法用来读写 1.首先创建一下file的对象&#xff1a; 里面可以写相对路径或者绝对路径 File file new File("CCC.java"); 也可以使用其他构造方法 //String path …

【PyQt5 应用程序】PyQt基础组件:连接数据库

在开发现代应用程序时,与数据库的交互几乎是不可避免的。不论是存储用户信息、订单详情还是应用配置,数据库都扮演着核心角色。幸运的是,PyQt提供了一系列的工具来简化数据库的操作。在这一部分,我们将探讨如何使用PyQt连接到数据库,并通过具体的例子来说明如何进行数据的…

第四届长城杯-misc

BrickGame 就连连看 或者 改图标会快一点吧 漏洞探踪&#xff0c;流量解密 第一阶段 192.168.30.234 第二阶段 bdb8e21eace81d5fd21ca445ccb35071 bdb8e21eace81d5fd21ca445ccb350715a76f6751576dbe1af49328aa1d2d2bea16ef62afa3a7c616dbdb8e21eace81d5fd21ca445ccb35071 …

【代码随想录训练营第42期 续Day52打卡 - 图论Part3 - 卡码网 103. 水流问题 104. 建造最大岛屿

目录 一、做题心得 二、题目与题解 题目一&#xff1a;卡码网 103. 水流问题 题目链接 题解&#xff1a;DFS 题目二&#xff1a;卡码网 104. 建造最大岛屿 题目链接 题解&#xff1a;DFS 三、小结 一、做题心得 也是成功补上昨天的打卡了。 这里继续图论章节&#xff…