第一章 FastDFS入门
1.1 分布式文件系统
分布式文件系统是一个软件/软件服务器,这个软件管理的文件通常不是在一个服务器节点上,而是在多个服务器节点上,这些服务器节点通过网络相连构成一个庞大的文件存储服务器集群,这些服务器都用于存储文件资源,通过分布式文件系统来管理这些服务器上的文件。
常见的DFS有:FastDFS,GFS,HDFS,Lustre,Ceph,GridFS,mogileFS,TFS等。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JzQySTLn-1666595816577)(C:\Users\ADMINI~1\AppData\Local\Temp\WeChat Files\0dbff8dfe04b1b87496ffece6de7422.jpg)]
1.2 FastDFS简介
FastDFS是Alibaba开发的,C语言开发的。
FastDFS对文件进行管理,功能包括:文件存储,文件同步,文件访问(文件上传,文件下载,文件删除)等,解决了大容量文件存储的问题。
FastDFS文件系统由两大部分组成,一个是客户端,一个是服务器。
服务端由两个部分组成,一个是跟踪器tracker,一个是存储节点storage。
跟踪器主要做调度工作,在内存中记录集群中存储节点storage的状态信息。
存储节点用于存储文件,包括文件和文件属性(meta data) 都保存到存储服务器磁盘上,完成文件管理的所有功能:文件存储,文件同步,文件访问等等。
1.3 FastDFS 安装
-
首先需要依赖
gcc libevent libevent-devel #centos中,查看是否安装 yum list installed | grep gcc yum list installed|grep libevent yum list installed|grep libevent-devel #如果没安装,则需要安装 yum install gcc libevent libevent-devel -y
-
从github上下载fastdfds,一共四个文件
fastdfs-5.11.tar.gz #fastdfs fastdfs-client-java-master.zip #源码 fastdfs-nginx-module-master.zip #nginx扩展模块 libfastcommon-1.0.36.tar.gz #公共库 首先要安装第四个文件,然后安装第一个文件 #在程序目录下 ./make.sh #编译 ./make.sh install #安装 #安装结束后 #fastdfs的所有配置文件都在/etc/fdfs目录下,里面一共有四个文件 client.conf.sample #客户端连接文件 storage.conf.sample #storage server storage_ids.conf.sample #记录id用的 tracker.conf.sample #tracker.server #将fastdfs安装目录下的conf中的http.conf mime.types拷贝到/etc/fdfs中
1.4 FastDFS配置和启动
主要配置两个配置文件:tracker.conf和storage.conf
tracker.conf
# the tracker server port
port=22122
# the base path to store data and log files
#path must exists
base_path=/opt/fastdfs/tracker
storage.conf
# comment or remove this item for fetching from tracker server,
# in this case, use_storage_id must set to true in tracker.conf,
# and storage_ids.conf must be configed correctly.
group_name=group1
# the storage server port
port=23000
# the base path to store data and log files
base_path=/opt/fastdfs/storage
# store_path#, based 0, if store_path0 not exists, it's value is base_path
# the paths must be exist
#存放文件的磁盘路径
store_path0=/opt/fastdfs/storage/files
#store_path1=/home/yuqing/fastdfs2
# tracker_server can ocur more than once, and tracker_server format is
# "host:port", host can be hostname or ip address
tracker_server=192.168.213.133:22122
#启动fastdfs tracker
fdfs_trackerd <config_file> [start | stop | restart]
#启动fastdfs storage
fdfs_storaged <config_file> [start | stop | restart]
1.5 fastdfs文件处理
启动后,在之前storage设置的files文件夹下会生成data文件夹,其中有256个文件夹,每个文件夹下又有256个文件夹,fastdfs就用这65536个文件夹来存储文件,之所以创建这么多文件夹是因为如果都放在一个文件夹中,读取会变慢
#测试fdfs上传文件
fdfs_test <config_file> <operation>
operation:upload,download,getmeta,setmeta,delete and query_servers
#在上传前,需要设置client.conf的日志存放位置和里面的ip地址。
fdfs_test /etc/fdfs/client.conf upload aa.txt
#上传后的打印信息
tracker_query_storage_store_list_without_group:
server 1. group_name=, ip_addr=192.168.213.133, port=23000
group_name=group1, ip_addr=192.168.213.135, port=23000
storage_upload_by_filename
#文件上传到哪个组中,组决定存到哪个机器中去,远程文件名称决定存放在哪个磁盘目录下,M00/00/00指第一块磁盘的00文件夹的00文件夹下
group_name=group1, remote_filename=M00/00/00/wKjVhWKRgrWAXSwdAAAAAAAAAAA352.txt
source ip address: 192.168.213.133
file timestamp=2022-05-27 19:02:29
file size=0
file crc32=0
example file url: http://192.168.213.133/group1/M00/00/00/wKjVhWKRgrWAXSwdAAAAAAAAAAA352.txt
storage_upload_slave_by_filename
group_name=group1, remote_filename=M00/00/00/wKjVhWKRgrWAXSwdAAAAAAAAAAA352_big.txt
source ip address: 192.168.213.133
file timestamp=2022-05-27 19:02:29
file size=0
file crc32=0
example file url: http://192.168.213.133/group1/M00/00/00/wKjVhWKRgrWAXSwdAAAAAAAAAAA352_big.txt
#下载文件
fdfs_test <config_file> download <group_name> <remote_filename>
#会下载到当前目录下
fdfs_test /etc/fdfs/client.conf download group1 wKjVhWKRgrWAXSwdAAAAAAAAAAA352.txt
第二章 fastDFS整合nginx
fastDFS提供了一个nginx扩展模块,利用该模块,可以通过Nginx访问已经上传到fastDFS的文件。
fastdfs-nginx-module-master.zip #扩展模块
因为这个模块必须要在Nginx的安装过程中才能添加,所以需要重新安装nginx,为了和原来的nginx区分,取名为nginx_fdfs
2.1 安装整合了fastDFS的Nginx
-
安装nginx时,使用如下配置
#在nginx的解压缩目录下,执行上述配置 ./configure --prefix=/usr/local/nginx_fdfs --add-module=/home/sunshine/software/fastdfs-nginx-module-master/src
-
安装完成后,配置nginx
将/home/sunshine/software/fastdfs-nginx-module-master/src目录下的mod_fastdfds.conf文件拷贝到/etc/fdfs/目录下,才能正常启动nginx 修改配置文件mod_fastdfs.conf # the base path to store log files base_path=/opt/fastdfs/nginx_mod # FastDFS tracker_server can ocur more than once, and tracker_server format is # "host:port", host can be hostname or ip address # valid only when load_fdfs_parameters_from_tracker is true tracker_server=192.168.213.133:22122 # if the url / uri including the group name # set to false when uri like /M00/00/00/xxx # set to true when uri like ${group_name}/M00/00/00/xxx, such as group1/M00/xxx # default value is false url_have_group_name = true # path(disk or mount point) count, default value is 1 # must same as storage.conf store_path_count=1 # store_path#, based 0, if store_path0 not exists, it's value is base_path # the paths must be exist # must same as storage.conf store_path0=/opt/fastdfs/storage/files #store_path1=/home/yuqing/fastdfs1 配置nginx的配置文件 location ~/group[1-9]/M[0-9]{ ngx_fastdfs_module; }
-
启动nginx
ngx_http_fastdfs_set pid=9337 #这句话表示扩展模块启动成功-·················································································
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4J2UCtke-1666595816578)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220528112337265.png)]
2.2 通过java程序处理文件
2.2.1 上传文件
public static void upload(){
TrackerServer ts = null;
StorageServer ss = null;
try {
//读取fastdfs的配置文件用于将所有的tracker的地址读取到内存中
ClientGlobal.init("fastdfs.conf");
TrackerClient tc = new TrackerClient();
ts = tc.getConnection();
ss = tc.getStoreStorage(ts);
//定义Storage的客户端对象,需要使用这个对象来完成具体的文件上传下载和删除操作
StorageClient sc = new StorageClient(ts,ss);
/*文件上传
* 参数1:为需要上传文件的绝对路径
* 参数2:需要上传文件的扩展名
* 参数3:文件的属性文件,通常不上传
* 返回一个String数组,这个数据十分重要,建议存入数据库
* */
String[] result = sc.upload_file("D:\\java\\java_study\\spring_framework\\springsession\\ch03-fastdfs-java\\src\\main\\resources\\test.jpg","jpg",null);
/*返回值是个数组,
* 数组有两个元素
* 第一个元素是文件所在的组名
* 第二个元素是文件所在的远程路径名称
* */
for(String str:result){
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
} catch (MyException e) {
e.printStackTrace();
}
finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
ts.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.2.2 下载文件
private static void download() {
TrackerServer ts = null;
StorageServer ss = null;
try {
//读取fastdfs的配置文件用于将所有的tracker的地址读取到内存中
ClientGlobal.init("fastdfs.conf");
TrackerClient tc = new TrackerClient();
ts = tc.getConnection();
ss = tc.getStoreStorage(ts);
//定义Storage的客户端对象,需要使用这个对象来完成具体的文件上传下载和删除操作
StorageClient sc = new StorageClient(ts,ss);
/*
* 文件下载
* 参数1:文件所在组名
* 参数2:文件远程文件名
* 参数3:需要保存的本地文件名称
* 返回一个int类型的数据,返回0表示文件下载成功。其他值都表示失败。
* */
String group_name = "group1";
String remote_filename = "M00/00/00/wKjVhWKYFMCAZDG1AAAGcNcjGq8790.jpg";
String local_filename = "D:\\java\\java_study\\spring_framework\\springsession\\ch03-fastdfs-java\\src\\main\\resources\\test_download.jpg";
int result = sc.download_file(group_name,remote_filename,local_filename);
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
} catch (MyException e) {
e.printStackTrace();
}
finally {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
ts.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.2.3 文件删除
private static void delete(){
TrackerServer ts = null;
StorageServer ss = null;
try {
ClientGlobal.init("fastdfs.conf");
TrackerClient tc = new TrackerClient();
ts = tc.getConnection();
ss = tc.getStoreStorage(ts);
StorageClient sc = new StorageClient(ts,ss);
/*
* 文件删除:
* 参数1:文件所在组名
* 参数2:文件远程文件名
* 返回值:0删除成功,其他值删除失败
* */
String group_name = "group1";
String remote_filename = "M00/00/00/wKjVhWKYFMCAZDG1AAAGcNcjGq8790.jpg";
int result = sc.delete_file(group_name,remote_filename);
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
} catch (MyException e) {
e.printStackTrace();
}
}
2.3 热部署插件GeneratorMapper.xml
<!--pom文件逆向工程插件-->
<plugin>
<!--mybatis代码自动生成插件-->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.6</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
</dependencies>
<configuration>
<!--配置文件的位置-->
<configurationFile>GeneratorMapper.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
</plugin>
GeneratorMapper.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- 指定连接数据库的 JDBC 驱动包所在位置,指定到你本机的完整路径 -->
<classPathEntry location="C:\Users\Administrator\.m2\repository\mysql\mysql-connector-java\8.0.29\mysql-connector-java-8.0.29.jar
"/>
<!-- 配置 table 表信息内容体,targetRuntime 指定采用 MyBatis3 的版本 -->
<context id="tables" targetRuntime="MyBatis3">
<!-- 抑制生成注释,由于生成的注释都是英文的,可以不让它生成 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<!-- 配置数据库连接信息 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://192.168.213.135:3306/fastdfs?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8"
userId="root"
password="syp960722..">
</jdbcConnection>
<!-- 生成 model 类,targetPackage 指定 model 类的包名, targetProject 指定
生成的 model 放在 idea 的哪个工程下面-->
<javaModelGenerator targetPackage="com.bjpowernode.model"
targetProject="src/main/java">
<property name="enableSubPackages" value="false"/>
<property name="trimStrings" value="false"/>
</javaModelGenerator>
<!-- 生成 MyBatis 的 Mapper.xml 文件,targetPackage 指定 mapper.xml 文件的
包名, targetProject 指定生成的 mapper.xml 放在 idea 的哪个工程下面 -->
<sqlMapGenerator targetPackage="mapper"
targetProject="src/main/resources">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!-- 生成 MyBatis 的 Mapper 接口类文件,targetPackage 指定 Mapper 接口类的包
名, targetProject 指定生成的 Mapper 接口放在 eclipse 的哪个工程下面 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.bjpowernode.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<!-- 数据库表名及对应的 Java 模型类名 -->
<table tableName="creditor_info" domainObjectName="CreditorInfo"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"
enableSelectByExample="false"
selectByExampleQueryId="false"/>
</context>
</generatorConfiguration>
最后执行生成
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-39Sf716Z-1666595816579)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220614102620913.png)]
2.4 上传文件
@Controller
public class CreditorController {
@Resource
private CreditorService creditorService;
@RequestMapping("/")
public String creditors(Model model){
List<CreditorInfo> list = creditorService.selectAll();
model.addAttribute("creditorList",list);
return "creditors";
}
@GetMapping("/upload/{id}")
public String toUpload(@PathVariable Integer id,Model model){ //通过 @PathVariable 可以将 URL 中占位符参数传入方法中
CreditorInfo creditorInfo = creditorService.selectById(id);
model.addAttribute("creditorInfo",creditorInfo);
return "upload";
}
/**
* 文件上传
* 参数MultipartFile 为spring提供的一个类,专门用于封装请求中的文件数据
* 属性名必须与表单中文件域的名字完全相同否则无法获取文件数据
* */
@PostMapping("/upload")
public String upload(Integer id, MultipartFile myfile, Model model) throws IOException {
// System.out.println(myfile.getBytes());//获取文件对应字节数组
// System.out.println(myfile.getInputStream());//获取文件对应输入流
// System.out.println(myfile.getContentType());//获取文件类型
// System.out.println(myfile.getName());//获取表单元素名
// System.out.println(myfile.getOriginalFilename());//获取文件名
// System.out.println(myfile.getSize());//获取文件大小
// System.out.println(myfile.isEmpty());//判断文件是否为空,如果没有上传文件或文件大小为0,这个值都为true
//获取文件对应字节数组
byte[] buffFile = myfile.getBytes();
//获取文件名
String fileName = myfile.getOriginalFilename();
//获取文件扩展名,但有的文件可能没有文件扩展名,需要做逻辑判断
String fileExtrName = fileName.substring(fileName.lastIndexOf(".")+1);
String[] result = FastDFSUtil.upload(buffFile,fileExtrName);
System.out.println(result);
CreditorInfo creditorInfo = new CreditorInfo();
creditorInfo.setId(id);
creditorInfo.setFileSize(myfile.getSize());
creditorInfo.setFileType(myfile.getContentType());
creditorInfo.setOldFilename(myfile.getOriginalFilename());
creditorInfo.setGroupName(result[0]);
creditorInfo.setRemoteFilepath(result[1]);
creditorService.updateFileInfo(creditorInfo);
//上传成功后页面不刷新
model.addAttribute("message","文件上传成功,点击确定返回列表界面");
model.addAttribute("url","/");
return "success";
}
}
<!--creditors页面-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<table>
<tr>
<td>序号</td>
<td>姓名</td>
<td>性别</td>
<td>电话</td>
<td>地址</td>
<td></td>
</tr>
<tr th:each="creditor:${creditorList}">
<td th:text="${creditorStat.count}">序号</td>
<td th:text="${creditor.realName}">姓名</td>
<td th:text="${creditor.sex==1?'男':'女'}">性别</td>
<td th:text="${creditor.phone}">电话</td>
<td th:text="${creditor.address}">地址</td>
<td>
<span th:if="${creditor.remoteFilepath==null||creditor.remoteFilepath==''}">
<a th:href="@{|/upload/${creditor.id}|}">上传</a>
</span>
<span th:if="${creditor.remoteFilepath!=null&&creditor.remoteFilepath!=''}">
<a th:href="@{|/download|}">下载</a>
<a th:href="@{|/delete|}">删除</a>
</span>
</td>
</tr>
</table>
</body>
</html>
<!--success页面-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<script>
if(confirm("[[${message}]]")){ //这一部分作用如下图所示
window.top.location.href="[[${url}]]"
}
</script>
</head>
<body>
</body>
</html>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UWG29syg-1666595816579)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220621110653832.png)]
<!--upload页面-->
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<!--上传文件的表单,method必须为post enctype必须指定为multipart/form-data,让表单以二进制的方式进行传递-->
<form th:action="@{|/upload|}" target="success" method="post" enctype="multipart/form-data">
姓名:<span th:text="${creditorInfo.realName}"></span><br>
性别:<span th:text="${creditorInfo.sex==1?'男':'女'}"></span><br>
电话:<span th:text="${creditorInfo.phone}"></span><br>
地址:<span th:text="${creditorInfo.address}"></span><br>
文件:<input type="file" name="myfile">
<input type="hidden" name="id" th:value="${creditorInfo.id}"><br>
<input type="submit" value="上传文件">
</form>
<iframe name="success" style="display: none"></iframe>
</body>
</html>
2.5 下载文件
@RequestMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable Integer id){
/*
* 创建响应实体对象,spring会将这个对象返回给浏览器,作为响应数据
* 参数1 为响应的具体数据
* 参数2 为响应时的头文件信息
* 参数3 为响应时的状态码
* */
CreditorInfo creditorInfo = creditorService.selectById(id);
byte[] bufferFile = FastDFSUtil.download(creditorInfo.getGroupName(),creditorInfo.getRemoteFilepath());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);//设置响应类型为文件类型
headers.setContentLength(creditorInfo.getFileSize());//设置响应时的文件大小用于自动提供下载进度
headers.setContentDispositionFormData("attachment",creditorInfo.getOldFilename());//设置下载时的默认文件名
System.out.println(creditorInfo.getOldFilename());
ResponseEntity<byte[]> responseEntity = new ResponseEntity<byte[]>(bufferFile,headers,HttpStatus.OK);
return responseEntity;//会自动下载
}
2.6 删除文件
@RequestMapping("/delete/{id}")
public String delete(@PathVariable Integer id){
creditorService.deleteFileById(id);
return "redirect:/";
}
@Override
public void deleteFileById(Integer id) {
CreditorInfo creditorInfo = creditorInfoMapper.selectByPrimaryKey(id);
FastDFSUtil.delete(creditorInfo.getGroupName(),creditorInfo.getRemoteFilepath());
creditorInfo.setGroupName("");
creditorInfo.setRemoteFilepath("");
creditorInfo.setOldFilename("");
creditorInfo.setFileType("");
creditorInfo.setFileSize(0L);
creditorInfoMapper.updateByPrimaryKeySelective(creditorInfo);
}
ava
@RequestMapping("/delete/{id}")
public String delete(@PathVariable Integer id){
creditorService.deleteFileById(id);
return "redirect:/";
}
@Override
public void deleteFileById(Integer id) {
CreditorInfo creditorInfo = creditorInfoMapper.selectByPrimaryKey(id);
FastDFSUtil.delete(creditorInfo.getGroupName(),creditorInfo.getRemoteFilepath());
creditorInfo.setGroupName("");
creditorInfo.setRemoteFilepath("");
creditorInfo.setOldFilename("");
creditorInfo.setFileType("");
creditorInfo.setFileSize(0L);
creditorInfoMapper.updateByPrimaryKeySelective(creditorInfo);
}