【C-实践】文件服务器(3.0)
- 文件服务器1.0
- 文件服务器2.0
- 文件服务器4.0
概述
使用了 tcp
+ epoll
+ 线程池 + 生产者消费者模型,实现文件服务器
有两个进程,主进程负责接收退出信号用来退出整个程序;子进程负责管理线程池、客户端连接以及线程池的退出
子进程中的主线程生产任务,其他子线程消费任务
功能
主要功能:客户端连接服务器,然后自动下载文件
注意
实际传输速度:主要取决于网络传输速度,而不是本地传输速度。网速不高的情况下,零拷贝接口并没有优势。
服务器搭建
- 创建管道,用于给子进程传递退出消息,创建子进程
- 主进程,注册
SIGUSR1
信号,等待回收子进程的资源,退出程序 - 子进程
- 从配置信息中取出:服务器的
ip
地址,port
端口号,线程数量 - 根据线程数量,创建线程池,初始化线程池,启动线程池
- 建立
tcp
监听,将服务器套接字和退出管道加入epoll
管理,等待客户端的连接- 如果有新客户端,将其加入线程池的任务队列中,通知线程池中的线程执行任务
- 如果退出管道就绪,取消并回收所有子线程,退出进程
- 从配置信息中取出:服务器的
客户端搭建
- 从配置文件中取出:服务器的
ip
地址,port
端口号 - 连接服务器
- 下载文件
启动
启动服务器
1、在bin
目录下生成可执行文件
w@Ubuntu20:bin $ gcc ../src/*.c -o server -lpthread
2、启动服务器
w@Ubuntu20:bin $ ./server ../conf/server.conf
启动客户端
1、在客户端的目录下生成可执行文件
w@Ubuntu20:client $ gcc *.c -o client
2、启动客户端
w@Ubuntu20:client $ ./client client.conf
目录设计
服务器
- bin:存放二进制文件
- conf:存放配置文件
- include:存放头文件
- src:存放源文件
w@Ubuntu20:src $ tree ..
..
├── bin
│ └── server
├── conf
│ └── server.conf
├── include
│ ├── head.h
│ ├── task_queue.h
│ └── thread_pool.h
├── resource
│ └── file
└── src├── interact_cli.c├── main_server.c├── send_file_truck.c├── send_file_mmap.c├── send_file_splice.c├── task_queue.c├── tcp_init.c└── thread_pool.c
客户端
w@Ubuntu20:client $ tree
.
├── client
├── client.conf
├── main_client.c
├── recv_file_mmap.c
├── recv_file_splice.c
└── recv_file_truck.c
配置文件
服务器配置文件 server.conf
存放服务器ip
地址,服务器port
端口,线程池中的线程数量
根据实际情况自行更改
192.168.160.129
2000
10
客户端配置文件 client.conf
存放服务器ip
地址,服务器port
端口
根据实际情况自行更改
192.168.160.129
2000
线程池退出方式
方式一:(本文采用)
给主进程发送退出信号
主进程收到信号后通知子进程退出
子进程收到退出信号后取消线程池,并回收线程池资源。
方式二:
给主进程发送退出信号
主进程收到信号后,通知子进程退出
- 如果是非忙碌的子进程,直接退出
- 如果是忙碌的子进程,就忙完了再退出
在方式一的基础上,给任务队列中新增一个退出标志位,每一个线程处理任务前,先看标志位决定是否退出线程
传输文件方式
方式一:使用自定义协议传输:先发送本次数据长度,再发送数据内容
方式二:使用零拷贝的方式传输
mmap和sendfile支持的最大发送字节数是2G(2G以上的文件,无法用mmap和sendfile)
splice没有发送字节数限制,每次最大发送65535字节(管道的最大承载量)
mmap零拷贝接口
NAMEmmap, munmap - map or unmap files or devices into memorySYNOPSIS#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void *addr, size_t length);
注意:服务器用mmap
发送的文件,客户端也需要用mmap
接收,因为不再是先接数据长度再接数据内容的形式了
//服务器用mmap建立文件映射,将文件直接映射到内核发送缓冲区
char *pMap = (char*)mmap(NULL, file_info.st_size, PROT_READ, MAP_SHARED, file_fd, 0);
ERROR_CHECK(pMap, (char*)-1, "mmap");send(socket_fd, pMap, file_info.st_size, 0);
munmap(pMap, file_info.st_size);
//客户端也需要用mmap接收,或者用while+recv接收(直接接收数据,不用先接收数据长度)
ftruncate(fd, filesize);//先固定文件大小
char *pMap = (char*)mmap(NULL, filesize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
ERROR_CHECK(pMap, (char*)-1, "mmap");recv(sfd, pMap, filesize, MSG_WAITALL); //要用MSG_WAITALL参数,避免没有全部接收到
munmap(pMap, filesize);
sendfile零拷贝接口
在两个文件描述符之间直接发送文件,将in_fd
发给out_fd
NAMEsendfile - transfer data between file descriptorsSYNOPSIS#include <sys/sendfile.h>ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
splice零拷贝接口
splice
传输必须要借助管道
将数据写入管道,再从管道传给发送缓冲区(是不经过用户态空间的)
管道最多可以存储65535个字节,如果文件较大则需要循环传输
splice
没有字节数限制,比上面两个好用一点,可以控制发送的快慢(第5个参数决定每次传输的字节数量)
NAMEsplice - splice data to/from a pipeSYNOPSIS#define _GNU_SOURCE /* See feature_test_macros(7) */#include <fcntl.h>ssize_t splice(int fd_in, loff_t *off_in, int fd_out,loff_t *off_out, size_t len, unsigned int flags);
代码
服务器代码
head.h
#ifndef __HEAD_H__
#define __HEAD_H__#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <pthread.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;} }int tcp_init(char *ip, int port);#endif
task_queue.h
#ifndef __TASK_QUEUE_H__
#define __TASK_QUEUE_H__#include "head.h"//任务队列节点
typedef struct TaskNode {int _clifd; //客户端fdstruct TaskNode *_pNext;
}TaskNode_t, *pTaskNode_t;//任务队列
typedef struct TaskQueue {int _size; //队列大小pTaskNode_t _pHead; //队头pTaskNode_t _pTail; //队尾pthread_cond_t _cond; //条件变量pthread_mutex_t _mutex; //互斥锁
}TaskQueue_t, *pTaskQueue_t;//初始化任务队列
int init_TaskQueue(pTaskQueue_t pQueue);
//入队
int push_TaskQueue(pTaskQueue_t pQueue, pTaskNode_t pNew);
//得到队头元素
int get_TaskNode(pTaskQueue_t pQueue, pTaskNode_t *ppGet);#endif
thread_pool.h
#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__#include "head.h"
#include "task_queue.h"//线程池
typedef struct {int _thread_num; //子线程数量pthread_t *_pthid; //子线程数组TaskQueue_t _que; //任务队列
}ThreadPool_t, *pThreadPool_t;//初始化线程池
int init_ThreadPool(pThreadPool_t pPool, int thread_num);//启动线程池
int boot_ThreadPool(pThreadPool_t pPool);//功能:服务器主线程,将新连接的客户端加入线程池中的任务队列,然后通知子线程处理
int interact_cli(int sfd, pThreadPool_t pPool, int exitpipe);//使用私有协议传输数据,给另一个进程传输文件
int send_file_truck(int socket_fd, char *filename);//使用sendfile,给另一个进程传输文件
int send_file_sendfile(int socket_fd, char *filename);//使用mmap接口,给另一个进程传输文件
int send_file_mmap(int socket_fd, char *filename);//使用splice接口,给另一个进程传输文件
int send_file_splice(int socket_fd, char *filename);#endif
main_server.c
#include "../include/head.h"
#include "../include/thread_pool.h"//发送信号,实现异步退出线程池
int exitpipe[2];
void sig_func(int sigNum)
{printf("sig %d is coming!\n", sigNum);write(exitpipe[1], "a", 1);
}int main(int argc, char *argv[])
{//配置文件ARGS_CHECK(argc, 2);//父子传递退出标记的管道pipe(exitpipe);//父进程用来接收退出信号if (fork()) {close(exitpipe[0]);signal(SIGUSR1, sig_func);//回收子进程的资源wait(NULL);printf("thread_pool exit!\n");exit(0);}//子进程用来管理线程池close(exitpipe[1]);//从配置文件中取出服务器ip地址、port端口号、子线程数量FILE *fp = fopen(argv[1], "r");char ip[128] = {0};int port = 0;int thread_num = 0;fscanf(fp, "%s%d%d", ip, &port, &thread_num);fclose(fp);//创建线程池,并初始化ThreadPool_t pool;init_ThreadPool(&pool, thread_num);//启动线程池boot_ThreadPool(&pool);//创建一个正在监听的tcp套接字int sfd = tcp_init(ip, port);//处理客户端的请求,和退出请求//有新请求就新建任务节点,放入任务队列//子线程等待任务,有任务立刻处理if (-1 != sfd) {interact_cli(sfd, &pool, exitpipe[0]);}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;
}
thread_pool.c
#include "../include/head.h"
#include "../include/thread_pool.h"
#include "../include/task_queue.h"//线程清理函数
void clean_func(void *p)
{pTaskQueue_t pQue = (pTaskQueue_t)p;pthread_mutex_unlock(&pQue->_mutex);
}//线程函数
void *thread_func(void *p)
{//拿到任务队列地址pTaskQueue_t pQue = (pTaskQueue_t)p;pTaskNode_t pCur = NULL;while (1) {//取任务pthread_mutex_lock(&pQue->_mutex);pthread_cleanup_push(clean_func, pQue);if (0 == pQue->_size) {pthread_cond_wait(&pQue->_cond, &pQue->_mutex);}get_TaskNode(pQue, &pCur);pthread_mutex_unlock(&pQue->_mutex);//执行任务,发送文件char filename[128] = "file";send_file_truck(pCur->_clifd, filename); printf("----------------------------send finish!\n");//任务完成,销毁任务节点free(pCur);pCur = NULL;pthread_cleanup_pop(1);}pthread_exit(NULL);
}//初始化线程池
int init_ThreadPool(pThreadPool_t pPool, int thread_num)
{pPool->_thread_num = thread_num;pPool->_pthid = (pthread_t*)calloc(thread_num, sizeof(pthread_t));init_TaskQueue(&pPool->_que);return 0;
}//启动线程池
int boot_ThreadPool(pThreadPool_t pPool)
{printf("thread_num: %d\n", pPool->_thread_num);for (int i = 0; i < pPool->_thread_num; ++i) {pthread_create(&pPool->_pthid[i], NULL, thread_func, &pPool->_que);}return 0;
}
task_queue.c
#include "../include/head.h"
#include "../include/task_queue.h"//初始化任务队列
int init_TaskQueue(pTaskQueue_t pQueue)
{pQueue->_size = 0;pQueue->_pHead = pQueue->_pTail = NULL;pthread_cond_init(&pQueue->_cond, NULL);pthread_mutex_init(&pQueue->_mutex, NULL);return 0;
}//入队
int push_TaskQueue(pTaskQueue_t pQueue, pTaskNode_t pNew)
{if (NULL == pQueue->_pHead) {pQueue->_pHead = pQueue->_pTail = pNew;}else {pQueue->_pTail->_pNext = pNew;pQueue->_pTail = pNew;}++pQueue->_size;return 0;
}//得到队头元素
int get_TaskNode(pTaskQueue_t pQueue, pTaskNode_t *ppGet)
{//没有元素if (0 == pQueue->_size) {return -1;}//有元素,取出,更新队头*ppGet = pQueue->_pHead;pQueue->_pHead = pQueue->_pHead->_pNext;//只有一个元素,更新队尾if (1 == pQueue->_size) {pQueue->_pTail = NULL;}//减小队列长度--pQueue->_size;return 0;
}
interact_cli.c
#include "../include/head.h"
#include "../include/task_queue.h"
#include "../include/thread_pool.h"//将fd加入epfd
int epollAddFd(int fd, int epfd)
{struct epoll_event event;memset(&event, 0, sizeof(event));event.events = EPOLLIN;event.data.fd = fd;int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);ERROR_CHECK(ret, -1, "EPOLL_CTL_ADD");return 0;
}//将fd从epfd中移除
int epollDelFd(int fd, int epfd)
{struct epoll_event event;memset(&event, 0, sizeof(event));event.events = EPOLLIN;event.data.fd = fd;int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);ERROR_CHECK(ret, -1, "EPOLL_CTL_DEL");return 0;
}//功能:服务器主线程,将新连接的用户加入线程池中的任务队列,然后通知子线程处理
//参数:服务器套接字,线程池地址,退出管道
int interact_cli(int sfd, pThreadPool_t pPool, int exitpipe)
{//使用epoll管理所有文件描述符int epfd = epoll_create(1);//将sfd添加进epfdepollAddFd(sfd, epfd);//将用来退出的管道加入epfdepollAddFd(exitpipe, epfd);int readyFdNum = 0;//就绪的文件描述符数量struct epoll_event evs[2]; //epoll_wait等待数组的大小int newfd = 0;//客户端的套接字//epoll等待就绪的文件描述符while (1) {readyFdNum = epoll_wait(epfd, evs, 2, -1);int i;for (i = 0; i < readyFdNum; ++i) {//服务端套接字就绪,有新用户连接if (evs[i].data.fd == sfd) {//接收用户端newfd = accept(sfd, NULL, NULL);//新建任务节点pTaskNode_t pNew = (pTaskNode_t)calloc(1, sizeof(TaskNode_t));pNew->_clifd = newfd;pthread_mutex_lock(&pPool->_que._mutex);//加锁push_TaskQueue(&pPool->_que, pNew); //将新节点放入任务队列pthread_cond_signal(&pPool->_que._cond);//通知子线程pthread_mutex_unlock(&pPool->_que._mutex);//解锁}//退出管道就绪,退出线程池if (evs[i].data.fd == exitpipe) {//取消线程池for (int j = 0; j < pPool->_thread_num; ++j) {pthread_cancel(pPool->_pthid[j]);}//回收线程池的资源for (int j = 0; j < pPool->_thread_num; ++j) {pthread_join(pPool->_pthid[j], NULL);}//退出子进程exit(0);}}}return 0;
}
send_file_truck.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_truck(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));ERROR_CHECK(truck._data_len, -1, "read");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;
}
send_file_mmap.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>
//mmap
#include <sys/mman.h>#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\perror(msg); return -1;} }//传输文件协议:小货车
typedef struct {int _data_len;//货车头,表示数据长度char _data[1000];//火车车厢,表示数据
}Truck_t;//使用mmap,给另一个进程传输文件
int send_file_mmap(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");printf("filesize = %ld\n", file_info.st_size);//再发文件内容//用mmap建立文件映射,将文件直接映射到内核发送缓冲区char *pMap = (char*)mmap(NULL, file_info.st_size, PROT_READ, MAP_SHARED, file_fd, 0);ERROR_CHECK(pMap, (char*)-1, "mmap");send(socket_fd, pMap, file_info.st_size, 0);munmap(pMap, file_info.st_size);//关闭传输文件close(file_fd);return 0;
}
send_file_sendfile.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>
//sendfile
#include <sys/sendfile.h>#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\perror(msg); return -1;} }//传输文件协议:小货车
typedef struct {int _data_len;//货车头,表示数据长度char _data[1000];//火车车厢,表示数据
}Truck_t;//使用sendfile,给另一个进程传输文件
int send_file_sendfile(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");printf("filesize = %ld\n", file_info.st_size);//再发文件内容//用sendfile接口,整体传输sendfile(socket_fd, file_fd, 0, file_info.st_size);//关闭传输文件close(file_fd);return 0;
}
send_file_splice.c
#define _GNU_SOURCE
#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;//使用splice,给另一个进程传输文件
int send_file_splice(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");printf("filesize = %ld\n", file_info.st_size);//再发文件内容//用splice接口,需要借助管道int fds[2];pipe(fds);int cur_len = 0; //已经发送的字节数/* int maxsize = 4096; //每次最多发送的字节数,修改这个参数来控制传输的速度 */int maxsize = 1280; //每次最多发送的字节数,修改这个参数来控制传输的速度//先读文件到管道,再从管道放入发送缓冲区while (cur_len < file_info.st_size) {ret = splice(file_fd, 0, fds[1], 0, maxsize, 0);ret = splice(fds[0], 0, socket_fd, 0, ret, 0);cur_len += ret;}//关闭传输文件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>
#include <sys/mman.h>
#include <sys/time.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;}}//从服务器下载文件,用while+recv+小货车的方式
int recv_file_truck(int sfd);//从服务器下载文件,用mmap的方式
int recv_file_mmap(int sfd);//从服务器下载文件,用splice的方式
int recv_file_splice(int sfd);int main(int argc, char *argv[])
{ARGS_CHECK(argc, 2);//从配置文件中拿到服务器的ip和portFILE *fp = fopen(argv[1], "r");char ip[128] = {0};int port = 0;fscanf(fp, "%s%d", ip, &port);fclose(fp);//生成一个tcp类型的套接字,并连接上了服务器int sfd = 0;/* tcp_connect(ip, port, &sfd); */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");printf("connect server!\n");//计算下载时间,用gettimeofday接口struct timeval begin;struct timeval end;gettimeofday(&begin, NULL);//下载文件recv_file_truck(sfd);/* recv_file_mmap(sfd); *//* recv_file_splice(sfd); */gettimeofday(&end, NULL);printf("cost time : %ld us\n", (end.tv_sec - begin.tv_sec) * 1000000 + end.tv_usec - begin.tv_usec);//关闭服务器套接字close(sfd);return 0;
}
recv_file_truck.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.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 recv_file_truck(int sfd)
{int ret = -1;//接收文件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_WRONLY|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 (cur_size < total_size) {//重置小货车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);if (0 == ret) {printf("Download finish!\n");break;}//将接收数据写入文件write(file_fd, truck._data, ret);cur_size += ret;//打印进度条rate = (float)cur_size / total_size;printf("--------------------------%5.2f%%\r", rate * 100);fflush(stdout);//防止光标抖动}//关闭文件close(file_fd);return 0;
}
recv_file_mmap.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\perror(msg);\return -1;}}//从服务器下载文件,用mmap的方式
int recv_file_mmap(int sfd)
{//先接收文件名字int data_len = 0;char filename[1000] = {0};recv(sfd, &data_len, sizeof(int), 0);recv(sfd, filename, data_len, 0);//创建一个新文件int file_fd = open(filename, O_RDWR | O_CREAT, 0600);ERROR_CHECK(file_fd, -1, "open");//再接收文件大小off_t filesize = 0;//此处类型必须为off_t,否则ftrucate会失败recv(sfd, &data_len, sizeof(int), 0);recv(sfd, &filesize, data_len, 0);printf("filesize: %ld\n", filesize);//用mmap接收文件int ret = ftruncate(file_fd, filesize);ERROR_CHECK(ret, -1, "ftruncate");printf("recv start...\n");char *pMap = (char*)mmap(NULL, filesize, PROT_READ|PROT_WRITE, MAP_SHARED, file_fd, 0);ERROR_CHECK(pMap, (char*)-1, "mmap");recv(sfd, pMap, filesize, MSG_WAITALL);munmap(pMap, filesize);printf("recv finish!\n");//关闭文件close(file_fd);return 0;
}
recv_file_splice.c
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>//检查系统调用返回值
#define ERROR_CHECK(ret, num, msg) { if (ret == num) {\perror(msg);\return -1;}}//从服务器下载文件,用splice的方式
int recv_file_splice(int sfd)
{int ret = -1;//先接收文件名字int data_len = 0;char filename[1000] = {0};recv(sfd, &data_len, sizeof(int), 0);recv(sfd, filename, data_len, 0);//创建一个新文件int file_fd = open(filename, O_RDWR | O_CREAT, 0600);ERROR_CHECK(file_fd, -1, "open");//再接收文件大小off_t filesize = 0;//此处类型必须为off_t,否则ftrucate会失败recv(sfd, &data_len, sizeof(int), 0);recv(sfd, &filesize, data_len, 0);printf("filesize: %ld\n", filesize);//用splice接收文件int fds[2];pipe(fds);int cur_len = 0; //当前接收长度int maxsize = 4096;//每次传输的最大字节数printf("recv start...\n");while (cur_len < filesize) {ret = splice(sfd, 0, fds[1], 0, maxsize, 0);ret = splice(fds[0], 0, file_fd, 0, ret, 0);cur_len += ret;}printf("recv finish!\n");//关闭文件close(file_fd);return 0;
}
总结
一个练习tcp
,epoll
和线程池的小项目
相关文章:

