文件管理是每一个部分不可缺少的一部分,特别是一些相册网站、视频网站登过,文件管理服务是非常重要的一部分。开源的分布式文件系统有很多包括HDFS、MooseFS、FastDFS等,每个分布式系统各有各自的特点,但大体上的功能包括:文件存储、文件访问、文件同步,并且有些系统会帮我们解决大容量存储和负载均衡的问题。在这繁多的分布式系统中,我只了解过HDFS和FastDFS,两者的侧重点不同,HDFS是适合存储和管理大文件,不太适合存储海量的小文件、频繁的修改文件以及大量的随机读,HDFS经常用于大数据分析领域,而FastDFS相反,特别适合存储和管理小型文件。所以这里介绍一下FastDFS。
在FastDFS中有三个重要的角色:Tracker Server、Storage Server、Client。
Tracker Server:跟踪服务器,负责文件管理、调度、控制中心以及负载均衡的操作。特别类似于注册中心【相当于Dubbo】,因此每个Storage Server启动后会连接Tracker Server,并告知自己的信息,并保持心跳。
Storage Server:存储服务器,也就是FastDFS中真正干活的一个角色,提供文件上传\下载、文件修改、文件删除的服务。在Storage Server中又是以Group为单位的,每个Group中可以有几个Storage Server,几个之间持续实现数据备份。
Client:客户端,也就是我们自己的服务器,调用FastDFS实现文件上传下载等操作。

文件的上传流程

1)Storage Server会定时上传信息到Tracker Server中,并在Tracker Server中进行注册。
2)上传连接请求会先访问Tracker Server,Tracker Server 返回空闲的、可用的Storage Server。将Storage Server的IP和端口号返回。
3)Client拿到Tracker Server返回的IP和端口号后,开始文件的上传。
4)Storage Server将文件写入服务器的本地磁盘中,并将文件的存储信息返回给用户。上传的文件会生成一个唯一的ID值。
上传完成后会给客户端返回一个文件ID,此ID用于以后访问该文件的索引信息。文件的索引信息包括:组名、虚拟磁盘路径、数据两级目录、文件名。
例如返回:group1 /M00 /02/44/ wKgDrE34E8wAAAAAAAAGkEIYJK42378.sh。
组名 【group1】:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。
虚拟磁盘路径【/M00】:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
数据两级目录【/02/44/】:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据文件。
文件名【wKgDrE34E8wAAAAAAAAGkEIYJK42378.sh】:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
文件的下载流程

