当前位置: 博客首页>> 技术分享 >> 阅读正文

FastDFS教程

作者: 分类: 技术分享 发布于: 2023-04-14 18:26:52 浏览:1,269 评论(0)


FastDFS教程

lecture:wayee

一、FastDFS 介绍

简介

FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。

8b29ebbf95ce4b1b85c57f293e8bace3.png

FastDFS两个主要的角色:Tracker Server 和 Storage Server 。

Tracker:跟踪服务器,管理集群,tracker也可以实现集群。每个tracker节点地位平等。收集Storage集群的状态。

Storage:存储服务器,实际保存文件,分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

应用场景

  1. 网络文件共享:可以在多个服务器之间共享文件,实现文件共享和访问。
  2. 图片、音频、视频等大文件存储:FastDFS可以管理大文件,实现文件处理和访问,适用于多媒体资源存储。
  3. CDN(内容分发网络):FastDFS与CDN结合,可以实现多地域、多节点的内容分发,提高访问速度和稳定性。
  4. 云存储:FastDFS支持横向扩展,可以扩展存储节点,支持云存储。
  5. 日志处理:日志是通常的批量文件写入,FastDFS可以有效地处理大量的小文件写入,适合用于日志收集与处理。

上传交互过程

409e32ab21684682ba2e87a56562e031.png

  1. Client通过Tracker server查找可用的Storage server。

  2. Tracker server向Client返回一台可用的Storage server的IP地址和端口号。

  3. Client直接通过Tracker server返回的IP地址和端口与其中一台Storage server建立连接并进行文件上传。

  4. 上传完成,Storage server返回Client一个文件ID(卷、目录),文件上传结束。

下载交互过程

36df070059cb47e59aa0a4c335ebb8e0.png

  1. client询问tracker下载文件的storage,参数为文件标识(卷名和文件名);
  2. tracker返回一台可用的storage;
  3. client直接和storage通讯完成文件下载。 需要说明的是,client为使用FastDFS服务的调用方,client也应该是一台服务器,它对tracker和storage的调用均为服务器间的调用。

二、安装

环境准备

使用的系统软件

名称 说明
centos 7.x
libfastcommon FastDFS分离出的公用函数库
libserverframe FastDFS分离出的网络框架
FastDFS FastDFS本体
fastdfs-nginx-module FastDFS和nginx的关联模块
nginx nginx1.15.4

可以采用yum安装和源码编译两种方式之一。

yum安装

针对CentOS 7 和 CentOS 8及同类Linux发行版。 先安装FastOS.repo yum源,然后就可以安装FastDFS相关软件包了。

CentOS 7、RHEL 7、Oracle Linux 7、Alibaba Cloud Linux 2、Anolis 7、AlmaLinux 7、Amazon Linux 2、Fedora 27及以下版本:

rpm -ivh http://www.fastken.com/yumrepo/el7/noarch/FastOSrepo-1.0.0-1.el7.centos.noarch.rpm

安装 FastDFS软件包:

yum install fastdfs-server fastdfs-tool fastdfs-config -y

编译环境

CentOS

yum install git gcc gcc-c++ make automake autoconf libtool pcre pcre-devel zlib zlib-devel openssl-devel wget vim -y

Debian

apt-get -y install git gcc g++ make automake autoconf libtool pcre2-utils libpcre2-dev zlib1g zlib1g-dev openssl libssh-dev wget vim

磁盘目录

说明 位置
所有安装包 /usr/local/src
数据存储位置 /home/dfs/
#这里我为了方便把日志什么的都放到了dfs
mkdir /home/dfs #创建数据存储目录
cd /usr/local/src #切换到安装目录准备下载安装包

安装libfastcommon

git clone https://github.com/happyfish100/libfastcommon.git --depth 1
cd libfastcommon/
./make.sh && ./make.sh install #编译安装
cd ../ #返回上一级目录

安装libserverframe

git clone https://github.com/happyfish100/libserverframe.git --depth 1
cd libserverframe/
./make.sh && ./make.sh install #编译安装
cd ../ #返回上一级目录

安装FastDFS

git clone https://github.com/happyfish100/fastdfs.git --depth 1
cd fastdfs/
./make.sh && ./make.sh install #编译安装
#配置文件准备
cp /usr/local/src/fastdfs/conf/http.conf /etc/fdfs/ #供nginx访问使用
cp /usr/local/src/fastdfs/conf/mime.types /etc/fdfs/ #供nginx访问使用
cd ../ #返回上一级目录

安装nginx

wget http://nginx.org/download/nginx-1.15.4.tar.gz #下载nginx压缩包
tar -zxvf nginx-1.15.4.tar.gz #解压
cd nginx-1.15.4/
#添加fastdfs-nginx-module模块
./configure --add-module=/usr/local/src/fastdfs-nginx-module/src/ 
make && make install #编译安装

