爆肝两万字,详解fastdfs分布式文件系统

1.学习目标

image.png

2.简介

技术论坛:http://bbs.chinaunix.net/forum-240-1.html
资源地址:https://sourceforge.net/projects/fastdfs/
源码地址:https://github.com/happyfish100

  1. FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
  2. FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
  3. FastDFS服务端有两个角色:跟踪器(tracker)和存储节点(storage)。跟踪器主要做调度工作,在访问上起负载均衡的作用。
  4. 存储节点存储文件,完成文件管理的所有功能:就是这样的存储、同步和提供存取接口,FastDFS同时对文件metadata进行管理。所谓文件的metadata就是文件的相关属性,以键值对(key value)方式表示,如:width=1024,其中的key为width,value为1024。文件metadata是文件属性列表,可以包含多个键值对。
  5. 跟踪器和存储节点都可以由一台或多台服务器构成。跟踪器和存储节点中的服务器均可以随时增加或下线而不会影响线上服务。其中跟踪器中的所有服务器都是对等的,可以根据服务器的压力情况随时增加或减少。
  6. 为了支持大容量,存储节点(服务器)采用了分卷(或分组)的组织方式。存储系统由一个或多个卷组成,卷与卷之间的文件是相互独立的,所有卷的文件容量累加就是整个存储系统中的文件容量。一个卷可以由一台或多台存储服务器组成,一个卷下的存储服务器中的文件都是相同的,卷中的多台存储服务器起到了冗余备份和负载均衡的作用
  7. 在卷中增加服务器时,同步已有的文件由系统自动完成,同步完成后,系统自动将新增服务器切换到线上提供服务。当存储空间不足或即将耗尽时,可以动态添加卷。只需要增加一台或多台服务器,并将它们配置为一个新的卷,这样就扩大了存储系统的容量。

FastDFS中的文件标识分为两个部分:卷名和文件名,二者缺一不可。

2.1.架构图

image.png
解释:
写入
假设我现在client要上传文件,我要找到跟踪器tracker,tracker找到client,然后tracker找到存储节点,看看存储节点那个卷下面的节点比较空闲,能放得下这个文件,然后写入进去,生成一个文件名
读取
如果我们client要下载文件,不需要与tracker再做交互,直接与storage打交道,根据我们当时上传文件tracker给我们提供的文件名,我们加上服务器名字和端口号再加上完整的文件名即可读取下载成功
主备切换
我们storage存储节点是由多个卷构成的一个大的集群,比方说c d e盘构成一个硬盘,每个卷又分成主和子,他们两者文件类型一致,如果主服务器崩了,子服务器马上可以顶上

2.2.上传流程

image.png

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

2.3.下载流程

image.png

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

client可以直接去到Storage进行在线的读取和下载,这个在线读取和下载前提是我们知道它的一个ip地址和端口号,后面跟上卷名,再跟上文件名,我们如果知道这个完整路径的话,可以直接去到我们存储节点进行在线预览,或者是在线下载;如果我们现在只知道一个卷名和文件名,我们并不知道ip和端口,我们也可以去找我们的tracker,拿着我们的卷名和文件名去找我们的跟踪器,跟踪器就会找到对应的卷名和文件名所在的节点,会把这个存储节点的ip地址和端口号返回给我们的客户端,然后我们client再次通过我们的ip端口卷名文件名,然后直接通过我们的存储节点进行我们的读取和下载操作,一般我们如果考虑效率问题的话,肯定是我们直接拿着ip端口卷名文件名直接去存储节点,读取我们的一个文件,如果实在是不知道ip和端口情况下,可能就需要通过我们tracker,但一般情况下,我们tracker上传的时候,我们tracker会返回一个完整的卷名和文件名加ip和端口,我们一般呢会把返回的相对路径存放到数据库里面去,那我们需要进行文件预览和下载的时候呢,我们一般从数据库拿到我们带有ip和端口的卷名和文件名这一整串信息的数据直接去storage进行一个下载 ,效率更高一点!!!

2.4.术语介绍

  1. TrackerServer:跟踪服务器,主要做调度工作,在访问上起负载均衡的作用,记录storage server的状态,是连接Client和Storage server的枢纽。
  2. Storage Server:存储服务器,文件和meta data都保存到存储服务器上
  3. group:组,也称为卷,同组内服务器上的文件是完全相同的【主崩子接】
  4. 文件标识:包括两部分:组名和文件名(包含路径)
  5. meta data:文件相关属性,键值对(Key Value Pair)方式,如:width1024,height=768

2.5.同步机制

  1. 同一组内的storage server之间是对等的,文件上传、删除等操作可以在任意一台storage server上进行;【比方我们client去进行写操作,会根据tracker自己去调度,假设三台服务器,会根据tracker调度结果,返回里面任意一台的IP地址和端口,主要是看storage那一台服务器符合调度的一个规则,比方说那一台storage它现在是空闲的,那一台storage它的一个剩余磁盘容量能放下这个文件,它返回的并不一定是某一台的ip地址,可能是三台里面随机选一个ip地址或者端口进行返回】
  2. 文件同步只在组内的storage server之间进行,采用push方式,即源服务器同步给目标服务器;【采用广播方式,比如说我们现在是一个高并发,每一台都正好在进行写操作,传统的我们一个服务器新增了数据量,它们之间要进行一个相互通信,一直通信到最后一个服务器,再进行相应的数据同步,表示我这里现在新增了数据,你和我同步一下,这是传统的,我们发现它们相互通信的次数非常频繁;所以我们一般采用广播模式,比方说我们某个服务器写了一条数据,它就会广播告诉其它服务器,表示我这里新增了数据,你们也新增一下;】
  3. 源头数据才需要同步,备份数据不需要再次同步,否则就构成环路了;【被写入数据称为源服务器;我们源头数据才需要进行一个同步,如果是备份数据不需要广播同步,一直不停广播就形成环路了!!!】
  4. 上述第二条规则有个例外,就是新增加一台storage server时,由已有的一台storage server将已有的所有数据(包括源头数据和备份数据)同步给新增服务器 【假设我们有三台服务器,增加了一台,加的这一台就会从我们原有的三台里面,随机选一台作为一个源头数据,然后就会把这个源头数据和我们的备份数据同步到新增的服务器里面】