【C-实践】文件服务器(3.0)
文件服务器1.0文件服务器2.0文件服务器4.0 概述 使用了 tcp epoll 线程池 生产者消费者模型,实现文件服务器 有两个进程,主进程负责接收退出信号用来退出整个程序;子进程负责管理线程池、客户端连接以及线程池的退出 子进程中的主线程生…...

LeetCode 2181.合并零之间的节点
题目描述 给你一个链表的头节点 head ,该链表包含由 0 分隔开的一连串整数。链表的 开端 和 末尾 的节点都满足 Node.val 0 。 对于每两个相邻的 0 ,请你将它们之间的所有节点合并成一个节点,其值是所有已合并节点的值之和。然后将所有 0 …...

千益畅行,共享旅游卡,引领旅游新潮流
千益畅行旅游卡是一款专为旅游爱好者打造的超值卡片。它就像一把神奇的钥匙,为您打开国内丰富多彩的旅游世界。 我们的旅游卡拥有众多令人惊喜的特点。首先,它涵盖了国内 40 多条精心策划的旅游线路,无论您是向往历史文化名城的厚重底蕴&…...

K均值聚类
根据到给点样本的距离,来聚类。 1.曼哈顿距离、 2.欧几里得距离 直线距离 3.切比雪夫距离 4.闵氏距离 5.余弦相似度 对数据大小/长度等不关注,只关注相似度。 6.汉明距离 二进制距离 二、密度聚类 DBSCAN 前提是样本是根据紧密程度分布的。 先用超参…...