单机部署

tracker配置

#服务器ip为 192.168.2.100
#我建议用ftp下载下来这些文件 本地修改
vim /etc/fdfs/tracker.conf
#需要修改的内容如下
port=22122  # tracker服务器端口(默认22122,一般不修改)
base_path=/home/dfs  # 存储日志和数据的根目录

storage配置

vim /etc/fdfs/storage.conf
#需要修改的内容如下
port=23000  # storage服务端口(默认23000,一般不修改)
base_path=/home/dfs  # 数据和日志文件存储根目录
store_path0=/home/dfs  # 第一个存储目录
tracker_server=192.168.2.100:22122  # tracker服务器IP和端口
http.server_port=8888  # http访问文件的端口(默认8888,看情况修改,和nginx中保持一致)

client配置

vim /etc/fdfs/client.conf
#需要修改的内容如下
base_path=/home/dfs
tracker_server=192.168.2.100:22122    #tracker服务器IP和端口

启动tracker

#修改 /usr/lib/systemd/system/fdfs_trackerd.service 中的 PIDFile,格式为:
PIDFile=$base_path/data/fdfs_trackerd.pid
#比如:
PIDFile=/home/dfs/data/fdfs_trackerd.pid
systemctl start fdfs_trackerd #启动tracker服务
systemctl restart fdfs_trackerd #重启动tracker服务
systemctl stop fdfs_trackerd #停止tracker服务
systemctl enable fdfs_trackerd  #开机自启动

启动storage

#修改 /usr/lib/systemd/system/fdfs_storaged.service 中的 PIDFile,格式为:
PIDFile=$base_path/data/fdfs_storaged.pid
#比如:
PIDFile=/home/dfs/data/fdfs_storaged.pid
systemctl start fdfs_storaged #启动storage服务
systemctl restart fdfs_storaged  #重动storage服务
systemctl stop fdfs_storaged  #停止动storage服务
systemctl enable fdfs_storaged   #开机自启动

client测试

#返回ID表示成功 如:group1/M00/00/00/xx.tar.gz
fdfs_upload_file /etc/fdfs/client.conf /usr/local/src/nginx-1.15.4.tar.gz

配置mod_fastdfs

vim /etc/fdfs/mod_fastdfs.conf
#需要修改的内容如下
tracker_server=192.168.2.100:22122  #tracker服务器IP和端口
url_have_group_name=true
store_path0=/home/dfs

配置nginx

#配置nginx.config
vim /usr/local/nginx/conf/nginx.conf
#添加如下配置
server {
    listen       8888;    ## 该端口为storage.conf中的http.server_port相同
    server_name  localhost;
    location ~/group[0-9]/ {
        ngx_fastdfs_module;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
    root   html;
    }
}

启动nginx

/usr/local/nginx/sbin/nginx #启动nginx
/usr/local/nginx/sbin/nginx -s reload #重启nginx
/usr/local/nginx/sbin/nginx -s stop #停止nginx

关闭防火墙

#不关闭防火墙的话无法使用
systemctl stop firewalld.service #关闭
systemctl restart firewalld.service #重启

测试下载

#测试下载,用外部浏览器访问刚才已传过的nginx安装包,引用返回的ID
http://192.168.2.100:8888/group1/M00/00/00/wKgAQ1pysxmAaqhAAA76tz-dVgg.tar.gz
#弹出下载单机部署全部跑通

三、Springboot整合

引入包

<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
    <version>1.27.2</version>
</dependency>

普通上传与删除

import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.multipart.MultipartFile;

/**
 * 文件上传
 *
 * @param multipartFile
 * @return
 * @throws IOException
 */
public String uploadCommonFile(MultipartFile multipartFile) throws IOException {
    StorePath storePath = fastFileStorageClient.uploadFile(multipartFile.getInputStream(), multipartFile.getSize(), this.getFileType(multipartFile), new HashSet<>());
    return storePath.getPath();
}

/**
 * 文件删除
 * @param filePath
 */
public void deleteFile(String filePath) {
    fastFileStorageClient.deleteFile(filePath);
}


/**
 * 获取文件类型
 *
 * @param multipartFile
 * @return
 */
public String getFileType(MultipartFile multipartFile) {
    String filename = multipartFile.getOriginalFilename();
    int index = filename.lastIndexOf(".");

    return filename.substring(index + 1);
}

上传带缩略图的图片

配置缩略图大小

fdfs:
  thumb-image: # 缩略图
    width: 60
    height: 60

上传方法