2.6.FastDFS运行时目录结构

2.6.1.Tracker Server目录

2.7.FastDFS和其它文件存储的简单对比

2.7.1.FastDFS和集中存储方式对比

指标 FastDFS NFS 集中存储设备加 NetApp、NAS
线性扩容性 高【扩容好】
文件高并发访问性能 高【速度快】 一般
文件访问方式 专用API【有自己的AIP】 POSIX【可移植操作系统接口】 POSIX
硬件成本 较低【成本低】 中等【硬盘成本高】 高【硬盘成本高】
相同内容文件只保存一份 支持【相同文件只有只保留一份】 不支持 不支持

2.7.2.FastDFS和mogileFS对比

指标 FastDFS mogileFS
系统简洁性 简洁 只有两个角色:tracker和storage 一般有三个角色:trocker、storage和存储文件信息的mysql db
系统性能 很高(没有使用数据库,文件同步直接点对点,不经过tracker中转) 高(使用mysql来存储文件索引信息,文件同步通过tracker调度和中转)
系统稳定性 高(C语言开发,可以支持高并发和高负载) 一般(Perl语言开发,高并发和高负载支持一般)【没有C语言高并发高负载好】
RAID方式 分组(组内冗余),灵活性较大 动态冗余,灵活性一般
通信协议 专用协议,下载文件支持http【专用协议最大的好处就是写的操作效率跟高,在tcp/ip之上】 http
技术文档 较详细 较少
文件附加属性(meta data) 支持【文件相关属性存储在storage】 不支持
相同内容只保存一份 支持【根据文件相关属性判断,如果属性一致则覆盖】 不支持
下载文件时支持文件偏移量【断点续传, 从指定位置向前向后移动的字节数,比如我们下载一个文件一半点暂停,然后再点开始会从你已经下载好的进度开始,而有的文件你点了暂停可能就让你从头开始下载】 支持 不支持

有没有比FastDFS更好的呢?当然有,那就是我们的hdfs,hdfs更多的是大数据方面去用,它的性能会比FastDFS更好一点

3.安装

3.1.安装简介

FastDFS主要是两个角色,一个tracker和storage,它们本质上都是一个FastDFS一个包,它们通过对应不同配置来确认它们不同的角色,所以它们通用的安装都是FastDFS的安装包
我们这边也准备了两台服务器,一台是安装我们tracker,一台安装storage,它们只是对应角色配置不一样
image.png
如果电脑配置比较差的话,也可以直接安装在一台服务器上,只需修改对应角色配置即可,安装包都是一样,也可以实现!!!

3.2.FastDFS安装包

上传所选安装包

  1. 第一个就是fastdfs安装包
  2. 第二个对应client安装包
  3. 第三个是nginx模块包
  4. 第四个就是fastdfs公用的库
  5. 第五个fastdfs和nginx整合

image.png

3.3.安装依赖

3.3.1.安装c++相关依赖

我们fastdfs是根据C语言进行开发的,所以我们需要安装C++相关依赖

yum -y install cmake gcc-c++

image.png

3.3.2.安装fastdfs核心库

安装完成之后,我们还需要安装我们fastdfs核心库
image.png
这个核心库呢其实是从fastdfs和fastdht中提取出来的公用的C函数的库,fastdfs和fastdht是同样一个作者去写的两个产品,都是C语言编写,然后里面会有一些公用函数库,作者把它提取出来当成一个专门核心库
如果我们后缀名是.zip,需要安装一个zip解压插件

yum -y install unzip

image.png
我们把需要安装的fastdfs所以文件放在一个文件夹方便管理

mkdir -p /usr/local/fastdfs

然后我们就可以去解压了
zip后缀

unzip libfastcommon-1.0.43.zip

tar后缀

tar -zxvf libfastcommon-1.0.43.zip

image.png
我们进行之后看见有一个make.sh执行脚本,我们就通过这个脚本去进行一个编译和安装
image.png

.make.sh

这样就会进行编译
image.png
编译好后我们就可以进行安装了

.make.sh install

image.png
还有一个问题就是我们fastdfs主程序的lib目录是在/usr/local/lib下面的,所以我们需要去创建一些软链接,这些软链接就相当于快捷方式,把它的一个快捷方式从原本指向的目录改成我们想要的指向的目录
这个意思相当把前面原本指向的路径指定到我们后面需要我们指定路径

ln -s /usr/1ib64/1ibfastcommon.so /usr/local/lib/libfastcommon.so
ln -s /usr/local/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so
ln -s /usr/local/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so

3.3.3.安装fastdfs

image.png

tar -zxvf fastdfs-6.06.tar.gz

我们发现这里也有make.sh
image.png
我们可以做一个可选的操作,就是说我们可以把它的一个路径改成指定的路径,因为它默认安装路径是在/usr下面,我们可以把它改成/usr/local下面去,当然在集群下面就不要去改,那我们现在是安装的一个单节点,可以去尝试的改一下
我们进入fastdfs

vim make.sh

我们搜索/TAGE_PREFIX
image.png
我们把它改到/usr/local
image.png
然后我们再次去安装

./make.sh 先编译

image.png
再安装

./make.sh install

安装后,FastDFS主程序所在的位置是:

  • /usr/local/bin可执行文件所在位置。默认安装在/usr/bin中。
  • /etc/fdfs配置文件所在位置。就是默认位置。
  • /usr/1oca1/1ib64主程序代码所在位置。默认在usr/bin中。
  • /usr/local/irclude/fastdfs包含的一些插件组所在位置。默认在/usr/include/fastdfs中。

image.png
安装好了之后,我们可以去看一下服务脚本所在位置

cd /etc/init.d/

image.png
然后我们可以看一下我们配置文件的模板所在位置

cd /etc/fdfs/

