【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_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_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_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");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.xdnf.cn/news/1524283.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

线性表之双向链表

1. 双向链表的结构 对于单向链表和单向循环链表而言有一个共同的特点&#xff0c;就是链表的每个节点都只有一个指向后继节点的指针&#xff0c;通过这个指针我们就可以从前往后完成对链表的遍历。但是开弓没有回头箭&#xff0c;遍历到尾节点之后再想要回到头结点&#xff0c…

Python(TensorFlow)和MATLAB及Java光学像差导图

&#x1f3af;要点 几何光线和波前像差计算入瞳和出瞳及近轴光学计算波前像差特征矩阵方法计算光谱反射率、透射率和吸光度透镜像差和绘制三阶光线像差图和横向剪切干涉图分析瞳孔平面焦平面和大气湍流建模神经网络光学像差计算透镜光线传播几何偏差计算像差和像散色差纠正对齐…

lambda表达式用法——C#学习笔记

“Lambda 表达式”是一个匿名函数&#xff0c;它可以包含表达式和语句&#xff0c;并且可用于创建委托或表达式目录树类型。 实例如下&#xff1a; 代码如下&#xff1a; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.…

表连接查询之两个left join与递归SQL

一、如下SQL1 SELECT i.*,su1.name as createName,su2.name as updateNameFROM information ileft join sys_user su1 on su1.idi.create_idleft join sys_user su2 on su2.idi.update_id 二、分析 1、SELECT i.*,su.name as createName,sua.name as updateName&#xff1a; 这…

负载均衡 Ribbon 与 Fegin 远程调用原理

文章目录 一、什么是负载均衡二、Ribbon 负载均衡2.1 Ribbon 使用2.2 Ribbon 实现原理 (★)2.3 Ribbon 负载均衡算法 三、Feign 远程调用3.1 Feign 简述3.2 Feign 的集成3.3 Feign 实现原理 (★) 一、什么是负载均衡 《服务治理&#xff1a;Nacos 注册中心》 末尾提到了负载均…

污点、容忍、不可调度、排水、数据卷

目录 污点taint 污点的格式 1. key:effect 键名&#xff1a;污点类型 2. keyvalue:effect 键名数值&#xff1a;污点类型 污点的类型 1. NoSchedule 2. PreferNoSchedule 3. NoExecute&#xff08;驱逐&#xff09; 设置污点&#xff08;主节点操作&#xff09…

[论文笔记]大模型微调数据配比策略

大模型微调数据配比策略 How Abilities in Large Language Models are Affected by Supervised Fine-tuning Data Composition https://arxiv.org/pdf/2310.05492 一、背景&#xff1a; 大模型是无监督的多任务学习器&#xff0c;其强大的泛化能力可以同时理解并执行多种任务…

PointNet++改进策略 :模块改进 | PAConv,位置自适应卷积提升精度

题目&#xff1a;PAConv: Position Adaptive Convolution with Dynamic Kernel Assembling on Point Clouds来源&#xff1a;CVPR2021机构&#xff1a;香港大学论文&#xff1a;https://arxiv.org/abs/2103.14635代码&#xff1a;https://github.com/CVMI-Lab/PAConv 前言 PA…

elasticsearch文档Delete By Query API(一)

这里的查询需要使用和Search API&#xff08;后文会讲&#xff09;相同的方式来将查询条件作为query的值传递&#xff0c;当然也可以使用q关键字&#xff0c;例如如下请求&#xff1a; curl -X POST “localhost:9200/twitter/_delete_by_query?pretty&quser:kimchy” -H…

HTTP 二、进阶

四、安全 1、TLS是什么 &#xff08;1&#xff09;为什么要有HTTPS ​ 简单的回答是“因为 HTTP 不安全”。由于 HTTP 天生“明文”的特点&#xff0c;整个传输过程完全透明&#xff0c;任何人都能够在链路中截获、修改或者伪造请求 / 响应报文&#xff0c;数据不具有可…

