本项目使用c++实现一个文件服务器,核心功能是上传与下载。所以该项目可以作为网盘,也可以作为图床来用。
本文的核心重点是介绍FastDFS的概念、构成、配置文件、启动与上传下载的实现。后续将逐步介绍nginx,mysql,redis,fastcgi等内容。
本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。

Nginx
fastCGI
FastDFS - 分布式文件系统
传统的文件系统可以被挂载和卸载,格式:ntfs / fat32 / ext3 / ext4
一台主机的磁盘插槽不可能是无限的,当磁盘都满了之后想要扩充,而磁盘插槽都被插满了,那么这个时候就需要使用分布式文件系统了。
完整的文件系统, 不在同一台主机上,而是在很多台主机上,多个分散的文件系统组合在一起,形成了一个完整的文件系统。

分布式文件系统:
上传

下载

追踪器 (tracker)
存储节点(storage )
客户端

为什么需要追踪器集群?
多个tracker如何工作?
如何实现集群
存储节点集群
fastDFS如何管理存储节点?
集群方式(两种扩容方式)
如何实现集群
横向扩容 - 增加容量:
假设当前有两个组: group1, group2
需要添加一个新的分组 -> group3
新主机属于group3
添加一台新的主机 -> 容量增加了
不同组的主机之间不需要通信
纵向扩容 - 数据备份:
假设当前有两个组: group1, group2
将新的主机放到现有的组中
每个组的主机数量从1 -> N
- 这n台主机的关系就是相互备份的关系
- 同一个组中的主机需要通信
- 每个组的容量 等于 组内容量最小的主机容量(比如Group1的容量是500G)
安装流程:
git clone https://github.com/happyfish100/libfastcommon.git
cd libfastcommon
./make.sh
./make.sh install
git clone https://gitee.com/fastdfs100/fastdfs.git
cd fastdfs
./make.sh
./make.sh install
来看看make install都做了哪些事情


测试
fdfs_test
ls /usr/bin/fdfs_*
root@wxf:/# fdfs_test
This is FastDFS client test program v6.08
Copyright (C) 2008, Happy Fish / YuQing
FastDFS may be copied only under the terms of the GNU General
Public License V3, which may be found in the FastDFS source kit.
Please visit the FastDFS Home Page http://www.fastken.com/
for more detail.
Usage: fdfs_test <config_file> <operation>
operation: upload, download, getmeta, setmeta, delete and query_servers
#fastDFS安装的所有的可执行程序:
root@wxf:/# ls /usr/bin/fdfs_*
/usr/bin/fdfs_appender_test /usr/bin/fdfs_download_file /usr/bin/fdfs_test
/usr/bin/fdfs_appender_test1 /usr/bin/fdfs_file_info /usr/bin/fdfs_test1
/usr/bin/fdfs_append_file /usr/bin/fdfs_monitor /usr/bin/fdfs_trackerd
/usr/bin/fdfs_crc32 /usr/bin/fdfs_regenerate_filename /usr/bin/fdfs_upload_appender
/usr/bin/fdfs_delete_file /usr/bin/fdfs_storaged /usr/bin/fdfs_upload_file
为什么在别的目录输入fdfs_test 也能执行?因为make install的时候,已经将程序拷贝到/usr/bin目录下了。
root@wxf:/temp/fastdfs# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
在上面已经分析了fdfs的配置文件放在/etc/fdfs/目录下。
root@wxf:/etc/fdfs# ls
client.conf storage.conf storage_ids.conf tracker.conf
不同的tracker主机需要各自修改自己的配置文件
# 下列是需要修改的地方
# 将追踪器和部署的主机的IP地址进程绑定, 也可以不指定
# 如果不指定, 会自动绑定当前主机IP, 如果是云服务器建议不要写
# 这里的ip和port是提供给storage,client建立连接用的
bind_addr=192.168.109.100
# 追踪器监听的端口
port=22122
# 追踪器存储日志信息的目录, xxx.pid文件, 必须是一个存在的目录
base_path=/fastdfs/tracker
这里因为fastdfs由client,storage,tracker构成,所以可以创建一个fastdfs目录,里面再分三个子目录,由一个大目录fastdfs进行统一管理。