image.png
如果我们这台服务器当做tracker来用的话,只需拷贝tracker配置拷贝过去进行修改,把后面的.sample删掉就可以用了,建议不要拿源文件直接用,最好是拷贝过去再进行修改,因为如果改错了还有备份!!!
我们还可查看内置命令所在目录

cd /usr/local/bin/

这个就是fastdfs内置命令,包括重启,启动,停止,还有测试,跟踪等等。。。
image.png
到这里,我们整个tracker服务器的fastdfs安装好了!!!
**同理,我们还要去安装storage,拿storage的安装和tracker是一模一样的,只是配置文件不同,其它安装包都一样,所以我们如果在同一台服务器部署只需修改配置文件即可!!! **

4.配置tracker

4.1.拷贝tracker.conf.sample

我们首先进入配置文件模板

cd /etc/fdfs

我们看到里有个tracker.conf.sample
image.png
然后我们拷贝一下,到conf就行

cp tacker..sample tracker.conf

4.2.配置tracker.conf

然后我们就可以进行配置了

vim tracker.conf

image.png
我们简单了解一下里面的属性

  • bind_addr = 绑定的ip地址
  • port = 22122 端口号
  • connect_timeout = 5 连接超时
  • network_timeout = 60 网络超时
  • base_path = /home/yuqing/fastdfs【这个是fastdfs我们一个tracker启动之后使用的一个根目录,它呢也要去存储一些信息,它存储的就是我们卷里面的一些storage的存储节点,包括我们卷1 卷2 卷3 每个卷下面有哪些storage,每个storage它的ip和它的端口都要去存储一下,因为它是做一个中转调度操作,client发起一个写操作的时候,我们的一个tracker就会去调度找到哪一个节点可以给我们去写操作,然后会返回ip和端口,tracker需要把我们卷下面的所有的一个存储节点ip和端口存储一下,这边我们可以把这个目录修改一下,base_path = /fastdfs/tracker,这个目录是我们自定义的,待会我们要去创建这个目录
  • max_connections = 1024 最大连接
  • accept_threads = 1 接收的线程
  • work_threads = 4 工作的线程
  • min_buff_size = 8KB 最小缓冲8KB
  • max_buff_size = 128KB 最大的缓冲
  • store_server = 0 负载均衡策略,0表示轮询机制,如果是1的话,就是第一个服务器通过我们ip地址查找到的第一个服务器,如果是2的话查找出来的也是第一个服务器,不是通过ip地址
  • store_path = 0 默认轮询
  • download_server = 0 下载服务默认轮询

这里我们就配置完成了,我们主要配置base_path 根目录即可!!!
然后我们别忘了创建刚才自定义的目录

mkdir -p /fastdfs/tracker

4.3.启动tracker

我们创建完目录呢,就可以启动tracker了,来到启动目录

cd /etc/init.d/

image.png
我们看到有两个文件 fdfs_trackerd和fdfs_storaged这两个就是我们启动的文件
我们之前安装的时候,我们修改过它的一个目录,所以呢我们这边启动的时候呢,也去修改它的一个目录,如果我们没有去修改它的目录,这边可以直接启动!!!
我们进入配置文件修改目录即可!!!

vim fdfs_trackerd

我们修改之后应该是/usr/local/bin/fdfs_trackerd,
image.png
保存并退出, 启动即可

./fdfs_trackerd start

image.png
怎么去看有没有启动成功呢?
两种方法:
查看状态

./fdfs_trackerd status

image.png
查看进程

ps -ef|grep fdfs

image.png
停止

.fdfs_trackerd stop

重启

/etc/init.d/fdfs_trackerd restart

开机启动
我们进入文件

vim /etc/rc.d/rc.local

添加启动文件
image.png

/etc/init.d/fdfs_tracked start

4.4.小结

我们现在已经启动了tracker包括我们相应的一些配置,其实真正要做的配置没有,默认的端口都没改,只是把它的base_path 根目录进行了一个修改,当然这个修改也是可有可无的,但是记住一定要创建,有一个负载均衡,大部分默认都是一个轮询的方式,其它的也没什么!!!

5.配置Storage

5.1.拷贝storage.conf.sample

跟tracker一样

cd /etc/fdfs

image.png
同样,拷贝一份

cp storage.conf.sample storage.conf

5.2.配置storage.conf

修改storage

vim storage.conf

也是一样,我们观察一下里面的属性
image.png

  • group_name = group1 默认组名,也是卷名
  • bind_addr = 绑定的ip地址
  • client_bind = true 是否允许客户端访问
  • port = 23000 端口号
  • connect_timeout = 5 连接超时
  • network_timeout = 60 网络超时
  • base_path = /home/yuqing/fastdfs【同样这里也是存放storage_server它里面基础数据的内容,以及日志内容的目录,比如说启动的进程号、同步的相应信息,我们也可以进行修改,/fastdfs/storage/base
  • max_connections =1024 最大连接数
  • buff_size = 256KB 缓冲大小
  • accept_threads = 1接收的线程
  • work_threads = 4 工作的线程
  • store_path0 = /home/yuqing/fastdfs【这个目录是我们真正存放文件的目录,我们也更改一下 /fastdfs/storage/store,这个目录会在我们storage启动的时候,它会去生成256x256目录,当然我们base_path和store_path0可以用同一个目录也是没有问题的,一般建议分开好区分】
  • tracker_server = 192.168.209.121:22122【这里也是我们需要修改的地方,有两个我们只需要一个即可,为什么有两个,因为我们tracker也是可以搞集群的,可以配置多个,这里我们直接写我们tracker追踪服务器ip地址,如果是同一台服务器写本机ip即可,端口不变!!!】

保存并退出即可!!!
创建我们刚才自定义的目录

mkdir -p /fastdfs/storage/base
mkdir -p /fastdfs/storage/store

我们安装的时候也修改了目录,所以我们这里也要修改一下

vim /etc/init.d/fdfs_storaged

修改PRG=/usr/local/bin/fdfs_storaged
image.png

