1、准备

在日常的项目管理中提供主要逻辑服务的服务器上总会保存着许多其他的静态资源,例如文件图片等占用大量的磁盘空间,因此可以将这些和业务处理不相关的资源进行拆开存放,因此需要准备一台服务器专门用作文件的管理,包括增删改查等维护性操作,这里采用分布式的文件管理 FastDFS 来完成文件系统的搭建,使用起来相对也比较简单。

1.1 硬件准备

可以准备一台 linux 系统机器,真实服务器或者虚拟机都可以,操作步骤没有多大的区别。

1.2 环境准备

先采用 docker 容器进行搭建,原生的搭建步骤比较多,会单独写一篇来记录。 如果不会 docker 的可以先去看一下关于 docker 的文章,当然我的博客记录下也有,不想去寻找其他的文章的话可以去看一下。

  • 拉取容器:docker pull morunchang/fastdfs

  • 运行 tracker 容器:docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh

  • 运行 storage 容器:docker run -d --name storage --net=host -e TRACKER_IP=<主机ip>:22122 -e GROUP_NAME=<组名> morunchang/fastdfs sh storage.sh

主机名填自己的虚拟机的 ip (**TP:** 如果发现每次虚拟机重启之后 IP 老是变动的话,可以直接去网络管理里面进行网络地址的配置,这样就固定了),组名按道理是自己进行定义的,但是为了方便的话可以定义 group1 这种分序号的方式,这个之后在进行文件下载的时候会使用。

1.3 配置系统

在上面的两个容器都启动之后,通过 docker 命令进入到容器当中配置一些相关的信息。

docker exec -it storage /bin/bash

修改内置nginx的配置文件,添加禁止缓存( 添不添加其实无所谓 )。

# vi /etc/nginx/conf/nginx.conf

location ~ /M00 {
    root /data/fast_data/data;
    ngx_fastdfs_module;
}

在里面添加 add_header Cache-Control no-store; 就可以禁止缓存了。后面如果涉及跨域访问的话,也可以回来在这里进行跨域的请求设置。

进入 http.conf 文件下,开启 token 检查和设置密钥。开启之后通过 nginx 进行的资源访问就需要添加 token,没有相应的令牌就会访问默认的图片,也就是当前配置文件的结束那里指定的文件,当然这个密钥也就是生成对应 token 时需要使用的,一旦暴露出去,别人也可以进行 token 的生成,需要进行保密。

设置完毕之后就可以退出容器,然后重启容器服务即可。这里如果是用的服务器,需要在安全组中开放对应的 22122230008080 这几个端口,防火墙也需要将端口暴露出来,默认使用的几个端口,可以自己在配置文件中进行修改,重启就可以了。

2、FastDFS 串讲

如果想了解一下 FastDFS 的相关信息的话可以看一下这个小节,不想看的话也可以直接到第三节开始下面的连接配置。

2.1 简介

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

FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

2.2 架构说明

FastDFS 架构包括 Tracker serverStorage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

  • Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器,你也可以理解为是一个前台,做接待用的。

  • Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器,也就是实际存储文件数据的东西,和 NTFS 这些干着同样的活。

2.3 上传流程

  • 文件ID:文件ID由组名,虚拟磁盘路径,数据两级目录,文件名四个部分组成,例如 group1/M00/00/00/rB6S8GGFLuWARl13AAC-WxoW0Zk101.jpg

  • 组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存,也就是最开始启动 storage 时指定的组名。

  • 虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了 store_path0 则是M00,如果配置了 store_path1 则是 M01,以此类推,可以在 nginx.conf 配置文件中进行配置。

  • 数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件,就是用于防止文件之间重名导致文件被覆盖。

  • 文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

3、后台环境搭建

3.1 依赖导入

这里采用 springboot 来搭建项目结构,其他的可以么,也可以,但是需要自己去搭建,都差不多。

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>1.3.2</version>
</dependency>

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

<dependency>
  <groupId>net.oschina.zcx7878</groupId>
  <artifactId>fastdfs-client-java</artifactId>
  <version>1.27.0.0</version>
</dependency>

3.2 相关配置

在项目的 application.yaml 配置文件中添加 dfs 的相关配置。

# 配置端口信息
server:
port: 80
tomcat:
  uri-encoding: UTF-8

spring:
http:
  encoding:
    charset: utf-8
    force: true
    enabled: true
servlet:
  multipart:
    enabled: true
    max-file-size: 10MB #单个文件上传大小
    max-request-size: 20MB #总文件上传大小

fdfs:
# 链接超时
connect-timeout: 5000
# 读取时间
so-timeout: 5000
# 生成缩略图参数
thumb-image:
  width: 150
  height: 150
tracker-list: 刚才配置的文件服务器的IP地址:22122

3.3 配置类

client 来进行文件的上传下载的话,这个配置基本不需要进行什么配置。

package com.beordie.config;

import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;

/**
* @Description 文件上, 下传配置
* @Date 2021/11/2 17:28
* @Created 30500
*/
@Configuration
@Import(FdfsClientConfig.class)
// 防止重复注入 bean
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class DfsConfig {

}