不同的storage主机需要各自修改自己的配置文件
# 下列是需要修改的地方
# 当前存储节点对应的主机属于哪一个组
group_name=group1
# 当前存储节点和所应该的主机进行IP地址的绑定, 如果不写, 有fastdfs自动绑定
# 这里的ip和port是给client建立连接用的
bind_addr=
# 存储节点绑定的端口
port=23000
# 存储节点写log日志的路径
base_path=/fastdfs/storage
# 存储节点提供的存储文件的路径个数
store_path_count=2
# 具体的存储路径 【M00 M01映射的路径】
store_path0=/fastdfs/storage0
store_path1=/fastdfs/storage1
# 追踪器的地址信息
tracker_server=192.168.109.100:22122
tracker_server=192.168.109.101:22122
上面配置两个存储目录和两个追踪器地址只是演示怎么配置而已,后文用的配置如下
# 存储节点写log日志的路径
base_path=/fastdfs/storage
# 存储节点提供的存储文件的路径个数
store_path_count=1
# 具体的存储路径 【M00 M01映射的路径】
store_path0=/fastdfs/storage
# 追踪器的地址信息
tracker_server=192.168.109.100:22122
# 下列是需要修改的地方
# 客户端写log日志的目录
# 该路径必须存在
# 当前的用户对于该路径中的文件有读写权限
base_path=/fastdfs/client
# 要连接的追踪器的地址信息
tracker_server=192.168.109.100:22122
tracker_server=192.168.109.101:22122
假设现在有6台主机,2台client,2台tracker,2台storage。
第一步:6台主机全部安装FastDFS
第二步:修改各自对应的配置文件

所有的启动程序都在/usr/bin/目录下
root@wxf:/# ls /usr/bin/fdfs_*
/usr/bin/fdfs_appender_test /usr/bin/fdfs_download_file /usr/bin/fdfs_test
/usr/bin/fdfs_appender_test1 /usr/bin/fdfs_file_info /usr/bin/fdfs_test1
/usr/bin/fdfs_append_file /usr/bin/fdfs_monitor /usr/bin/fdfs_trackerd
/usr/bin/fdfs_crc32 /usr/bin/fdfs_regenerate_filename /usr/bin/fdfs_upload_appender
/usr/bin/fdfs_delete_file /usr/bin/fdfs_storaged /usr/bin/fdfs_upload_file
其实下面命令中不指定/usr/bin也可以,我这里为了演示清楚目录,全部补全了。
# 使用的方式如下
# <程序> <配置文件> <行为>
# 启动
#fdfs_trackerd 追踪器的配置文件(/etc/fdfs/tracker.conf)
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf start
# 关闭
#fdfs_trackerd 追踪器的配置文件(/etc/fdfs/tracker.conf) stop
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf stop
# 重启
#fdfs_trackerd 追踪器的配置文件(/etc/fdfs/tracker.conf) restart
/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
# 使用的方式如下
# <程序> <配置文件> <行为>
# 启动
#fdfs_storaged 存储节点的配置文件(/etc/fdfs/stroga.conf)
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf start
# 关闭
#fdfs_storaged 存储节点的配置文件(/etc/fdfs/stroga.conf) stop
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf stop
# 重启
#fdfs_storaged 存储节点的配置文件(/etc/fdfs/stroga.conf) restart
/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
# 使用的方式如下
# <程序> <配置文件> <行为>
# 上传
#fdfs_upload_file 客户端的配置文件(/etc/fdfs/client.conf) 要上传的文件(/etc/fdfs/client.conf)
# 得到的结果字符串: group1/M00/00/00/wKhtZWMVtDGAR4d0AAAHTaCnXjs12.conf
root@wxf:/# /usr/bin/fdfs_upload_file /etc/fdfs/client.conf /etc/fdfs/client.conf
group1/M00/00/00/wKhtZWMVtDGAR4d0AAAHTaCnXjs12.conf
# 下载
#fdfs_download_file 客户端的配置文件(/etc/fdfs/client.conf) 上传成功之后得到的字符串(fileID)
root@wxf:/# /usr/bin/fdfs_download_file /etc/fdfs/client.conf group1/M00/00/00/wKhtZWMVtDGAR4d0AAAHTaCnXjs12.conf
root@wxf:/# ls
wKhtZWMVtDGAR4d0AAAHTaCnXjs12.conf
# 发现原来的文件名被修改了,所以需要把原来的文件名和保证之后的文件名做一个映射保存在数据库中