5.3.启动storage

保存并退出,启动storage

/etc/init.d/fdfs_storaged start	 

image.png
查看状态

/etc/init.d/fdfs_storaged status

image.png
停止

/etc/init.d/fdfs_storaged stop

重启

/etc/init.d/fdfs_storaged restart

开启启动
我们进入文件

vim /etc/rc.d/rc.local

添加启动文件
image.png
启动之后我们可以去看看我们刚才创建的两个目录

cd /fastdfs storage/

image.png
base目录【基础数据目录】
可以看到有一个data和logs 基础数据和日志
image.png
可以看到有对应的一个日志
image.png
进入data目录image.png
里面有对应我们storage的一个进程号,然后启动的一个数据和**同步相应的相关信息 **
我们回去看我们store

cd ../../store

这里也有一个data,这个data存放我们上传文件的目录
image.png
我们发现这边使用16进制,从00一直到FF,一共是256个目录,然后我们每一个目录下面还有256个子目录
image.png
我们cd 00 再 ls
我们发现还是有 00 -FF 256个目录
image.png
再00就没了!!!再往下这边就会存放我们的文件了,至于我们文件上传上来之后会存放在那个目录下面,这个不需要我们去关系,storage会自己去存放,并且我们的tracker会去把我们的一个完整的卷名加文件名,这个文件名就是从data到00再到00再到下面的具体文件名一整个完整路径返回给我们,我们直接能拿到,它具体存放在着256个目录那个目录这个不需要我们操心,这是storage自定义去完成的,我们这里storage开机启动前提是必须先启动tracker再启动storage,不然会报错,因为我们storage配置文件里面配置了tracker_server,所以我们这边如果想要stroage开机自启,必须设置tracker也开机自启,不然不建议storage自启!!!

6.Client配置【可选】

客户端配置不是必需的,因为客户端配置完相当于用命令行去测试我们的fastdfs,所以我们如果不准备用命令行测试,而是准备用代码测试的话,我们完全可以跳过!!!
我们可以把Client放在tracker和storage任意服务器下,并不影响,把它配置改一下即可!!!

6.1.拷贝client.conf.sample

首先进入我们配置模板

cd /etc/fdfs

我们可以看到这边有一个client.conf.sample
image.png同样,我们先拷贝

cp client.conf.sample client.conf

6.2.配置client.conf

进入配置文件

vim client.conf

可以看下它的属性,更改的地方加粗

  • connect_timeout =5 连接超时
  • network_timeout = 60 网络超时
  • base_path = /home/yuqing/fastdfs 基础目录 【我们更改一些,/fastdfs/client,这里也是放客户端运行所产生的一些相应的数据】
  • tracker_server = 192.168.0.196:22122【我们之前说过,我们如果是客户端进行一个上传和下载的话,中间都要经过一个tracker,我们上传的时候呢,根据tracker找到storage的ip和端口进行上传,如果我们是下载的话,客户端也需要通过我们的tracker找到我们的一个ip和端口进行一个下载,所以我们这边要配置一个tracker,改成本机地址 192.168.248.101】

到这里,client配置就完成了,我们保存并退出,创建刚刚自定义的目录

mkdir -p /fastdfs/client

6.3.上传文件

我们看到root目录下面有个图片,我们可以把这个上传上去
image.png怎么去上传呢?上传的话因为我们安装的时候改过对应的一个目录,我们的命令目录呢在/usr/local/bin
我们上传就是fdfs_upload_file
image.png

./fdfs_upload_file /etc/fdfs/client.conf ~/cat-114782_640 (1).jpg

上传命令+客户端配置+上传的文件
返回上传的完整卷名加文件名,它的文件名是重新命名的
image.png

  • group1是我们storage里的配置,配的一个卷名的名称

image.png

  • M00 这是一个虚拟目录
  • 00和00表示它放在data-00-00目录下,当然一般情况下是按照顺序保存的,但我们也不能完全保证,storage有自己的一个规则,上传到哪一个目录下面去
  • wKj4ZWQJdC-AOV6HAAKSU_3XkA0484.jpg 文件名 重新命名了,防止文件名重复!!!

我们可以进入storage存放文件目录去查看我们上传的文件
image.png

小结

我们要记住M00是一个虚拟目录,有点相当于我们windows的快捷方式,它的引用主要是引用到我们的data目录下面,data就对应M00,快捷方式的一个意思

6.4.删除文件

./fdfs_delete_file /etc/fdfs/client.conf group1/M00/00/00/wKj4ZWQJdC-AOV6HAAKSU_3XkA0484.jpg

删除命令+client配置文件+完整的卷名和文件名
这里要注意,因为我们现在操作没有ip和端口,所以我们需要根据追踪器,根据这一整串完整的文件名去获取stroage的ip和端口,才会去进行一个删除这也就是我们client为什么要去配置tracker服务器
这样则表示删除成功
image.png
我们可以进入data-00-00查看是否删除成功
我们发现后最484的文件名没有了
image.png

7.安装nginx和fastdfs_nginx_module

为什么要安装nginx呢?因为我们fastdfs是一个文件系统,那可以存放很多类型的文件,比如说存放图片或者其它一个类型,图片的话我们可以通过网页直接去预览,而不需要通过我们现在这个操作,通过client拿到一个完整的卷名加文件名,通过tracker拿到对应的ip地址和端口再去下载预览,太麻烦,我们可以通过http协议直接在我们的url里面输入我们的一个ip端口,卷名文件名直接在浏览器里面可以访问这张图片或预览,那这个时候呢我们fastdfs没法实现,就要安装fastdfs_module_nginx和nginx进行代理
image.png

7.1.安装组件fastdfs-nginx-module

解压

tar zxvf fastdfs-nginx-module-1.22

cd 进入目录
image.png

  • HISTOPY 历史文件
  • INSTALL 安装
  • src 源码

我们来到src源码目录
image.png
源码目录有相应的一个配置,我们需要修改相应的一个配置

vim config

