前言
我们在做项目的时候经常会遇到文件的上传与下载。你们是怎么做的呢?现在有一个技术可以非常简单的实现这个功能——fastDFS
简介
FastDFS是一个分布式文件系统,使用FastDFS可以非常容易搭建一套高性能的文件服务器集群提供文件上传、下载服务。
原理
它主要包含两个部分, Tracker server
和 Storage server
客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker server主要是用来进行负载均衡和调度。其实它就是一个调度服务器,通过一些策略找到Storage server来提供文件上传服务。而Storage server的主要作用就是用来进行文件存储。我们上传上来的数据最终也是存储到Storage服务器上的。需要注意的一点就是它本身是没有实现自己的文件系统,而是利用操作系统上的文件系统来管理文件的。所以Storage被称为存储服务器。
文件上传示意图:
文件下载示意图:
如何使用
- 导入依赖
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
- 编写配置文件
server.port=8080
# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs.so-timeout = 1501
fdfs.connect-timeout = 601
#缩略图生成参数
fdfs.thumb-image.width= 150
fdfs.thumb-image.height= 150
#TrackerList参数,支持多个
fdfs.tracker-list=10.199.12.106:22122
#访问路径
fdfs.web-server-url=http://10.199.12.106:8888/
- 代码
创建一个工具类
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.domain.proto.storage.DownloadCallback;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@Component
public class FastDFSClientUtil {
@Autowired
private FastFileStorageClient storageClient;
/**
* 上传
* @param file 文件类
* @return fileId 文件Id,包含group和uri的完整路径 EG=>>MO/22/22/**.jpg
* @throws IOException
*/
public String uploadFile(MultipartFile file) throws IOException {
StorePath storePath = storageClient.uploadFile((InputStream) file.getInputStream(), file.getSize(),FilenameUtils.getExtension(file.getOriginalFilename()), null);
return storePath.getFullPath();
}
/**
* 删除
* @param filePath 文件的fileId 包含分组,EG=>>group1/M00/22/22/***.**
*/
public void delFile(String filePath) {
storageClient.deleteFile(filePath);
}
/**
* 下载
* @param groupName 分组id EG=>> group10
* @param path 文件uri 分组之后的内容[不含开头的/] EG=>>MO/22/22/kkk.jpg
* @return 文件的字节数组
*/
public byte[] download(String groupName, String path) throws IOException {
InputStream ins = storageClient.downloadFile(groupName, path, new DownloadCallback<InputStream>() {
@Override
public InputStream recv(InputStream ins) throws IOException {
// 将此ins返回给上面的ins
return ins;
}
});
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc = 0;
while ((rc = ins.read(buff, 0, 100)) > 0) {
byteArrayOutputStream.write(buff, 0, rc);
}
return byteArrayOutputStream.toByteArray();
}
}
实现功能
import com.doria.fastdfs.utils.FastDFSClientUtil;
import com.github.tobato.fastdfs.exception.FdfsServerException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
@RestController
@RequestMapping("/file")
@Slf4j
public class FileController {
// 引入客户端工具类
@Autowired
private FastDFSClientUtil fastDFSClientUtil;
// 获取respon稍后向前端页面写入图片
@Autowired
private HttpServletResponse response;
// 读取调度服务所在服务器稍后可以进行拼接,便于下次直接填写,这里由于nginx监听的是80端口,所以没有配置端口。默认访问80
@Value("${fileServerUrl}")
private String fileServerUrl;
@PostMapping("/upload")
public String uploadFile(MultipartFile file) {
try {
// 判断文件是否存在
if (file == null) {
throw new RuntimeException("文件不存在");
}
// 获取文件的完整名称
String originalFilename = file.getOriginalFilename();
if (StringUtils.isEmpty(originalFilename)) {
throw new RuntimeException("文件不存在");
}
// 获取到返回的fileId
String url = fastDFSClientUtil.uploadFile(file);
// 拼接返回
return fileServerUrl + url;
} catch (Exception e) {
e.printStackTrace();
}
return "文件上传失败";
}
// 删除文件[这里仅作测试用,实际中不会这样直接在controller删除
// 是会根据业务从数据库中拿到url进行删除同时删除数据库关联数据,注意被占用无法删除]
@DeleteMapping("/del")
public String delFile(@RequestParam String fileId) {
try {
fastDFSClientUtil.delFile(fileId);
return "删除成功";
} catch (Exception e) {
e.printStackTrace();
}
return "删除失败";
}
/**
* 文件下载,页面直接访问我们这个服务器,在页面眼里,直接从我们这个服务拿到了文件,而我们去fastdfs服务器获取文件
* 前方的ip端口我们可以固定一个服务器,ip就是当前方法所在服务器的ip,前端找到我们。我们再去给他下载转发
* 也可以将这个服务部署在fastdfs的本地,然后通过springcloud即时发现服务调用下载服务
* @param groupName
* @param path
*/
@GetMapping("/download")
public void download(@RequestParam String groupName, @RequestParam String path) {
try {
// 拆分获取出文件名称,方便一会写入的时候写出正确的文件名(url中的文件名和服务器中是一致的,至少默认是这样的)
String[] split = path.split("/");
String imgName=split[split.length-1];
// 设置请求头为附件模式
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(imgName, "UTF-8"));
// 调用客户端获取文件字节码
byte[] imageByte = fastDFSClientUtil.download(groupName, path);
// 从response获取响应流
ServletOutputStream outputStream = response.getOutputStream();
// 向流写入数据
outputStream.write(imageByte);
// 关流
outputStream.close();
}catch (FdfsServerException e){
log.error("文件不存在");
}
catch (Exception e) {
e.printStackTrace();
}
}
}