【Ubuntu】安装常用软件包
安装java 直接输入java,如果没有安装的话会提醒你输入命令安装,类似 Command java not found, but can be installed with: sudo apt install jdkxxxxxxxxxxxxxx然后选一个版本安装就好,我这里选的jdk17,安装完确认一下 ubuntuVM-4-13-ubu…...

探索全光网技术 | 全光网产品解决方案整理-(宇洪科技)
探索全光网技术 |全光网产品解决方案整理-宇洪科技 目录 一、数据中心场景1、方案概述2、方案需求3、相关产品4、产品推荐5、方案价值 二、教育场景1、方案概述2、方案需求3、相关产品4、方案价值 三、医疗场景1、方案概述2、方案需求3、相关产品4、方案价值 注:本文…...

资料分析(2)
C B 增长量不变就是1002020 上面是利滚利:按照20%当利息 本题:涨跌幅度的意思就是增长率,本题是按照增长率不变的情况下进行计算D B 7551400X>1.2*100000 B B B 总体增量部分增量之和 先进行计算固定通信业务收入的增长量移动通信业务实现收入的增长量 增长量现期…...

百元以下蓝牙耳机性价比之王品牌?四大高能性价比机型推荐
面对市场上琳琅满目的蓝牙耳机品牌和型号,消费者往往难以抉择,特别是当预算限定在百元以下时,找到一款既满足基本功能又具备一定品质的蓝牙耳机变得尤其困难,那么百元以下蓝牙耳机性价比之王品牌?尽管价格是一个重要的…...