为什么要改配置呢?因为我们去安装我们的组件之后呢,会去安装我们的nginx,安装nginx的时候需要把我们的一个module模块加进去,加进去之后会寻找我们的一个fastdfs对应的一个安装的目录,如果目录不正确可能就安装失败了!!!所以我们需要去更改我们的一个目录路径
image.png
**修改的时候也有区别,因为我们装fastdfs的时候修改了我们安装目录,所以我们这串目录是改过目录之后的,如果我们没有修改过fastdfs的安装目录的,我们改的是另一个目录 **

/usr/local/include/fastdfs /usr/include/fastcommon/

fastdfs位置+核心库,这两个改呢,是因为我们安装改过,如果没有的话呢,我们改的不是这两个,就应该是把local删掉就行了
保存并退出,我们的模块就改好了,改好了之后并不代表它已经安装了,怎么去安装呢,就是我们去安装nginx的时候,把模块添加上去即可,所以我们还要去安装我们的nginx
image.png

7.2.安装nginx

依赖
安装之前我们还需要安装对应依赖

yum install -y gcc gcc-c++ make automake autoconf libtool pcre pcre-develzlib zlib-devel
openss1 openss1-devel

image.png
解压nginx
image.png

tar -zxvf nginx-1.16.1.tar.gz

查看目录
image.png
我们需要去更改一下它的目录
更改目录
先准备一个目录

mkdir -p /var/temp/nginx

配置nginx安装信息

./configure \
--prefix=/usr/local/nginx \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi\
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi \
--add-module=/usr/local/fastdfs/fastdfs-nginx-module-1.22/src
  • --prefix=/usr/local/nginx \ 安装路径

下面这些信息放入我们刚才创建的文件

  • --http-client-body-temp-path=/var/temp/nginx/client \
  • --http-proxy-temp-path=/var/temp/nginx/proxy \
  • --http-fastcgi-temp-path=/var/temp/nginx/fastcgi\
  • --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
  • --http-scgi-temp-path=/var/temp/nginx/scgi \
  • --add-module=/usr/local/fastdfs/fastdfs-nginx-module-1.22/src【这个是必须定义的,这个是我们安装nginx的时候需要加载的一个模块,如果没有指定的话呢,nginx安装过程中呢,就不会去加载fastdfs_nginx_module模块,如果nginx配置文件添加了这个模块,nginx启动会报错,后续功能无法实现!!!】

这样我们nginx安装信息就配置完成
image.png
接下来我们就可以进行安装了
预编译

make

编译加安装

make install

8.配置nginx模块

8.1.拷贝mod_fastdfs.conf

同理跟上面操作一样我们先把fastdfs_nginx_module模块配置文件修改

cd /usr/local/fastdfs/fastdfs-nginx-module-1.22/src/

这里面有一个我们对应的一个配置文件mod_fastdfs.conf
image.png

cd mod_fastdfs.conf /etc/fdfs

image.png
然后我们进行一个相应的修改

8.2.修改mod_fastdfs.conf

image.png

  • connect_timeout = 2 连接超时 ,可改可不改,改成10
  • network_timeout = 30 网络超时
  • base_path 基础目录
  • tracker_server = 192.168.248.101:22122【这里也需要我们tracker服务器,改成本机跟踪服务器ip即可】
  • storage_server_port = 23000 端口号
  • url_have_group_name = false 【表示我的url中是否要包含group名字,这里我们改成true
  • 包含即可 】
  • store_path0 = /home/yuqing/fastdfs【我们这里要改成我们之前存放文件的目录 /fastdfs/store】

保存并退出

8.3.拷贝http配置

这两个文件是因为我们nginx在网页上直接看图片的时候呢,是用的一个http协议,所以我们需要去拷贝一下
http

cp /usr/local/fastdfs/fastdfs-6.06/conf/http.conf /etc/fdfs/

请求头

cp /usr/local/fastdfs/fastdfs-6.06/conf/mime.types /etc/fdfs/

8.4.创建nginx

8.4.1.启动软连接

因为我们nginx启动的时候呢,会在默认的/usr/lib64目录下面去查找所需的so文件,那因为我们安装fastdfs的时候呢,已经修改了它的目录,所以我们一定要创建它的软链接,不然会失败

ln-s /usr/local/lib64/libfdfsclient.so /usr/lib64/libfdfsclient.so

8.4.2.网络服务启动软链接

这个软链接呢,我们的追踪服务返回的里面有一个M00,这个M00其实就是一个data,它是相当于windows的一个快捷方式,那我们需要把这个M00指定到data下面

ln -s /fastdfs/storage/store/data//fastdfs/storage/store/data/M00

我们可以去看一下我们创建的软连接是否生效

cd /fastdfs/storage/store/data

我们可以看到这边有个M00
image.png
它指定的目录呢就是我们data目录下
image.png

9.配置nginx

9.1.修改nginx.conf

cd /usr/local/nginx/

进入conf目录

cd conf

修改nginx.conf
image.png

vim nginx.conf	

image.png
修改哪些东西呢?

  • **user nobby **【因为是学习原因,我们这边直接配置root用户即可,正式工作中肯定是根据实际情况来决定的,我们user改成root用户意思是nginx访问alias必须要有文件系统的权限,那我们改成root就表示文件系统的一个权限是root用户的一个权限,如果我们不开启这个可能会出现404错误,那实际工作中,我们肯定不可能把这个root用户权限放出来,可能会自己创建一个用户指派一些权限,所以我们基于学习的一个目的直接开放即可!!!】
  • listen:8888【为什么是8888呢,我们cd /etc/fdfs,vim storage.conf,这边有一个htpp.server_port端口,它这边就是8888,如果我们这边改了,我们对应nginx配置文件也需要更改!!!】

加入以下配置

location ~/group[0-9]/M00{
    ngx_fastdfs_module;
}
  • ~/group[0-9]表示我们group0-9组都可以
  • M00表示data
  • ngx_fastdfs_module 是我们fastdfs_nginx_module配置的nginx模块