/**
 * 图片上传(生成缩略图)
 * @param multipartFile
 * @return
 * @throws IOException
 */
public String uploadImageAndCrtThumbImage(MultipartFile multipartFile) throws IOException {
    StorePath storePath = fastFileStorageClient.uploadImageAndCrtThumbImage(multipartFile.getInputStream(), multipartFile.getSize(), this.getFileType(multipartFile), new HashSet<>());
    return storePath.getFullPath();
}

访问图片

#原图地址
/group1/M00/00/00/wKgCZGQQkCWAcbtjAIzhKt0hRzs539.png
#缩略图地址
/group1/M00/00/00/wKgCZGQQkCWAcbtjAIzhKt0hRzs539_60x60.png

断点续传

文件断点续传是一种实现文件上传和下载的技术。当文件在传输过程中,网络连接失败或延时时,可以暂停传输并保留已发送的数据,在网络重新连接后,可以从断点位置继续传输而不用重新开始传输,从而提高了文件的传输效率。

文件分片上传逻辑

//分组
private static final String GROUP_NAME = "GROUP1";
//上传分片地址key
private static final String PATH_KEY = "path-key:";
//已上传分片大小key
private static final String UPLOADED_SIZE_KEY = "uploaded-size-key:";
//已上传分片数key
private static final String UPLOADED_NO_KEY = "uploaded-no-key:";

*
*
*

/**
 * 断点续传的文件第一分片上传
 *
 * @param multipartFile
 * @return
 * @throws Exception
 */
public String uploadAppenderFile(MultipartFile multipartFile) throws Exception {
    String fileType = this.getFileType(multipartFile);
    //第一次上传使用uploadAppenderFile方法
    StorePath storePath = appendFileStorageClient.uploadAppenderFile(GROUP_NAME, multipartFile.getInputStream(), multipartFile.getSize(), fileType);
    return storePath.getPath();
}

/**
 * 文件续传方法
 *
 * @param multipartFile
 * @param filePath
 * @param offset
 * @throws Exception
 */
public void modifierAppenderFile(MultipartFile multipartFile, String filePath, long offset) throws Exception {
    //使用modifyFile方法防止同一分片文件重复上传导致文件内容重复问题
    appendFileStorageClient.modifyFile(GROUP_NAME, filePath, multipartFile.getInputStream(), multipartFile.getSize(), offset);
}


/**
 * 上传分片文件逻辑
 *
 * @param multipartFile
 * @param fileMd5
 * @param sliceNo
 * @param totalSliceNo
 * @return
 * @throws Exception
 */
public String uploadFileBySlices(MultipartFile multipartFile, String fileMd5, Integer sliceNo, Integer totalSliceNo) throws Exception {
    String pathKey = PATH_KEY + fileMd5;
    String uploadedSizeKey = UPLOADED_SIZE_KEY + fileMd5;
    String uploadedNoKey = UPLOADED_NO_KEY + fileMd5;

    String uploadedSizeStr = stringRedisTemplate.opsForValue().get(uploadedSizeKey);

    Long uploadedSize = 0L;
    if (!StringUtil.isNullOrEmpty(uploadedSizeStr)) {
        uploadedSize = Long.valueOf(uploadedSizeStr);
    }

    if (sliceNo == 1) {
        //上传第一个分片
        String filePath = this.uploadAppenderFile(multipartFile);
        if (StringUtil.isNullOrEmpty(filePath)) {
            throw new Exception("上传失败");
        }
        //存储第一个分片文件路径
        stringRedisTemplate.opsForValue().set(pathKey, filePath);
        //存储已上传分片序号
        stringRedisTemplate.opsForValue().set(uploadedNoKey, "1");
    } else {
        //非第一个分片
        String filePath = stringRedisTemplate.opsForValue().get(pathKey);
        if (StringUtil.isNullOrEmpty(filePath)) {
            throw new Exception("上传失败");
        }
        this.modifierAppenderFile(multipartFile, filePath, uploadedSize);
        stringRedisTemplate.opsForValue().increment(uploadedNoKey);
    }
    //更新已上传分片文件大小
    uploadedSize += multipartFile.getSize();
    stringRedisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));
    //如果所有分片全部上传完毕,清空redis里面相关的key和value
    String uploadedNoStr = stringRedisTemplate.opsForValue().get(uploadedNoKey);
    Integer uploadedNo = Integer.valueOf(uploadedNoStr);

    String resultPath = "";
    if (uploadedNo.equals(totalSliceNo)) {
        resultPath = stringRedisTemplate.opsForValue().get(pathKey);
        List<String> keyList = Arrays.asList(pathKey, uploadedSizeKey, uploadedNoKey);
        stringRedisTemplate.delete(keyList);
    }

    return resultPath;
}