log4j 清除MDC上下文 MDC分类日志

在项目里需要分类收集处理日志信息&#xff0c;使用 log4j的MDC在线程中添加分类信息。不过最近却出现日志信息记录错误的情况&#xff0c;具体来说&#xff0c;就是会出现本来是属于下一个分类的一部分信息莫名的记录到上一个分类的日志文件中了。这很显然是MDC信息错误造成的…

【2024 CCF编程能力等级认证(GESP)Python 】一级大纲

目录 1. 背景2. 考核知识块3. 考核内容3.1 计算机基础知识3.2 编程规范3.3 基础语法3.4 数据类型3.5 三大基本结构3.6 运算符3.7 模块导入与输入输出3.8 Turtle绘图4. 考核目标5. 题型分布6. 考试时长7. 认证时间与报名8. 政策与福利9. GESP一级认证形式 1. 背景 官网&#xff…

[UVM]3.核心基类 uvm_object 域的自动化 copy() compare() print() pack unpack

1.核心基类&#xff1a;uvm_object &#xff08;1&#xff09;虚类只能声明&#xff0c;不能例化。 &#xff08;2&#xff09;uvm_object提供的方法 2.域的自动化&#xff08;field automation&#xff09; &#xff08;1&#xff09;简述 &#xff08;2&#xff09;示例 格…

php、Java、python酒店预约与送餐系统 酒店管理系统 酒店预订入住系统(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

C++系统教程001

1. 安装 Dev-C编程软件 2. 熟悉 Dev-C的界面 3. cout 输出语句的使用 4. 学会 C程序的编译运 一、认识编译器 我们平时所说的程序&#xff0c;一般指双击后就可以直接运行的程序&#xff0c;这样的程序又称为可执行程序。Windows系统下&#xff0c;可执行程序的后缀一般为.ex…

The Llama 3 Herd of Models【论文原文下载】

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间 The Llama 3 Herd of Models【论文原文】 点击下载&#xff1a;原文下载链接 摘要 现代人工智能&#xff08;AI&#xff09;系统由基础模型驱动。本文介绍了一组新的基础模型&#xff0c;称为 Llama 3。它是…

PostgreSQL的repmgr工具介绍

PostgreSQL的repmgr工具介绍 repmgr&#xff08;Replication Manager&#xff09;是一个专为 PostgreSQL 设计的开源工具&#xff0c;用于管理和监控 PostgreSQL 的流复制及实现高可用性。它提供了一组工具和实用程序&#xff0c;简化了 PostgreSQL 复制集群的配置、维护和故障…

欺诈文本分类检测(十):QLora量化微调

1. 引言 前文微调方法概览总结了微调的各种方法&#xff0c;并且在更前面两篇文章Lora单卡训练 和 lora单卡二次调优中已经尝试过用Lora进行微调&#xff0c;本文出于好奇准备尝试下用QLora进行微调的效果。 QLoRA是一种新的微调大型语言模型&#xff08;LLM&#xff09;的方…

使用Python的Elasticsearch客户端 elasticsearch-py 来完成删除现有索引、重新创建索引并测试分词的示例代码

以下是一个使用Python的Elasticsearch客户端 elasticsearch-py 来完成删除现有索引、重新创建索引并测试分词的示例代码 一、安装依赖 pip install elasticsearch二、运行效果 三、程序代码 from elasticsearch import Elasticsearch, NotFoundError# 连接到Elasticsearch es…

PS系统教程32

调色之单通道调色 上次分享内容调色可通过 色阶调色曲线调色 案例-复古 CtrlM调出曲线图选择单色通道-蓝色降到1/2绿色降1/4红色定点上拉 冷风 Alt复位降到1/2绿色降1/4红色定点下拉 调色-色相饱和度&#xff08;ctrlu&#xff09; 原图 只改变背景不改变蜥蜴的颜色 对比…