当我们去访问8888端口时候呢,它就会找到我们的本地的group0/M00,然后通过我们的ngx_fastdfs_module这样的一个模块去找到我们storage上面存储的一个对应文件就可以预览了!!!

9.2.重启storage并启动nginx

保存并退出,我们nginx配置就完成了,我们需要重启一下我们的storage并且要启动nginx

/etc/init.d/fdfs_storaged restart

image.png
我们来到nginx-sbin目录启动nginx

./nginx

看到端口表示启动成功!!!
image.png
查看启动状态

ps -ef |grep nginx

image.png

9.3.上传图片通过网页访问

我们通过客户端上传图片
image.png

/usr/local/bin/fdfs_upload_file /etc/fdfs/client.conf  cat-114782_640.jpg

同理,**上传命令+client配置+文件名即可 **
image.png查看上传的文件

cd /fastdfs/storage/store/data/00/00/

image.png 根据tracker给出的路径通过网页进行访问,ip+端口+卷名和文件名
image.png

10.Java API

10.1.依赖

<dependency>
  <groupId>org.csource</groupId>
  <artifactId>fastdfs-client-java</artifactId>
  <version>1.30-SNAPSHOT</version>
</dependency>

注意:这边我们maven仓库是没有这个依赖的,我们需要自己去下载然后安装到仓库

  • 首先确保我们环境依赖没有问题

image.png

image.png

  • 这里安装在了我本地的maven仓库里,如果没有配置maven本地仓库,那么他会默认下载到默认的本地仓库C:${user.home}.m2\repository
  • 这是本地已经安装好的fastdfs-client-java-master依赖

image.png

  • 然后重新加载maven依赖就可以啦,注意版本要对应

10.2.常用类

10.2.1.CLientGlobal【加载配置文件】

用于加载配置文件的公共客户端工具。
常用方法:

  • init(String conf_filename);根据配置文件路径及命名,加载配置文件并设置客户端公共参数,配置文件类型为conf文件。可以使用绝对路径或相对路径加载。
  • initByProperties(Properties props);根据Properties对象设置客户端公共参数。

注意:使用conf或prooerties进行客户端参数配置时,参数key命名不同。

10.2.2.TrckerClient【跟踪器客户端类型】

跟踪器客户端类型。创建此类型对象时,需传递跟踪器组,就是跟踪器的访问地址信息。无参构造方法默认使用ClientGlobal_tracker_group 常量作为跟踪器组来构造对象。【就是我们上面ClientGlobal定义的常量】
创建对象的方式为:

new TrackerClient();或new TrackerClient(ClientGlobal.g_tracker_group)

10.2.3.TrackerServer【跟踪器服务类型】

跟踪器服务类型,此类型的对象是通过跟踪器客户端对象创建的。实质上就是一个FastDFS Tracker Server的链接对象。是代码中与Tracker Server链接的工具
构建对象的方式为:

trackerClient.getTrackerServer();

10.2.4.StorageServer【存储服务类型】

存储服务类型。此类型的对象是通过跟踪器客户端对象创建的。实质上就是一个FastDFS Storage Server的链接对象。是代码中与StorageServer链接的工具。获取具体存储服务链接,是由Tracker Server分配的,所以构建存储服务对象时,需要依赖跟踪器服务对象
构建对象的方式为:

trackerClient.getStorage(trackerServer);

10.2.5.StorageClient【真正去操作文件的客户端类型,需要传入tracker和storage】

存储客户端类型,此类型对象是通过构造方法创建的。创建时,需传递跟踪服务对象和存储服务对象。此对象实质上就是一个访问FastDFS Storage Server的客户端对象,用于实现文件的读写操作
创建对象的方式为:

new StorageClient(trackerServer,storageServer);

常用方法有:

  • upload_file(String local_filename【文件路径】,String file_ext_name【文件扩展名,如果我们这里传null的话,那我们fastdfs就会自动解析文件扩展名,自动找.后面的一串 】,NameValuePair[] meta_list【文件属性集合】);上传文件的方法,参数local_filename为要上传的本地文件路径及文件名,可使用绝对路径或相对路径;参数file_ext_name为上传文件的扩展名,如果扩展null,则自动解析文件扩展名;参数meta_list是用于设置上传文件的源数据,如上传用户、上传描述等。
  • download_file(String group_name【卷名】,String remote_file_name【下载的文件名】);下载文件的方法,参数group为组名/卷名,就是Storage Server中/etc/fdfs/storage.conf配置文件中配置的group_name参数值,也是要下载的文件所在组/卷的命名;参数remote_file_name为要下载的文件路径及文件名。
  • delete_file(String group_name【卷名】,String remote_file_name【删除的文件名】);删除文件的方法,参数含义同download_file方法参数

11.Demo

11.1.创建项目

创建fastdfsdemo,加上web和thymeleaf依赖
image.png

11.2.导入相关依赖

<!--thylemeaf-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--web-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--fastdfs-->
<dependency>
  <groupId>org.csource</groupId>
  <artifactId>fastdfs-client-java</artifactId>
  <version>1.30-SNAPSHOT</version>
</dependency>  
<!--test-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

11.3.创建fdfs_client.conf配置文件

#连接超时
connect_timeout = 2
#网络超时
network_timeout = 30
#编码格式	
charset = utf-8
#tracke端口号
http.tracker_http_port = 8888
#防盗链功能
http.anti_steal_token = no
#秘钥
http.secret_key = FastDFS1234567890
#tracker ip :端口号
tracker_server = 192.168.248.101:22122
#连接池配置
connection_pool.enabled = true
connection_pool.max_count_per_entry = 500
connection_pool.max_idle_time = 3600
connection_pool.max_wait_time_in_ms = 1000

11.4.加载配置文件,生成服务端和客户端

//日志
private static Logger logger = LoggerFactory.getLogger(FastDFSClient.class);

//初始化FastDFS,ClientGlobal.init方法会读取配置文件,并初始化对应的属性
static {
    try {
        String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
        ClientGlobal.init(filePath);
    } catch (IOException | MyException e) {
        logger.error("FastDFS Client init Fail!", e);
        e.printStackTrace();
    }
}