考场考生行为检测数据集 7000张 带标注 voc yolo
数据集名称: 考场考生行为检测数据集 数据集规模: 图像数量:7000张标注类型:行为检测(例如:作弊、玩手机、睡觉等)格式兼容性:支持VOC和YOLO标注格式 数据集内容: 该…...

深度学习算法,该如何深入,举例说明
深度学习算法的深入学习可以从理论和实践两个方面进行。理论上,深入理解深度学习需要掌握数学基础(如线性代数、概率论、微积分)、机器学习基础和深度学习框架原理。实践上,可以通过实现和优化深度学习模型来提升技能。 理论深入…...

舵机的原理及应用
舵机是一种位置(角度)伺服的驱动器,主要由外壳、电机、减速齿轮组、位置传感器和控制电路等部分组成。一、工作原理 舵机的工作原理是控制电路接收信号源的控制信号,并将其转换为电流信号,驱动电机转动。电机通过减速齿轮组带动输出轴…...

Nacos与Eureka--微服务注册中心
Nacos与Eureka Nacos和Eureka都是微服务架构中常用的服务发现和注册中心解决方案,它们帮助微服务架构中的各个服务实例进行互相发现和通信。 Nacos 是由阿里巴巴开源的一个动态服务发现、配置管理和服务管理平台。它支持服务的注册与发现,并且提供了配…...