#查看端口是否被占用:
netstat -apn | grep 80
#查看占用某个端口的进程:
sudo lsof -i:80
#关键字搜索某个进程
ps -aux | grep fdfs
lsof -i:22122
lsof -i:23000
ps aux|grep fdfs_

fdfs_monitor /etc/fdfs/client.conf
可以看到fdfs_monitor 后面跟的是client的配置信息,所以可以把它当作一个客户端,启动时先连接追踪器,向追踪器询问storage的信息

STORAGE SERVER的状态通常有七种,正常状态必须是ACTIVE:
# FDFS_STORAGE_STATUS:INIT :初始化,尚未得到同步已有数据的源服务器
# FDFS_STORAGE_STATUS:WAIT_SYNC :等待同步,已得到同步已有数据的源服务器
# FDFS_STORAGE_STATUS:SYNCING :同步中
# FDFS_STORAGE_STATUS:DELETED :已删除,该服务器从本组中摘除
# FDFS_STORAGE_STATUS:OFFLINE :离线
# FDFS_STORAGE_STATUS:ONLINE :在线,尚不能提供服务
# FDFS_STORAGE_STATUS:ACTIVE :在线,可以提供服务
# 从集群中删除
fdfs_monitor /etc/fdfs/client.conf delete group1 192.168.109.100
# 删除数据文件夹
rm -rf /fastdfs/storage/data
# 重启节点
fdfs_storaged /etc/fdfs/storage.conf restart

M00 - 虚拟目录
00/00
wKhS_VlrEfOAdIZyAAAJTOwCGr43848
| 4bytes | 4bytes | 8bytes |4bytes | 2bytes |
| ip | timestamp | file_size |crc32 | 校验值 |
我们上面已经用过fdfs_upload_file了,这是fdfs作者提供好的可执行程序,所以用多进程的方式是一种比较投机的方式。将fdfs_upload_file的输出重定位到管道pipe,由父进程读取。

exec函数协议族函数(exec族函数用一个新的进程映像替换当前进程映像):execl、execlp。
子进程 -> 执行execlp("fdfs_upload_file" , "xx", arg, NULL)。此时会有结果输出到标准输出终端里面,我们想让它输出到终端,使用dup2(old标准输出, new管道的写端)。将file_id写入pipe中,最终由父进程进行读取。pipe在子进程创建之前创建即可。父进程记得还要wait(),释放子进程的资源。
操作步骤
创建管道 - pipe
创建子进程
子进程干什么?
父进程?
作者并没有提供api与文档出来,但是我们可以通过fdfs_upload_file.c,看看源码是怎么做的。