/**
* 生成Storage客户端
* @return
* @throws IOException
*/
private static StorageClient getStorageClient() throws IOException {
    TrackerServer trackerServer = getTrackerServer();
//我们可以通过trackerServer拿到StorageServer,不设置也能拿到StorageClient
return new StorageClient(trackerServer, null);
}

/**
* 生成Tracker服务器端
* @return
* @throws IOException
*/
private static TrackerServer getTrackerServer() throws IOException {
    TrackerClient trackerClient = new TrackerClient();
return trackerClient.getTrackerServer();
}

11.5.上传、下载、删除、查看文件

   /**
     * 上传
     *
     * @param file 文件对象
     * @return
     */
    private static String[] upload(FastDFSFile file) {
        //打印相关信息
        logger.info("File Name:" + file.getName() + ",File lenght" + file.getContent().length);
        //文件属性信息
        NameValuePair[] meta_list = new NameValuePair[1];
        meta_list[0] = new NameValuePair("author", file.getAuthor());
        //上传时间
        long startTime = System.currentTimeMillis();
        //返回一个String数组
        String[] uploadResults = null;
        StorageClient storageClient = null;
        try {
            //拿到客户端
            storageClient = getStorageClient();
            //文件数组;后缀名;文件属性相关数组
            uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
        } catch (IOException | MyException e) {
            e.printStackTrace();
            //上传失败打印文件名
            logger.error("上传失败 File Name:" + file.getName(), e);
        }
        //上传时间
        logger.info("上传时间:" + (System.currentTimeMillis() - startTime + "ms"));
        //验证上传结果
        if (uploadResults == null && storageClient != null) {//判断上传结果是否为空
            logger.error("上传失败" + storageClient.getErrorCode());//上传失败拿到错误结果
        }
        //上传成功会返回相应信息
        logger.info("上传成功,group_name:" + uploadResults[0] + "remoteFileName:" + uploadResults[1]);//打印卷名和完整名字
        return uploadResults;
    }

    /**
     * 下载文件
     *
     * @param groupName      卷
     * @param remoteFileName 完整文件名
     * @return
     */
    public static InputStream downFile(String groupName, String remoteFileName) {
        try {
            //创建storage客户端对象
            StorageClient storageClient = getStorageClient();
            //调用下载方法,返回一个数组
            byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
            //输入要下载的文件
            InputStream ins = new ByteArrayInputStream(fileByte);
            return ins;
        } catch (IOException | MyException e) {
            logger.error("下载失败", e);
        }
        return null;
    }

    /**
     * 删除文件
     *
     * @param groupName      卷
     * @param remoteFileName 完整文件名
     */
    public static void deleteFile(String groupName, String remoteFileName) {
        try {
            StorageClient storageClient = getStorageClient();
            int i = storageClient.delete_file(groupName, remoteFileName);
            logger.info("删除成功" + i);
        } catch (IOException | MyException e) {
            logger.error("删除失败", e);
        }
    }

    /**
     * 查看文件信息
     * @param groupName      卷名
     * @param remoteFileName 完整文件名
     * @return
     */
    public static FileInfo getFile(String groupName, String remoteFileName) {
        try {
            StorageClient storageClient = getStorageClient();
            //获取文件方法
            FileInfo file_info = storageClient.get_file_info(groupName, remoteFileName);
            return file_info;
        } catch (Exception e) {
            logger.error("查看文件信息失败", e);
        }
        return null;
    }

11.6.网页查看图片方法


    /**
     * 获取文件路径,上传至后返回的完整路径
     * @return
     * @throws IOException
     */
    public static String getTrackerUrl() throws IOException {
        return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":8888/";
    }

11.7.完整代码

package com.zhang.fastdfsdemo.utils;
import com.zhang.fastdfsdemo.pojo.FastDFSFile;
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 java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
 * @author zb
 * @version 1.0
 * @date 2023/3/9 18:22
 * @ClassName FastDFSClient
 * @Description FastDFS工具类
 * StorageServer也可以通过Tracker拿到,我们直接那StorageClient即可,
 * 因为我们后期操作我们的文件,不管是上传、下载、删除都是通过我们storageClient进行操作的
 */
public class FastDFSClient {
    //日志
    private static Logger logger = LoggerFactory.getLogger(FastDFSClient.class);

    //初始化FastDFS,ClientGlobal.init方法会读取配置文件,并初始化对应的属性
    static {
        try {
            String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();
            ClientGlobal.init(filePath);
        } catch (IOException | MyException e) {
            logger.error("FastDFS Client init Fail!", e);
            e.printStackTrace();
        }
    }

    /**
     * 生成Storage客户端
     *
     * @return
     * @throws IOException
     */
    private static StorageClient getStorageClient() throws IOException {
        TrackerServer trackerServer = getTrackerServer();
        //我们可以通过trackerServer拿到StorageServer,不设置也能拿到StorageClient
        return new StorageClient(trackerServer, null);
    }

    /**
     * 生成Tracker服务器端
     *
     * @return
     * @throws IOException
     */
    private static TrackerServer getTrackerServer() throws IOException {
        TrackerClient trackerClient = new TrackerClient();
        return trackerClient.getTrackerServer();
    }

    /**
     * 上传
     *
     * @param file 文件对象
     * @return
     */
    private static String[] upload(FastDFSFile file) {
        //打印相关信息
        logger.info("File Name:" + file.getName() + ",File lenght" + file.getContent().length);
        //文件属性信息
        NameValuePair[] meta_list = new NameValuePair[1];
        meta_list[0] = new NameValuePair("author", file.getAuthor());
        //上传时间
        long startTime = System.currentTimeMillis();
        //返回一个String数组
        String[] uploadResults = null;
        StorageClient storageClient = null;
        try {
            //拿到客户端
            storageClient = getStorageClient();
            //文件数组;后缀名;文件属性相关数组
            uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
        } catch (IOException | MyException e) {
            e.printStackTrace();
            //上传失败打印文件名
            logger.error("上传失败 File Name:" + file.getName(), e);
        }
        //上传时间
        logger.info("上传时间:" + (System.currentTimeMillis() - startTime + "ms"));
        //验证上传结果
        if (uploadResults == null && storageClient != null) {//判断上传结果是否为空
            logger.error("上传失败" + storageClient.getErrorCode());//上传失败拿到错误结果
        }
        //上传成功会返回相应信息
        logger.info("上传成功,group_name:" + uploadResults[0] + "remoteFileName:" + uploadResults[1]);//打印卷名和完整名字
        return uploadResults;
    }