Android 调试桥——ADB
文章目录 前言ADB 的主要功能设备连接与管理应用安装与卸载文件传输日志查看设备重启 常用命令连接方式有线无线注意点 前言 ADB(Android Debug Bridge,安卓调试桥)是 Android SDK 提供的一种命令行工具,用于在开发者的计算机和 …...

闲鱼放弃成为淘宝复刻版了吗?上线学生专属交易交流版块“学生鱼”频道
闲鱼是阿里巴巴旗下闲置用品交易平台,目前拥有超5亿用户规模、4000万日活,在去年被阿里定位为第一批战略创新业务,更是肩负“造血”的重任。闲鱼并未明确表示放弃成为淘宝,但近期确实上线了一个针对学生群体的专属交易交流版块——…...

【学习笔记11】如何找到twitter中自己的cookie?
步骤 在浏览器中打开twitter网站,按下CtrlShiftI(i)按下面步骤以此点击 参考 如何找到自己的Facebook XS Cookie和X/Twitter Auth_Token? 一張圖秒懂...

新办建筑智能化专项乙级设计资质,郑州企业需要达到哪些要求?
新办建筑智能化专项乙级设计资质,郑州企业需要达到以下要求: 一、企业基本条件 法人资格: 申请企业必须具有独立法人资格,能够在郑州地区合法经营。注册资本: 企业的注册资本需达到规定标准,通常要求不低于…...

