小东子的个人技术专栏

dubbo2.5-spring4-mybastis3.2-springmvc4-mongodb3.4-redis3.2整合(八)SpringMVC上传文件到FastDFS

目前项目中需要存储一些文件、视频等。于是乎,查找了一些关于文件服务器资料。其中有Lustre、HDFS、Gluster、Alluxio、Ceph 、FastDFS。下面简单介绍一下:

  1. Lustre 是一个大规模的、安全可靠的、具备高可用性的集群文件系统,它是由SUN公司开发和维护的。该项目主要的目的就是开发下一代的集群文件系统,目前可以支持超过10000个节点,数以PB的数据存储量。

  2. HDFS Hadoop Distributed File System,简称HDFS,是一个分布式文件系统。HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。

  3. GlusterFS 是一个集群的文件系统,支持PB级的数据量。GlusterFS 通过RDMA和TCP/IP方式将分布到不同服务器上的存储空间汇集成一个大的网络化并行文件系统。

  4. Alluxio 前身是Tachyon,是以内存为中心的分布式文件系统,拥有高性能和容错能力,能够为集群框架(如Spark、MapReduce)提供可靠的内存级速度的文件共享服务。

  5. Ceph 是新一代开源分布式文件系统,主要目标是设计成基于POSIX的没有单点故障的分布式文件系统,提高数据的容错性并实现无缝的复制。

  6. FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
    通过以上6中文件的服务器的介绍,我们业务非常适合选择用FastDFS,所以就了解学习了一番,感觉确实颇为强大,在此再次感谢淘宝资深架构师余庆大神开源了如此优秀的轻量级分布式文件系统,本篇文章就记录一下FastDFS的最新版本5.0.9在CentOS7中的安装与配置。

1.Fastdfs的简介

了解一下基础概念,FastDFS是一个开源的轻量级分布式文件系统,由跟踪服务器(tracker server)、存储服务器(storage server)和客户端(client)三个部分组成,主要解决了海量数据存储问题,特别适合以中小文件(建议范围:4KB < file_size <500MB)为载体的在线服务。

FastDFS系统结构如下图所示:

这里写图片描述

跟踪器和存储节点都可以由一台多台服务器构成。跟踪器和存储节点中的服务器均可以随时增加或下线而不会影响线上服务。其中跟踪器中的所有服务器都是对等的,可以根据服务器的压力情况随时增加或减少。

为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷 的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起 到了冗余备份和负载均衡的作用。

在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服务。

当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。

2.FastDFS的下载

Fastdfs的稳定版下载地址
这里写图片描述

3.FastDFS的 安装

详细见
CentOS 7 安装配置分布式文件系统 FastDFS 5.0.5

4.SpringMVC上传文件到FastDFS

####4.1 fast_client.cnf配置

1
2
3
4
5
6
7
8
9
10
11
connect_timeout = 2
#网络超时时间
network_timeout = 30
#字符集
charset = UTF-8
#跟踪服务器的端口
http.tracker_http_port = 9099
http.anti_steal_token = no
http.secret_key = FastDFS1234567890
#跟踪服务器地址 。跟踪服务器主要是起到负载均衡的作用
tracker_server = 192.168.0.116:22122

4.2 fastdfs文件上传的流程

这里写图片描述

上传文件交互过程:

  1. client询问tracker上传到的storage,不需要附加参数;
  2. tracker返回一台可用的storage;
  3. client直接和storage通讯完成文件上传。

4.3 FastDFS文件下载的流程

这里写图片描述

下载文件交互过程:

  1. client询问tracker下载文件的storage,参数为文件标识(卷名和文件名);
  2. tracker返回一台可用的storage;
  3. client直接和storage通讯完成文件下载。

需要说明的是,client为使用FastDFS服务的调用方,client也应该是一台服务器,它对tracker和storage的调用均为服务器间的调用。

