Docker - 部署FastDFS及SpringBoot文件上传下载实现
一. 前言
- 本文参考了以下文本,将其中内容进行个人化统合和补充:
- 本地环境
项目 版本 docker宿主机系统 openSUSE Leap 15.3 x64 docker版本 20.10.6-ce FastDFS镜像版本 season/fastdfs:1.2 开发环境 macOS Catalina 10.15.5 Spring Boot 版本 2.1.8.RELEASE JDK Version Java SE 1.8.0_281 包依赖管理工具 Apache Maven 3.6.3
注:因为某些地区和网络供应商下,无法访问DockerHub、mvnrepository等服务器,本文默认在无墙环境下操作,如因网络原因导致。
二. 部署FastDFS
- FastDFS简介
FastDFS是阿里余庆大神做的一个个人项目,以 C 语言开发的一项开源轻量级分布式文件系统,提供了文件存储、文件同步、文件访问(文件上传/下载)等功能。注:FastDFS适合存储海量的小文件,对于大文件(超过100M)存储管理,不建议使用FastDFS。
FastDFS由两部分组成:Tracker Server(跟踪服务器)、Storage Server(存储服务器)。跟踪服务器,主要做调度工作,起到均衡的作用,负责管理所有的 storage server和 group,每个 storage 在启动后会连接 Tracker,告知自己所属 group 等信息,并保持周期性心跳;存储服务器,主要提供容量和备份服务,以 group 为单位,每个 group 内可以有多台 storage server,数据互为备份。 - 拉取镜像并创建容器
- 首先这里选择的镜像版本是最新(CREATED 6 years ago)的纯净版(docker hub)
# 拉取命令 docker pull season/fastdfs:1.2 # 等待下载完毕后见下方提示为成功 # Downloaded newer image for season/fastdfs:1.2 # docker.io/season/fastdfs:1.2
- 创建Tracker Server和Storage Server挂载目录
# 目录可调整,但是后续操作需要根据当前创建的挂载目录进行修改 mkdir -p /usr/local/server/fastdfs/tracker/data mkdir -p /usr/local/server/fastdfs/storage/data mkdir -p /usr/local/server/fastdfs/storage/path
- 创建Tracker Server容器
注:在这里创建的容器使用host网络模式目的是让Tracker Server容器和Storage Server容器在同一网段下,这样才方便连接。如果使用默认的bridge桥接模式启动,会在各自容器内创建独立的虚拟网卡,在Tracker Server和Storage Server连接的时候出现无法找到问题。有关docker网络模式解释详见:docker --net详解_Docker网络通信# 这里对下方启动参数进行说明 # -id == -i -d :交互模式运行容器 & 后台运行容器 # --name :为启动的容器命名 # --restart=always :开机自启 # --net host :宿主机和容器共享networknamespace,也就是使用同一个网络环境,Tracker Server默认占用22122端口 # -v :挂载目录(宿主机目录:容器目录),这里其实可以多加一个容器的/etc/fdfs/目录挂载,方便后期修改配置文件 docker run -id --name tracker \ --restart=always --net host \ -v /usr/local/server/fastdfs/tracker/data:/fastdfs/tracker/data \ season/fastdfs:1.2 tracker
- 创建Storage Server容器
# 对下方命令进行解释(重复的见上方解释) # -e TRACKER_SERVER :指定Tracker Server的 ip地址:端口 # Storage Server容器也使用host网络模式,默认占用23000端口 docker run -id --name storage \ --restart=always --net host \ -v /usr/local/server/fastdfs/data/storage:/fastdfs/store_path \ -e TRACKER_SERVER="{Tracker Server主机ip}:22122" \ season/fastdfs:1.2 storage
- 首先这里选择的镜像版本是最新(CREATED 6 years ago)的纯净版(docker hub)
- 修改Tracker Server配置文件
- Tracker Server容器中自带了Tracker Server和Client客户端工具,用来连接Tracker Server,而Client客户端工具默认的配置文件/etc/fdfs/client.conf中配置的默认Tracker Server地址大部分是错误的,需要手动修改配置文件用来连接正确的Tracker Server。
- 因为根据上面对Tracker Server容器运行时命令并为创建映射目录,且在伪终端中无法执行编辑命令,故拷出配置文件在宿主机中修改后再拷入容器。
# 拷出到 /usr/local/server/fastdfs/ 目录下 docker cp trakcer:/etc/fdfs/client.conf /usr/local/server/fastdfs/ # 编辑 nano /usr/local/server/fastdfs/client.conf # 修改tracker_server为Tracker Server的ip和端口,例如: tracker_server=192.168.123.200:22122 # 保存后退出编辑器,再将文件覆盖到容器中 docker cp /usr/local/server/fastdfs/client.conf tracker:/etc/fdfs
- 验证一下FastDFS是否部署成功
注:Storage Server映射的data路径是:/usr/local/server/fastdfs/data/storage/data# docker 命令检查容器是否正常启动 docker ps # 进入Tracker Server容器 docker exec -it tracker bash # 检查是否能连接到Storage Server,成功的话会打印宿主机信息和各Storage Server信息 fdfs_monitor /etc/fdfs/client.conf # 创建测试文件 echo "test" > test.txt # 使用Client客户端向Storage Server发送测试文件 fdfs_upload_file /etc/fdfs/client.conf test.txt # 如果成功的话,Console会打印一串文件路径,例如: # group1/M00/00/00/wKh7yGIQxuuAH6baAAAACBfWGpM819.txt # 退出伪终端 exit
- 修改宿主机防火墙
这一步是为了Spring Boot集成Client可以正常访问而做的操作。
执行操作前,可以检查FastDFS所占用的端口。# 检查防火墙状态,建议保持启动,开放需要端口即可 systemctl status firewalld # 检查已开启端口,如果已经存在 22122/tcp 或 23000/tcp,则无需执行对应端口的开放操作 firewall-cmd --zone=public --list-ports # 开放22122/tcp 和 23000/tcp,console打印success即为成功 firewall-cmd --permanent --zone=public --add-port=22122/tcp firewall-cmd --permanent --zone=public --add-port=23000/tcp # 刷新防火墙配置 firewall-cmd --reload
# 查看当前已占用的所有端口和进程 lsof -i # 检查对应端口的进程 lsof -i:22122 lsof -i:23000
三. 部署配套的Nginx(非必须)
- 因为部署的FastDFS只能在宿主机使用,这肯定是不方便的,此时就需要Nginx进行代理,实现web访问。
- 创建Nginx映射目录
mkdir -p /usr/local/server/fastdfs/nginx/
- Storage Server自带了一份nginx配置文件,使用这个即可
# 拷出到映射目录中 docker cp storage:/etc/nginx/conf/nginx.conf /usr/local/server/fastdfs/nginx/ # 编辑配置文件 nano /usr/local/server/fastdfs/nginx/nginx.conf # 修改配置文件中local节点内容为: location / { root /fastdfs/store_path/data; ngx_fastdfs_module; } # 保存并退出编辑器
- 创建nginx容器
# 映射宿主机的8888端口,这个可以随意改动 # -e TRACKER_SERVER 需要改为Tracker Server的ip地址 docker run -id --name fastdfs_nginx \ --restart=always \ -v /usr/local/server/fastdfs/data/storage:/fastdfs/store_path \ -v /usr/local/server/fastdfs/nginx/nginx.conf:/etc/nginx/conf/nginx.conf \ -p 8888:80 \ -e TRACKER_SERVER={Tracker Server主机ip}:22122 \ season/fastdfs:1.2 nginx
- 测试查看之前上传的文件
# 通过Nginx代理访问FastDFS # URL构成为:http://{宿主机ip}:{Nginx代理端口}/{Storage Server返回的文件路径},例如: curl -i http://127.0.0.1:8888/group1/M00/00/00/wKh7yGIQxuuAH6baAAAACBfWGpM819.txt # 如果成功,则会打印 http response 的头信息和文件内容 # 同时,也可将URL在web浏览器中访问
四. Spring Boot整合FastDFS_Client
-
给项目添加依赖(pom.xml):可以将依赖添加到common工程中
详情详见:https://github.com/tobato/FastDFS_Client<!-- FastDFS Client --> <dependency> <groupId>com.github.tobato</groupId> <artifactId>fastdfs-client</artifactId> <version>1.27.2</version> </dependency>
-
在需要使用FastDFS的工程的启动类(Application)添加FastDFS Java客户端注解:
@Import(FdfsClientConfig.class) @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
-
添加配置(application.yml),例如:
fdfs: so-timeout: 1500 # 读取时间 connect-timeout: 600 #连接时间 thumb-image: # 缩略图生成参数 width: 150 height: 150 tracker-list: # Tracker Server主机 - 192.168.123.200:22122 web-server-url: http://192.168.123.200:8888/
-
可以创建一个工具类,方便上传下载:
点击查看代码
import com.github.tobato.fastdfs.domain.conn.FdfsWebServer; import com.github.tobato.fastdfs.domain.fdfs.StorePath; import com.github.tobato.fastdfs.domain.proto.storage.DownloadByteArray; import com.github.tobato.fastdfs.exception.FdfsUnsupportStorePathException; import com.github.tobato.fastdfs.service.FastFileStorageClient; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.Charset; /** * FastDFS工具类 **/ @Component public class FastDFSClient { @Autowired private FastFileStorageClient storageClient; @Autowired private FdfsWebServer fdfsWebServer; /** * 上传文件 * * @param file 文件对象 * @return 文件访问地址 * @throws IOException */ public String uploadFile(MultipartFile file) throws IOException { StorePath storePath = storageClient.uploadFile(file.getInputStream(), file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()), null); return getResAccessUrl(storePath); } /** * 上传文件 * * @param file 文件对象 * @return 文件访问地址 * @throws IOException */ public String uploadFile(File file) throws IOException { FileInputStream inputStream = new FileInputStream(file); StorePath storePath = storageClient.uploadFile(inputStream, file.length(), FilenameUtils.getExtension(file.getName()), null); return getResAccessUrl(storePath); } /** * 将一段字符串生成一个文件上传 * * @param content 文件内容 * @param fileExtension * @return */ public String uploadFile(String content, String fileExtension) { byte[] buff = content.getBytes(Charset.forName("UTF-8")); ByteArrayInputStream stream = new ByteArrayInputStream(buff); StorePath storePath = storageClient.uploadFile(stream, buff.length, fileExtension, null); return getResAccessUrl(storePath); } /** * 封装图片完整URL地址 */ private String getResAccessUrl(StorePath storePath) { String fileUrl = fdfsWebServer.getWebServerUrl() + storePath.getFullPath(); return fileUrl; } /** * 删除文件 * * @param fileUrl 文件访问地址 * @return */ public void deleteFile(String fileUrl) { if (StringUtils.isEmpty(fileUrl)) { return; } try { StorePath storePath = StorePath.parseFromUrl(fileUrl); storageClient.deleteFile(storePath.getGroup(), storePath.getPath()); } catch (FdfsUnsupportStorePathException e) { System.out.println(e.getMessage()); /** TODO 只是测试,所以未使用,logger,正式环境请修改打印方式 **/ } } /** * 下载文件 * * @param fileUrl 文件URL * @return 文件字节 * @throws IOException */ public byte[] downloadFile(String fileUrl) throws IOException { String group = fileUrl.substring(0, fileUrl.indexOf("/")); String path = fileUrl.substring(fileUrl.indexOf("/") + 1); DownloadByteArray downloadByteArray = new DownloadByteArray(); byte[] bytes = storageClient.downloadFile(group, path, downloadByteArray); return bytes; } }
-
创建上传和下载方法
点击查看代码
@RestController @RequestMapping("/file") public class FileUploadController { @Autowired private FastDFSClient fastDFSClient; /** * 上传 * @param file * @return * @throws IOException */ @RequestMapping("/upload") public String uploadFile(MultipartFile file) throws IOException { }return fastDFSClient.uploadFile(file); } /** * 下载 * @param fileUrl * @param response * @throws IOException */ @RequestMapping("/download") public void downloadFile(String fileUrl, HttpServletResponse response) throws IOException { byte[] bytes = fastDFSClient.downloadFile(fileUrl); response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileUrl.substring(fileUrl.lastIndexOf("/") + 1), "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(); } } } }