项目管理:项目执行过程中的控制点——基线
项目进度基线详细记录了项目各项活动的计划开始时间、计划结束时间,是项目团队在执行和监控项目进度时的重要参考标准,使得项目执行过程中的任何偏差都能被及时发现和纠正。 基线在项目执行中的作用 1、监控与对比:基线为项目管理者提供了…...

NVIDIA驱动学习
lspci | grep -i vga 输出: 2d:00.0 VGA compatible controller: NVIDIA Corporation Device 2204 (rev a1) 99:00.0 VGA compatible controller: NVIDIA Corporation Device 2230 (rev a1) import torch print(torch.version.cuda) # 应该显示 CUDA 版本 print(to…...

小小GCD、LCM拿下拿下
目录 最大公约数(GCD) 最大公约数(GCD)求解: 一、辗转相除法 二、三目运算符 三、位运算 最大公约数(GCD)模板: 最大公约数(GCD)例题: 最…...

如何集成Android平台GB28181设备接入模块?
技术优势 大牛直播SDK的Android平台GB28181设备接入模块在适用场景、音视频能力、定位与通信、数据管理、安全性与稳定性、配置与扩展性以及集成与维护等方面均表现出显著的优势。这些优势使得该模块在视频监控、巡检抢修、远程指挥等多个领域具有广泛的应用前景和重要的应用价…...