3.4 工具类

文件的上传下载工作基本都会在这个工具类中进行管理,所以这个部分的代码编写比较重要,认真理解。

package com.beordie.utils;

import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.csource.common.MyException;
import org.csource.fastdfs.ProtoCommon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

/**
* @Description 文件上传下载
* @Date 2021/11/2 17:38
* @Created 30500
*/
@Component
public class DfsUtil{
  /**
   * 密钥
   */
  private final static String SECRET_KEY = "设置的密钥";
  /**
   * 服务器主机
   */
  private final static String HOST_NAME = "http://IP:port";

  /**
   * 日志的记录打印
   */
  private static final Logger LOGGING = LoggerFactory.getLogger(DfsUtil.class);

  @Autowired
  /**
   * 文件处理的主要链接对象
   */
  private FastFileStorageClient storageClient;

  /**
   * 上传文件
   * @param file 需要进行上传的文件
   * @return 返回文件ID
   * @throws IOException
   */
  public String uploadImage(MultipartFile file) throws IOException {
      String originalFilename = file.getOriginalFilename().substring(file.getOriginalFilename().
              indexOf('.') + 1);
      StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(file.getInputStream(),
              file.getSize(), originalFilename, null);
      return storePath.getFullPath();
  }

  /**
   * 文件的删除
   * @param fileName 文件id
   * @return
   */
  public boolean deleteFile(String fileName) {
      if(StringUtils.isEmpty(fileName)) {
          LOGGING.info("文件路径为空");
          return false;
      }
      StorePath storePath = null;
      try {
          storePath = StorePath.parseFromUrl(fileName);
          storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
      } catch (Exception e) {
          LOGGING.info(e.getMessage());
          return false;
      }
      return true;
  }

  /**
   * 根据文件名进行文件的下载, 不需要获取token
   * @param fileName 文件名
   * @param downName 下载到本地的名字
   * @return 具体文件
   */
  public byte[] downloadFile(String fileName, String downName) {
      byte[] content = null;
      HttpHeaders headers = new HttpHeaders();
      StorePath storePath = null;
      try {
          storePath = StorePath.parseFromUrl(fileName);
          storageClient.downloadFile(storePath.getGroup(), storePath.getPath(), null);
          headers.setContentDispositionFormData("attachment", new String(downName.getBytes("UTF-8"), "iso-8859-1"));
          headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          return content;
      }
  }

  /**
   * 获取文件的访问 token
   * @param fileName 文件名
   * @return 携带 token 的文件地址
   * @throws UnsupportedEncodingException
   * @throws NoSuchAlgorithmException
   * @throws MyException
   */
  public String getResourceUrl(String fileName) throws UnsupportedEncodingException, NoSuchAlgorithmException, MyException {
      String url = fileName.substring(fileName.indexOf("/") + 1);
      int lts = (int)(System.currentTimeMillis() / 1000);
      String token = ProtoCommon.getToken(url, lts, SECRET_KEY);
      return HOST_NAME + "/" + fileName + "?token=" + token + "&ts=" + lts;
  }
}

4、代码测试

相关配置编写完成之后,就可以编写一个控制器来检测一下具体的效果如何。利postman 来完成测试,因为文件上传需要用到 POST 请求。

package com.beordie.contrller;

import com.beordie.common.Response;
import com.beordie.utils.DfsUtil;
import com.beordie.utils.StringUtils;
import org.csource.common.MyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;

/**
* @Description 文件上传下载
* @Date 2021/11/6 15:32
* @Created 30500
*/
@RestController
@RequestMapping("file")
public class FileController {
  @Autowired
  private DfsUtil dfsUtil;

  /**
   * 日志打印
   */
  private final Logger LOGGING = LoggerFactory.getLogger(FileController.class);

  /**
   * 图片上传
   * @param image 图片资源
   * @return 文件ID
   */
  @RequestMapping(value = "image", method = RequestMethod.POST)
  public Response uploadImage(@RequestParam("image") MultipartFile image) {
      Response response = new Response();
      try {
          String imageId = dfsUtil.uploadImage(image);
          if (!StringUtils.isEmpty(imageId)){
              response.setMessage(imageId);
          } else {
              response.setMessage("上传失败");
          }
      } catch (IOException e) {
          LOGGING.info("服务异常");
      }
      return response;
  }

  /**
   * 获取token
   * @param fileId 文件ID
   * @return 携带token的地址
   */
  @RequestMapping(value = "token")
  public Response getToken(String fileId) {
      Response response = new Response();
      try {
          String url = dfsUtil.getResourceUrl(fileId);
          response.setMessage(url);
      } catch (UnsupportedEncodingException e) {
          LOGGING.info(e.getMessage());
      } catch (NoSuchAlgorithmException e) {
          LOGGING.info(e.getMessage());
      } catch (MyException e) {
          LOGGING.info(e.getMessage());
      }
      return response;
  }
}

4.1 文件上传

4.2 获取 token