看我写的//TODO 注释即可。那么我们的思路就很简单,把这个main函数改成一个接口即可。
/**
* Copyright (C) 2008 Happy Fish / YuQing
*
* FastDFS may be copied only under the terms of the GNU General
* Public License V3, which may be found in the FastDFS source kit.
* Please visit the FastDFS Home Page http://www.fastken.com/ for more detail.
**/
#include
#include
#include
#include
#include
#include
#include "fdfs_client.h"
#include "fastcommon/logger.h"
static void usage(char *argv[]) {
printf("Usage: %s " \
"[storage_ip:port] [store_path_index]\n", argv[0]);
}
int main(int argc, char *argv[]) {
char *conf_filename;
char *local_filename;
char group_name[FDFS_GROUP_NAME_MAX_LEN + 1];
ConnectionInfo *pTrackerServer;
int result;
int store_path_index;
ConnectionInfo storageServer;
char file_id[128];
if (argc < 3) {
usage(argv);
return 1;
}
log_init();
g_log_context.log_level = LOG_ERR;
ignore_signal_pipe();
conf_filename = argv[1];
//TODO fdfs_client_init解析传入的配置文件
if ((result = fdfs_client_init(conf_filename)) != 0) {
return result;
}
//TODO tracker_get_connection在上面client配置文件里面
//TODO 有追踪器的ip:port,所以这里就是连接追踪器,初始化pTrackerServer指针
pTrackerServer = tracker_get_connection();
if (pTrackerServer == NULL) {
fdfs_client_destroy();
return errno != 0 ? errno : ECONNREFUSED;
}
local_filename = argv[2];
*group_name = '\0';
//我们都是传3个参数,这里>=4不用看了
if (argc >= 4) {
const char *pPort;
const char *pIpAndPort;
pIpAndPort = argv[3];
pPort = strchr(pIpAndPort, ':');
if (pPort == NULL) {
fdfs_client_destroy();
fprintf(stderr, "invalid storage ip address and " \
"port: %s\n", pIpAndPort);
usage(argv);
return 1;
}
storageServer.sock = -1;
snprintf(storageServer.ip_addr, sizeof(storageServer.ip_addr), \
"%.*s", (int) (pPort - pIpAndPort), pIpAndPort);
storageServer.port = atoi(pPort + 1);
if (argc >= 5) {
store_path_index = atoi(argv[4]);
}
else {
store_path_index = -1;
}
}
//TODO tracker_query_storage_store追踪器 查询 存储节点 的 存储
//TODO storageServer是传入参数,对storageServer赋值
//TODO group_name是一个char数组,对group_name赋值
//TODO store_path_index是传入参数,对store_path_index赋值
else if ((result = tracker_query_storage_store(pTrackerServer, \
&storageServer, group_name, &store_path_index)) != 0) {
fdfs_client_destroy();
fprintf(stderr, "tracker_query_storage fail, " \
"error no: %d, error info: %s\n", \
result, STRERROR(result));
return result;
}
//TODO storage_upload_by_filename1查到了存储节点,就进行上传的动作
//TODO file_id传出参数,如果上传成功了,就打印file_id
result = storage_upload_by_filename1(pTrackerServer, \
&storageServer, store_path_index, \
local_filename, NULL, \
NULL, 0, group_name, file_id);
if (result == 0) {
printf("%s\n", file_id);
}
else {
fprintf(stderr, "upload file fail, " \
"error no: %d, error info: %s\n", \
result, STRERROR(result));
}
//TODO tracker_close_connection_ex与追踪器断开连接
tracker_close_connection_ex(pTrackerServer, true);
fdfs_client_destroy();
return result;
}
#include
#include
#include
#include
#include
#include
#include "fdfs_client.h"
#include
//使用多进程方式
int upload_file1(const char *confFile, const char *uploadFile, char *fileID, int size) {
//1. 创建管道
int fd[2];
int ret = pipe(fd);
if (ret == -1) {
perror("pipe error");
exit(0);
}
//2. 创建子进程
pid_t pid = fork();
//子进程
if (pid == 0) {
//3. 标准输出重定向 -> pipe写端
dup2(fd[1], STDOUT_FILENO);
//4. 关闭读端
close(fd[0]);
execlp("fdfs_upload_file", "fdfs_upload_file", confFile, uploadFile, NULL);
perror("execlp error");
}
else {
//父进程读管道
close(fd[1]);
read(fd[0], fileID, size);
//回收子进程的PCB
wait(NULL);
}
}
//使用fastDFS API实现
int upload_file2(const char *confFile, const char *myFile, char *fileID) {
char group_name[FDFS_GROUP_NAME_MAX_LEN + 1];
ConnectionInfo *pTrackerServer;
int result;
int store_path_index;
ConnectionInfo storageServer;
//TODO fdfs_client_init解析传入的配置文件
if ((result = fdfs_client_init(confFile)) != 0) {
return result;
}
//TODO tracker_get_connection在上面client配置文件里面
//TODO 有追踪器的ip:port,所以这里就是连接追踪器,初始化pTrackerServer指针
pTrackerServer = tracker_get_connection();
if (pTrackerServer == NULL) {
fdfs_client_destroy();
return errno != 0 ? errno : ECONNREFUSED;
}
*group_name = '\0';
//TODO tracker_query_storage_store追踪器 查询 存储节点 的 存储
//TODO storageServer是传入参数,对storageServer赋值
//TODO group_name是一个char数组,对group_name赋值
//TODO store_path_index是传入参数,对store_path_index赋值
if ((result = tracker_query_storage_store(pTrackerServer, \
&storageServer, group_name, &store_path_index)) != 0) {
fdfs_client_destroy();
fprintf(stderr, "tracker_query_storage fail, " \
"error no: %d, error info: %s\n", \
result, STRERROR(result));
return result;
}
//TODO storage_upload_by_filename1查到了存储节点,就进行上传的动作
//TODO file_id传出参数,如果上传成功了,就打印file_id
result = storage_upload_by_filename1(pTrackerServer, \
&storageServer, store_path_index, \
myFile, NULL, \
NULL, 0, group_name, fileID);
if (result == 0) {
printf("%s\n", fileID);
}
else {
fprintf(stderr, "upload file fail, " \
"error no: %d, error info: %s\n", \
result, STRERROR(result));
}
//TODO tracker_close_connection_ex与追踪器断开连接
tracker_close_connection_ex(pTrackerServer, true);
fdfs_client_destroy();
return result;
}
//
// Created by 68725 on 2022/9/5.
//
#ifndef FDFS_UPLOAD_FILE_H
#define FDFS_UPLOAD_FILE_H
extern int upload_file1(const char *confFile, const char *uploadFile, char *fileID, int size);
extern int upload_file2(const char *confFile, const char *myFile, char *fileID);
#endif //FDFS_UPLOAD_FILE_H
#include
#include
#include
#include
#include
#include
#include "fdfs_upload_file.h"
int main() {
char fildID[1024] = {0};
upload_file1("/etc/fdfs/client.conf", "main.c", fildID, sizeof(fildID));
printf("Multiprocess upload_file1 fildID:%s\n", fildID);
printf("=================================\n");
upload_file2("/etc/fdfs/client.conf", "main.c", fildID);
printf("Call API upload_file2 fildID:%s\n", fildID);
}
root@wxf:/test# ls
fdfs_upload_file.c fdfs_upload_file.h main.c
root@wxf:/test# gcc -o test *.c -I/usr/include/fastdfs/ -lfdfsclient
root@wxf:/test# ls
fdfs_upload_file.c fdfs_upload_file.h main.c test
root@wxf:/test# ./test
Multiprocess upload_file1 fildID:group1/M00/00/00/wKhtZWMV4USAeXL4AAACCAMe_TM89174.c
=================================
group1/M00/00/00/wKhtZWMV4UWAeb7BAAACCAMe_TM19985.c
Call API upload_file2 fildID:group1/M00/00/00/wKhtZWMV4UWAeb7BAAACCAMe_TM19985.c