java实现文件分片操作

//分片大小
private static final int SLICE_SIZE = 1024 * 1024 * 2;

/**
 * 文件分片
 * @param multipartFile
 * @throws Exception
 */
public void convertFileToSlices(MultipartFile multipartFile) throws Exception {
    String fileType = this.getFileType(multipartFile);
    File file = this.multipartFileToFile(multipartFile);
    long fileLength = file.length();
    int count = 1;
    for (int i = 0; i < fileLength; i += SLICE_SIZE) {
        RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
        randomAccessFile.seek(i);
        byte[] bytes = new byte[SLICE_SIZE];
        int len = randomAccessFile.read(bytes);
        String path = "/Users/wayee/tmpfile/" + count + "." + fileType;

        File sliceFile = new File(path);

        //使用文件输出流来写文件
        FileOutputStream fileOutputStream = new FileOutputStream(sliceFile);
        fileOutputStream.write(bytes, 0, len);
        fileOutputStream.close();
        randomAccessFile.close();
        count++;
    }
    //删除临时文件
    file.delete();
}

public File multipartFileToFile(MultipartFile multipartFile) throws Exception {
    String originalFilename = multipartFile.getOriginalFilename();
    String[] split = originalFilename.split("\\.");
    File file = File.createTempFile(split[0], split[1]);
    multipartFile.transferTo(file);
    return file;
}

秒传

文件秒传的核心原理是基于文件的特征值(如CRC、MD5或SHA1等)来快速检测文件是否已存在。这些特征值可以用来识别文件是否相同,从而可以实现文件秒传功能,使用户可以快速上传和下载文件。

数据表

CREATE TABLE `file` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '链接',
  `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '类型',
  `md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件md5值',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';

修改文件上传逻辑

// FileServiceImpl.java

@Resource
private FastDFSUtil fastDFSUtil;


/**
 * 分片文件上传
 * @param file
 * @param fileMd5
 * @param sliceNo
 * @param totalSliceNo
 * @return
 * @throws Exception
 */
@Override
public String uploadFileBySlices(MultipartFile file, String fileMd5, Integer sliceNo, Integer totalSliceNo) throws Exception {
    //查询文件是否已上传过
    File dbFile = this.getFileByMD5(fileMd5);
    if (dbFile != null) {
        //上传过,直接返回链接
        return dbFile.getUrl();
    }
    //未上传过,调用fastDFS上传方法, 返回有url,说明分片文件已上传完成, 则入库
    String url = fastDFSUtil.uploadFileBySlices(file, fileMd5, sliceNo, totalSliceNo);
    if (!StringUtil.isNullOrEmpty(url)) {
        dbFile = new File();
        dbFile.setType(fastDFSUtil.getFileType(file))
                .setUrl(url)
                .setMd5(fileMd5);

        save(dbFile);
    }

    return url;
}


/**
 * 通过文件md5值查询文件信息
 * @param fileMD5
 * @return
 */
@Override
public File getFileByMD5(String fileMD5) {
    QueryWrapper<File> wrapper = new QueryWrapper<>();
    wrapper.eq("md5", fileMD5).last("limit 1");

    return getOne(wrapper);
}

控制器调用修改

 @Autowired
private FileService fileService;

@PutMapping("/file-slices")
public String uploadFileBySlices(MultipartFile file,
                                 String fileMd5,
                                 Integer sliceNo,
                                 Integer totalSliceNo) throws Exception {
    return fileService.uploadFileBySlices(file, fileMd5, sliceNo, totalSliceNo);
}

文件下载

下载代码

/**
 * 文件下载
 *
 * @param filePath
 * @return
 */
public byte[] downloadFile(String filePath){
    StorePath storePath = StorePath.parseFromUrl(filePath);

    DownloadByteArray callback = new DownloadByteArray();
    return fastFileStorageClient.downloadFile(storePath.getGroup(), storePath.getPath(), callback);
}

控制器调用

@GetMapping("/download")
public void downloadFile(String filePath, HttpServletResponse response) throws UnsupportedEncodingException {
    byte[] bytes = fileService.download(filePath);
    response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(FileNameUtil.getName(filePath), "UTF-8"));
    response.setCharacterEncoding("UTF-8");
    ServletOutputStream outputStream = null;
    try {
        outputStream = response.getOutputStream();
        outputStream.write(bytes);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

浏览器访问http://localhost:6789/file/download?filePath=group1/M00/00/00/wKgCZGQQX_WEc9TDAAAAAGlBnQY995.pdf进行文件下载,filePath为需要下载的文件路径

       

转载时请注明出处及相应链接。

本文永久链接: https://blog.baigei.com/articles/fastdfs