概述
使用了 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零拷贝接口
NAME
mmap, munmap - map or unmap files or devices into memory
SYNOPSIS
#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
NAME
sendfile - transfer data between file descriptors
SYNOPSIS
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
splice零拷贝接口
splice
传输必须要借助管道
将数据写入管道,再从管道传给发送缓冲区(是不经过用户态空间的)
管道最多可以存储65535个字节,如果文件较大则需要循环传输
splice
没有字节数限制,比上面两个好用一点,可以控制发送的快慢(第5个参数决定每次传输的字节数量)
NAME
splice - splice data to/from a pipe
SYNOPSIS
#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; //客户端fd
struct 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和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;
}
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添加进epfd
epollAddFd(sfd, epfd);
//将用来退出的管道加入epfd
epollAddFd(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和port
FILE *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_WAITALL
ret = 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
和线程池的小项目