mysql——关于表的增删改查(CRUD)
目录 比较运算符和逻辑运算符图 一、增加(Create) 1、全列插入 2、指定列插入 二、查询(Retrieve) 1、全列查询 2、指定列查询 3、别名(as) 4、表达式查询 5、去重(distinct) 6、…...

docker 重启容器且修改服务映射端口
要重启 Docker 容器并修改服务的映射端口,可以按照以下步骤进行操作: 1. 停止当前运行的容器 如果你想重新配置端口,通常需要先停止当前运行的容器。你可以使用以下命令停止容器: docker stop <container_name_or_id>2. 删除现有容器 为了修改端口映射,你需要删…...

智能提取:OfficeImagesExtractor让文档图片提取更简单
“科技是国之利器,也是民之福祉。” 在数字化办公日益普及的今天,我们对文档处理的需求也在不断增长。尤其是对于Office文档中的图片、视频和音频等多媒体内容的提取,传统的方法是繁琐且效率低下的。在这样的背景下,一款能够高效、…...

【LLM论文日更】| LLM2Vec揭秘大型语言模型的文本嵌入潜能
论文:https://arxiv.org/pdf/2404.05961代码:https://github.com/McGill-NLP/llm2vec机构:McGill University, Mila ServiceNow Research ,Facebook CIFAR AI Chair领域:embedding model发表:COLM 2024 研…...