    /**
     * 下载文件
     *
     * @param groupName      卷
     * @param remoteFileName 完整文件名
     * @return
     */
    public static InputStream downFile(String groupName, String remoteFileName) {
        try {
            //创建storage客户端对象
            StorageClient storageClient = getStorageClient();
            //调用下载方法,返回一个数组
            byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
            //输入要下载的文件
            InputStream ins = new ByteArrayInputStream(fileByte);
            return ins;
        } catch (IOException | MyException e) {
            logger.error("下载失败", e);
        }
        return null;
    }

    /**
     * 删除文件
     *
     * @param groupName      卷
     * @param remoteFileName 完整文件名
     */
    public static void deleteFile(String groupName, String remoteFileName) {
        try {
            StorageClient storageClient = getStorageClient();
            int i = storageClient.delete_file(groupName, remoteFileName);
            logger.info("删除成功" + i);
        } catch (IOException | MyException e) {
            logger.error("删除失败", e);
        }
    }

    /**
     * 查看文件信息
     * @param groupName      卷名
     * @param remoteFileName 完整文件名
     * @return
     */
    public static FileInfo getFile(String groupName, String remoteFileName) {
        try {
            StorageClient storageClient = getStorageClient();
            //获取文件方法
            FileInfo file_info = storageClient.get_file_info(groupName, remoteFileName);
            return file_info;
        } catch (Exception e) {
            logger.error("查看文件信息失败", e);
        }
        return null;
    }

    /**
     * 获取文件路径,上传至后返回的完整路径
     * @return
     * @throws IOException
     */
    public static String getTrackerUrl() throws IOException {
        return "http://"+getTrackerServer().getInetSocketAddress().getHostString()+":8888/";
    }
}

11.8.前端测试

我们写一个contrller测试即可,service我们不写直接跳过,正常情况下应该要写service,service调我们工具类方法,contrller再调service,我们这里只是演示!!!
controller

package com.zhang.fastdfsdemo.controller;
import com.zhang.fastdfsdemo.pojo.FastDFSFile;
import com.zhang.fastdfsdemo.utils.FastDFSClient;
import org.csource.common.MyException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author zb
 * @version 1.0
 * @date 2023/3/9 21:12
 * @ClassName FileContrller
 * @Description TODD 测试文件上传、下载、删除、查看
 */
@Controller
public class FileContrller {
    private static Logger logger = LoggerFactory.getLogger(FileContrller.class);

    /**
     * 上传文件
     * 我们现在入参是 MultipartFile ,但我们真正要传进去的fastdfs file,那我们可以把它提取出来调用
     *
     * @param file
     * @param redirectAttributes
     * @return
     */
    @PostMapping("upload")
    public String singFile(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) {
        if (file.isEmpty()) {//判断上传文件是否为空
            redirectAttributes.addFlashAttribute("message", "请选择一个文件上传");
            return "redirect:uploadStatus";
        }
        //拿到客户端
        try {
            //上传文件,拿到返回文件路径
            String path = saveFile(file);
            redirectAttributes.addFlashAttribute("message", "上传成功" + file.getOriginalFilename());
            redirectAttributes.addFlashAttribute("path", "路径:" + path);
        } catch (IOException | MyException e) {
            e.printStackTrace();
        }
        return "redirect:uploadStatus";
    }

    /**
     * 跳转上传文件状态也
     *
     * @return
     */
    @GetMapping("/uploadStatus")
    public String uploadStatus() {
        return "uploadStatus";
    }

    /**
     * 跳转文件上传页
     *
     * @return
     */
    @GetMapping("/")
    public String upload() {
        return "upload";
    }

    /**
     * MultipartFile转fastdfsfile
     *
     * @param multipartFile
     * @return
     * @throws IOException
     */
    public String saveFile(MultipartFile multipartFile) throws IOException, MyException {
        String[] fileAbsolutePath = {};
        String fileName = multipartFile.getOriginalFilename();//获取文件名
        String ext = fileName.substring(fileName.lastIndexOf(".") + 1);//截取后缀
        byte[] file_buff = null;
        InputStream inputStream = multipartFile.getInputStream();
        if (inputStream != null) {
            int len1 = inputStream.available();//拿到长度
            file_buff = new byte[len1];
            inputStream.read(file_buff);
        }
        inputStream.close();
        FastDFSFile file = new FastDFSFile(fileName, file_buff, ext);//转成fastdfs文件类型
        //上传,//返回组名和文件名
        fileAbsolutePath = FastDFSClient.upload(file);
        if (fileAbsolutePath == null) {
            logger.error("上传失败");
        }
        String path = FastDFSClient.getTrackerUrl() + fileAbsolutePath[0] + "/" + fileAbsolutePath[1];//返回完整路径
        return path;
    }
}

html
上传文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <h1>Spring Boot file upload example</h1>
    <form method="post" action="/upload" enctype="multipart/form-data">
      <input type="file" name="file"><br><br>
      <input type="submit" value="submit">
    </form>
  </body>
</html>

上传状态

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>SpringBoot - Upload Status</h1>
<div th:if="${message}">
    <h2 th:text="${message}"></h2>
</div>
<div th:if="${path}">
    <h2 th:text="${path}"></h2>
</div>
</body>
</html>

测试
上传成功
image.png
流览图
image.png

posted @ 2023-03-09 22:31  SmallRoll(小卷)  阅读(293)  评论(0编辑  收藏  举报