4.4 FastDFSUtil 的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
package com.lidong.dubbo.util;
import org.csource.common.MyException;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
/**
* @项目名称:lidong-dubbo
* @类名:FastDFSUtil
* @类的描述: FastDFS 上传文件到文件服务器
* @作者:lidong
* @创建时间:2017/2/6 下午5:23
* @公司:chni
* @QQ:1561281670
* @邮箱:lidong1665@163.com
*/
public class FastDFSUtil {
private final static
Logger logger = LoggerFactory.getLogger(FastDFSUtil.class);
/**
*上传服务器本地文件-通过Linux客户端,调用客户端命令上传
* @param filePath 文件绝对路径
* @return Map<String,Object> code-返回代码, group-文件组, msg-文件路径/错误信息
*/
public static Map<String, Object> uploadLocalFile(String filePath) {
Map<String, Object> retMap = new HashMap<String, Object>();
/**
* 1.上传文件的命令
*/
String command = "fdfs_upload_file /etc/fdfs/client.conf " + filePath;
/**
* 2.定义文件的返回信息
*/
String fileId = "";
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
/**
* 3.通过调用api, 执行linux命令上传文件
*/
Process process = Runtime.getRuntime().exec(command);
/**
* 4.读取上传后返回的信息
*/
inputStreamReader = new InputStreamReader(process.getInputStream());
bufferedReader = new BufferedReader(inputStreamReader);
String line;
if ((line = bufferedReader.readLine()) != null) {
fileId = line;
}
/**
* 5.如果fileId包含M00,说明文件已经上传成功。否则文件上传失败
*/
if (fileId.contains("M00")) {
retMap.put("code", "0000");
retMap.put("group", fileId.substring(0, 6));
retMap.put("msg", fileId.substring(7, fileId.length()));
} else {
retMap.put("code", "0001"); //上传错误
retMap.put("msg", fileId); //返回信息
}
} catch (Exception e) {
logger.error("IOException:" + e.getMessage());
retMap.put("code", "0002");
retMap.put("msg", e.getMessage());
}finally {
if (inputStreamReader!=null){
try {
inputStreamReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return retMap;
}
/**
* Description: 直接通过fdfs java客户端上传到服务器-读取本地文件上传
*
* @param filePath 本地文件绝对路径
* @return Map<String,Object> code-返回代码, group-文件组, msg-文件路径/错误信息
*/
public static Map<String, Object> upload(String filePath) {
Map<String, Object> retMap = new HashMap<String, Object>();
File file = new File(filePath);
TrackerServer trackerServer = null;
StorageServer storageServer = null;
if (file.isFile()) {
try {
String tempFileName = file.getName();
byte[] fileBuff = FileUtil.getBytesFromFile(file);
String fileId = "";
//截取后缀
String fileExtName = tempFileName.substring(tempFileName.lastIndexOf(".") + 1);
ConfigAndConnectionServer configAndConnectionServer = new ConfigAndConnectionServer().invoke(1);
StorageClient1 storageClient1 = configAndConnectionServer.getStorageClient1();
storageServer = configAndConnectionServer.getStorageServer();
trackerServer = configAndConnectionServer.getTrackerServer();
/**
* 4.设置文件的相关属性。调用客户端的upload_file1的方法上传文件
*/
NameValuePair[] metaList = new NameValuePair[3];
//原始文件名称
metaList[0] = new NameValuePair("fileName", tempFileName);
//文件后缀
metaList[1] = new NameValuePair("fileExtName", fileExtName);
//文件大小
metaList[2] = new NameValuePair("fileLength", String.valueOf(file.length()));
//开始上传文件
fileId = storageClient1.upload_file1(fileBuff, fileExtName, metaList);
retMap = handleResult(retMap, fileId);
} catch (Exception e) {
e.printStackTrace();
retMap.put("code", "0002");
retMap.put("msg", e.getMessage());
}finally {
/**
* 5.关闭跟踪服务器的连接
*/
colse(storageServer, trackerServer);
}
} else {
retMap.put("code", "0001");
retMap.put("msg", "error:本地文件不存在!");
}
return retMap;
}
/**
* Description:远程选择上传文件-通过MultipartFile
*
* @param file 文件流
* @return Map<String,Object> code-返回代码, group-文件组, msg-文件路径/错误信息
*/
public static Map<String, Object> upload(MultipartFile file) {
Map<String, Object> retMap = new HashMap<String, Object>();
TrackerServer trackerServer = null;
StorageServer storageServer = null;
try {
if (file.isEmpty()) {
retMap.put("code", "0001");
retMap.put("msg", "error:文件为空!");
} else {
ConfigAndConnectionServer configAndConnectionServer = new ConfigAndConnectionServer().invoke(1);
StorageClient1 storageClient1 = configAndConnectionServer.getStorageClient1();
storageServer = configAndConnectionServer.getStorageServer();
trackerServer = configAndConnectionServer.getTrackerServer();
String tempFileName = file.getOriginalFilename();
//设置元信息
NameValuePair[] metaList = new NameValuePair[3];
//原始文件名称
metaList[0] = new NameValuePair("fileName", tempFileName);
//文件后缀
byte[] fileBuff = file.getBytes();
String fileId = "";
//截取后缀
String fileExtName = tempFileName.substring(tempFileName.lastIndexOf(".") + 1);
metaList[1] = new NameValuePair("fileExtName", fileExtName);
//文件大小
metaList[2] = new NameValuePair("fileLength", String.valueOf(file.getSize()));
/**
* 4.调用客户端呢的upload_file1的方法开始上传文件
*/
fileId = storageClient1.upload_file1(fileBuff, fileExtName, metaList);
retMap = handleResult(retMap, fileId);
}
} catch (Exception e) {
retMap.put("code", "0002");
retMap.put("msg", "error:文件上传失败!");
}finally {
/**
* 5.关闭跟踪服务器的连接
*/
colse(storageServer, trackerServer);
}
return retMap;
}
/**
* 下载文件
*
* @param response
* @param filepath 数据库存的文件路径
* @param downname 下载后的名称
* filepath M00/开头的文件路径
* group 文件所在的组 如:group0
* @throws IOException
*/
public static void download(HttpServletResponse response, String group, String filepath, String downname) {
StorageServer storageServer = null;
TrackerServer trackerServer = null;
try {
ConfigAndConnectionServer configAndConnectionServer = new ConfigAndConnectionServer().invoke(0);
StorageClient storageClient = configAndConnectionServer.getStorageClient();
storageServer = configAndConnectionServer.getStorageServer();
trackerServer = configAndConnectionServer.getTrackerServer();
/**
*4.调用客户端的下载download_file的方法
*/
byte[] b = storageClient.download_file(group, filepath);
if (b == null) {
logger.error("Error1 : file not Found!");
response.getWriter().write("Error1 : file not Found!");
} else {
logger.info("下载文件..");
downname = new String(downname.getBytes("utf-8"), "ISO8859-1");
response.setHeader("Content-Disposition", "attachment;fileName=" + downname);
OutputStream out = response.getOutputStream();
out.write(b);
out.close();
}
} catch (Exception e) {
e.printStackTrace();
try {
response.getWriter().write("Error1 : file not Found!");
} catch (IOException e1) {
e1.printStackTrace();
}
}finally {
/**
* 5.关闭跟踪服务器的连接
*/
colse(storageServer, trackerServer);
}
}
/**
* 删除文件
*
* @param group 文件分组, filepath 已M00/ 开头的文件路径
* @return Map<String,Object> code-返回代码, msg-错误信息
*/
public static Map<String, Object> delete(String group, String filepath) {
Map<String, Object> retMap = new HashMap<String, Object>();
StorageServer storageServer = null;
TrackerServer trackerServer = null;
try {
ConfigAndConnectionServer configAndConnectionServer = new ConfigAndConnectionServer().invoke(0);
StorageClient storageClient = configAndConnectionServer.getStorageClient();
storageServer = configAndConnectionServer.getStorageServer();
trackerServer = configAndConnectionServer.getTrackerServer();
/**
* 4.调用客户端的delete_file方法删除文件
*/
int i = storageClient.delete_file(group, filepath);
if (i == 0) {
retMap.put("code", "0000");
retMap.put("msg", "删除成功!");
} else {
retMap.put("code", "0001");
retMap.put("msg", "文件不存在!");
}
} catch (Exception e) {
e.printStackTrace();
retMap.put("code", "0002");
retMap.put("msg", "删除失败!");
} finally {
/**
* 5.关闭跟踪服务器的连接
*/
colse(storageServer, trackerServer);
}
return retMap;
}
/**
* 关闭服务器
*
* @param storageServer
* @param trackerServer
*/
private static void colse(StorageServer storageServer, TrackerServer trackerServer) {
if (storageServer != null && trackerServer != null) {
try {
storageServer.close();
trackerServer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理上传到文件服务器之后,返回来的结果
*
* @param retMap
* @param fileId
* @return
*/
private static Map<String, Object> handleResult(Map<String, Object> retMap, String fileId) {
if (!fileId.equals("") && fileId != null) {
retMap.put("code", "0000");
retMap.put("group", fileId.substring(0, 6));
retMap.put("msg", fileId.substring(7, fileId.length()));
} else {
retMap.put("code", "0003");
retMap.put("msg", "error:上传失败!");
}
return retMap;
}
/**
* @项目名称:lidong-dubbo
* @类名:FastDFSUtil
* @类的描述: ConfigAndConnectionServer
* @作者:lidong
* @创建时间:2017/2/7 上午8:47
* @公司:chni
* @QQ:1561281670
* @邮箱:lidong1665@163.com
*/
private static class ConfigAndConnectionServer {
private TrackerServer trackerServer;
private StorageServer storageServer;
private StorageClient storageClient;
private StorageClient1 storageClient1;
public TrackerServer getTrackerServer() {
return trackerServer;
}
public StorageServer getStorageServer() {
return storageServer;
}
public StorageClient getStorageClient() {
return storageClient;
}
public StorageClient1 getStorageClient1() {
return storageClient1;
}
public ConfigAndConnectionServer invoke(int flag) throws IOException, MyException {
/**
* 1.读取fastDFS客户端配置文件
*/
ClassPathResource cpr = new ClassPathResource("fdfs_client.conf");
/**
* 2.配置文件的初始化信息
*/
ClientGlobal.init(cpr.getClassLoader().getResource("fdfs_client.conf").getPath());
TrackerClient tracker = new TrackerClient();
/**
* 3.建立连接
*/
trackerServer = tracker.getConnection();
storageServer = null;
/**
* 如果flag=0时候,构造StorageClient对象否则构造StorageClient1
*/
if (flag == 0) {
storageClient = new StorageClient(trackerServer, storageServer);
} else {
storageClient1 = new StorageClient1(trackerServer, storageServer);
}
return this;
}
}
}

4.5 SpringMVC 上传文件到Fastdfs文件服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RequestMapping("/upload")
public String addUser(@RequestParam("file") CommonsMultipartFile[] files,
HttpServletRequest request){
for(int i = 0;i<files.length;i++){
logger.info("fileName-->" + files[i].getOriginalFilename()+" file-size--->"+files[i].getSize());
Map<String, Object> retMap = FastDFSUtil.upload(files[i]);
String code = (String) retMap.get("code");
String group = (String) retMap.get("group");
String msg = (String) retMap.get("msg");
if ("0000".equals(code)){
logger.info("文件上传成功");
//TODO:将上传文件的路径保存到mysql数据库
}else {
logger.info("文件上传失败");
}
}
return "/success";
}

基本上就这么多。大家在学习的过程中如果遇到问题。可以直接在下面评论、吐槽。

代码地址