大模型微调有必要做吗?LoRa还是RAG?
我需要对大模型做微调吗? 想自定义大模型时,选择:微调还是RAG还是ICL? 需要对大模型做微调? 在人工智能的世界里,大型语言模型(LLM)已经成为了我们探索未知、解决问题的得力助手。…...

机器人外呼系统如何使用呢?
智能电话机器人作为人工智能进入电销行业的一个分类,目前已取得不错的成绩。智能电话机器人针对电销行业的痛点所作出了改善。 作为新兴的一种电销手段,很多企业对其充满好奇又望而却步。那么很多朋友都有想知道为什么现在很多人都用AI机器人拓客&#x…...

python-月份有几天
题目描述 小理现在有一份日历,但是这个日历很奇怪并不能告诉小理日期信息。小理现在有年和月,希望你能帮他计算出来这一年这个月有几天。 输入 输入共一行,两个整数,代表年和月,中间用空格隔开。 输出 一个整数&am…...

1017 Queueing at Bank
链接: 1017 Queueing at Bank - PAT (Advanced Level) Practice (pintia.cn) 题目大意: 有n个客户,k个窗口。已知每个客户的到达时间和需要的时长,如果有窗口就依次过去,如果没有窗口就在黄线外等候(黄线…...

DPDK 测试说明
文章目录 2.DPDK 测试说明2.1硬件pci加密设备绑定到igb_uio驱动IGB_UIO 主要负责什么内容 ? 2.2 test命令使用说明2.3 dpdk-test-crypto-perf命令使用说明2.4 使用testpmd测试网卡性能 2.DPDK 测试说明 2.1硬件pci加密设备绑定到igb_uio驱动 dpdk-stable/usertool…...

上传及接收pdf文件,使用pdfbox读取pdf文件内容
前端上传pdf文件 html <form class"layui-form"><div style"background-color: #ffffff" ><div style"padding: 30px"><div class"layui-form-item"><div class"layui-inline"><label c…...