1) Client向Tracker Server发送下载请求,Tracker Server返回对应存储文件所在的Storage Server,然后将其IP和端口返回。
2)Client直接通过返回的IP和端口与其中的一台Storage Server建立连接并下载指定的文件。
文件服务的搭建
FastDFS的搭建可使用Docker进行安装,可以参考这篇文章《docker 安装 FastDFS》,文件服务这里使用基于SpringClound搭建的一个微服务。
①准备FastDFS的配置文件
connect_timeout=60
network_timeout=60
charset=UTF-8
# Tracker的Http请求端口
http.tracker_http_port=8080
# Tracker的TCP通信端口
tracker_server=192.168.132.132:22122
②项目的yml文件配置
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
application:
name: file
server:
port: 18082
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
③引入相应的maven依赖
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
④创建一个对应的FastDFS工具类
/**
* 实现FastDfs文件管理:
* 文件上传
* 文件删除
* 文件下载
* 文件信息获取
* Storage信息获取
* Tracker信息获取
*
* @author SunRains
* @date 2021/3/2 0002
*/
public class FastDFSUtil {
static {
try {
String item = new ClassPathResource("item").getPath();
System.out.println(item);
String fileName = new ClassPathResource("fdfs_client.conf").getPath();
ClientGlobal.init(fileName);
} catch (IOException e) {
e.printStackTrace();
} catch (MyException e) {
e.printStackTrace();
}
}
/**
* 文件上传
* @param fastDFSFile :上传文件信息封装
*/
public static String[] upload(FastDFSFile fastDFSFile) throws Exception {
NameValuePair[] metaList=new NameValuePair[1];
metaList[0]=new NameValuePair("author",fastDFSFile.getAuthor());
// 通过StorageClient访问Storage,实现文件上传,并且获取文件上传后的存储信息
StorageClient storageClient = getStorageClient();
/*
通过storageClient访问Storage,实现文件上传,并且获取文件上传的存储信息
1:上传文件的字节数组
2:文件的扩展名:jpg
3:附加参数:比如 拍摄地址:北京
返回数组
[0]:文件上传所存储的Storage的组名字 group1
[1]:文件存储到Storage上的文件名字 /M00/02/04/xx.jpg包括虚拟路径
*/
return storageClient.upload_file(fastDFSFile.getContent(), fastDFSFile.getExt(), metaList);
}
/**
* 获取文件信息
* @param groupName 文件组名:group1
* @param remoteFileName 文件的存储路径:M00/00/00/wKiEhGA-y1-AB3p8AABhjgoR8Vw160.png
*/
public static FileInfo getFile(String groupName, String remoteFileName) throws Exception {
// 创建TrackerClient对象,通过TrackerClient访问TrackerServer
StorageClient storageClient = getStorageClient();
// 获取文件信息
return storageClient.get_file_info(groupName,remoteFileName);
}
/**
* 文件下载
* @param groupName 文件组名:group1
* @param remoteFileName 文件的存储路径:M00/00/00/wKiEhGA-y1-AB3p8AABhjgoR8Vw160.png
*/
public static InputStream downloadFile(String groupName, String remoteFileName) throws Exception{
// 创建TrackerClient对象,通过TrackerClient访问TrackerServer
StorageClient storageClient = getStorageClient();
byte[] buffer = storageClient.download_file(groupName, remoteFileName);
return new ByteArrayInputStream(buffer);
}
/**
* 删除文件
* @param groupName 文件组名:group1
* @param remoteFileName 文件的存储路径:M00/00/00/wKiEhGA-y1-AB3p8AABhjgoR8Vw160.png
*/
public void deleteFile(String groupName, String remoteFileName) throws Exception{
StorageClient storageClient = getStorageClient();
storageClient.delete_file(groupName,remoteFileName);
}
/**
* 获取StorageClient对象
* @return
* @throws IOException
*/
private static StorageClient getStorageClient() throws IOException {
// 创建TrackerClient对象,通过TrackerClient访问TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过TrackerClient获取TrackerServer的连接对象
TrackerServer trackerServer = trackerClient.getConnection();
// 通过TrackerServer获取Storage信息,创建StorageClient对象存储Storage信息
return new StorageClient(trackerServer, null);
}
/**
* 获取Storage信息
* @throws Exception
*/
public static StorageServer getStorage() throws Exception{
// 创建TrackerClient对象,通过TrackerClient访问TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过TrackerClient获取TrackerServer的连接对象
TrackerServer trackerServer = trackerClient.getConnection();
// 获取Storage信息
return trackerClient.getStoreStorage(trackerServer);
}
/**
* 获取Tracker信息
* @throws Exception
*/
public static String getTrackerInfo() throws Exception{
// 创建TrackerClient对象,通过TrackerClient访问TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过TrackerClient获取TrackerServer的连接对象
TrackerServer trackerServer = trackerClient.getConnection();
// Tracker的IP和HTTP端口
String ip = trackerServer.getInetSocketAddress().getHostString();
int tracker_http_port = ClientGlobal.getG_tracker_http_port();
return "http://"+ip+":"+tracker_http_port;
}
/**
* 获取Storage的IP和端口信息
*/
public static ServerInfo[] getServerInfo(String groupName, String remoteFileName) throws Exception{
// 创建TrackerClient对象,通过TrackerClient访问TrackerServer
TrackerClient trackerClient = new TrackerClient();
// 通过TrackerClient获取TrackerServer的连接对象
TrackerServer trackerServer = trackerClient.getConnection();
// 获取Storage的IP和端口信息
return trackerClient.getFetchStorages(trackerServer,groupName,remoteFileName);
}
public static void main(String[] args) throws Exception {
// 文件下载
InputStream inputStream = downloadFile("group1", "M00/00/00/wKiEhGA-y1-AB3p8AABhjgoR8Vw160.png");
// 将文件写入本地磁盘
FileOutputStream os=new FileOutputStream("D:/1.png");
// 定义一个缓冲区
byte[] buffer=new byte[1024];
while(inputStream.read(buffer)!=-1){
os.write(buffer);
}
os.flush();
os.close();
inputStream.close();
}
}
当然需要自己封装一个文件信息的对象FastDFSFile。
public class FastDFSFile implements Serializable {
//文件名字
private String name;
//文件内容
private byte[] content;
//文件扩展名
private String ext;
//文件MD5摘要值
private String md5;
//文件创建作者
private String author;
public FastDFSFile(String name, byte[] content, String ext, String md5, String author) {
this.name = name;
this.content = content;
this.ext = ext;
this.md5 = md5;
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext) {
this.name = name;
this.content = content;
this.ext = ext;
}
public FastDFSFile() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
@Override
public String toString() {
return "FastDFSFile{" +
"name='" + name + '\'' +
", content=" + Arrays.toString(content) +
", ext='" + ext + '\'' +
", md5='" + md5 + '\'' +
", author='" + author + '\'' +
'}';
}
}
⑤编写对应的Controller即可。
/**
* @author SunRains
* @date 2021/3/2 0002
*/
@RestController
@CrossOrigin
@RequestMapping(value = "/upload")
public class FileUploadController {
@PostMapping
public Result upload(@RequestParam(value = "file")MultipartFile file) throws Exception{
// 封装文件信息
FastDFSFile fastDFSFile=new FastDFSFile(file.getOriginalFilename(),
file.getBytes(), StringUtils.getFilenameExtension(file.getOriginalFilename()));
String[] upload = FastDFSUtil.upload(fastDFSFile);
// 拼接访问地址
String url="http://192.168.132.132:8080/"+upload[0]+"/"+upload[1];
return new Result<>(true, StatusCode.OK,"上传文件成功",url);
}
}
就这样以后如果其他模块需要上传文件就只需要调用这个接口就可以实现文件的上传操作。