FastDFS分布式文件系统简单使用
FastDFS简介
1、FastDFS体系结构
FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传和下载)等,解决了大容量存储和负载均衡的问题,特别适合以文件为载体的在线服务,如相册网站、视频网站等等。
FastDFS架构包括Tracker server(跟踪服务,类似zookeeper注册中心,负责调度) 和storage server(真正存储文件的地方)。客户端(指的是系统,或者说项目,再或者说写代码的地方)发送请求,到traker server进行文件上传、下载,tracker server又会通过调度各个storage server进行负载均衡算法,将storage server的地址返回给客户端,客户端拿到地址以后就可以将文件存储到该地址的storager server的服务上,storger server又会返回存储好的文件id给客户端,客户端可以通过这个文件id在浏览器上访问。
2、使用FastDFS上传文件流程
解释:storager server是真正存储文件的地方,它会定时向调度中心(tracker server)上传状态信息,并且storage server可以有很多个,这样tracker server就可以做负载均衡。
在使用的时候,客户端需要先向调度中心(storager server)发送请求,调度中心(storage server)会根据负载均衡算法返回一个tracker server(存储文件的服务)的地址。
客户端获取到这个地址以后,就可以直接根据地址上传文件,storage server(存储文件的服务)会生成一个文件id,并将这个文件id返回,客户端拿到这个文件id,拼接ip和端口号以后,就可以通过浏览器访问该图片。
文件id的组成:
group1: 组名:文件上传后所在的storage server 组名称,文件上传后由storage server返回,需要客户端自行保存
/M00: 虚拟磁盘路径,storage server配置的虚拟路径,与磁盘选项store_path*对应。如果配置store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。
/02/44:数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据
/wkgD让E34E8wAAAAAAAA……文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。
3、下载文件流程
跟上传差不多,前端发送请求,带着图片路径,请求发送到处理图片的controller中,controller用代码去连接tracker server,tracker server返回这张图片存储的storage server地址,controller拿到地址以后,访问该地址的sotrage server,sotrage server就会找到该图片,并以流的形式返回给controller,contorller先读流,再写流到指定的位置,就完成了下载的流程。
4、用户访问图片流程
用户访问图片,不再像上传和下载那样,通过controller、tracker server、storage server了,这样反而绕了一大圈,并且获得的还是流数据,所以这里直接由用户发送http请求,由nginx接收,处理请求后最终获得图片,如上图,fastdfs_nginx_module能通过文件名中的组名等信息找到对应的storage server,从而获得图片返回。
5、微服务项目中,图片处理的解决方案
在微服务项目中,如果很多模块都需要用到图片上传,例如电商项目,一打开页面到处都是图片,那么图片请求的并发量肯定高,并且分布在各个模块,如果图片因为并发量的原因,给每个模块都进行集群的话,那么一些原本并发量并不大的模块因为其中的图片请求处理而需要集群的话,对于项目成本来说是不合适的,所以这个时候就可以将图片处理单独写一个微服务,无论哪个模块需要上传、下载图片或者文件,都只需要发请求到这个微服务中就可以了,如果请求并发量大,对这个图片处理微服务进行集群即可,跟原本的功能模块没有任何联系,它们之间想要联系,远程通过feign调用即可。
使用FastDFS实现文件上传、下载、删除
创建一个spring boot工程,该工程主要用于实现文件上传、下载、删除
1、添加依赖:下面的依赖其实有最后一个就行了,其它的是因为微服务中需要用到的,跟FastDFS没有直接联系
<!--spring-cloud-feign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--web起步依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--eureka客户端--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--httpclient支持--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency>
<!--fastDFS依赖--> <dependency> <groupId>net.oschina.zcx7878</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27.0.0</version> </dependency>
2、FastDFS配置
在resources文件夹下创建fasfDFS的配置文件fdfs_client.conf:
# connect_timeout:设置连接超时时间,单位为秒 connect_timeout=60 # network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后还不能发送或接收数据,则本次网络通信失败 network_timeout=60 # 字符集 charset=UTF-8 # tracker的http端口 http.tracker_http_port=8080 # tracker服务器IP和端口设置 tracker_server=192.168.211.132:22122
3、application.yml配置
在resources文件夹下创建application.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
4、启动类:创建com.xxx包,创建启动类FileApplication
@SpringBootApplication @EnableEurekaClient public class FileApplication { public static void main(String[] args) { SpringApplication.run(FileApplication.class); } }
5、创建测试类测试:
public class FastDFSTest { //上传图片 @Test public void upload() throws Exception { //1.创建一个配置文件 用于配置连接到tracker server的ip地址和端口 //2.加载配置文件使其生效,参数指定配置文件的全路径 ClientGlobal.init("D:\\XM1\\xxx\\xxx-parent\\xxx-service\\xxx-service-file\\src\\main\\resources\\fastDFS_client.conf"); //3.创建一个trackerClient TrackerClient trackerClient = new TrackerClient(); //4.获取tracker server TrackerServer trackerServer = trackerClient.getConnection(); //5.创建storageServer 赋值为空 //6.创建storageClient (提供了很多图片操作的API) //参数1 指定track server //参数2 指定storage server,如果你知道要上传到哪个storage server的ip地址和端口,那就填,但是这里并不知道 StorageClient storageClient = new StorageClient(trackerServer,null); //7.执行上传图片 //参数1 指定图片的所在的路径 //参数2 指定图片的后缀 去掉 "点" //参数3 指定图片的元数据 :高度 宽度 像素 作者 拍摄日期 文件大小......... String[] pngs = storageClient.upload_file("D:\\aaa\\微信图片_202008300825239.jpg", "png", null); for (String png : pngs) { System.out.println(png); } } }
启动上传方法,就会将指定路径中的图片上传到FastDFS的Storage 服务器中,然后输出返回的文件名,打印如下:
group1就是组名
可以用ip、端口、组名、这个文件名拼接,然后在浏览器访问:http://192.168.211.132:8080/group1/M00/00/00/wKjThF-rCGGAQaYRAAFNwOGrCrc492.png
上面测试了上传,那么下载和删除也差不多的步骤,接着在上面的测试类中加两个方法:
//下载图片 单例模式 @Test public void download() throws Exception{ //1.创建一个配置文件 用于配置连接到tracker server的ip地址和端口 //2.加载配置文件使其生效 ClientGlobal.init("D:\\XM1\\xxx\\xxx-parent\\changgou-service\\xxx-service-file\\src\\main\\resources\\fastDFS_client.conf"); //3.创建一个trackerClient TrackerClient trackerClient = new TrackerClient(); //4.获取tracker server TrackerServer trackerServer = trackerClient.getConnection(); //5.创建storageServer 赋值为空 //6.创建storageClient (提供了很多图片操作的API) //参数1 指定track server //参数2 指定storag eserver StorageClient storageClient = new StorageClient(trackerServer,null); //7.执行下载图片 //参数1 组名 参数2 文件名 byte[] bytes = storageClient.download_file("group1", "M00/00/00/wKjThF-rCGGAQaYRAAFNwOGrCrc492.png"); //将文件下载到的目录 FileOutputStream fileOutputStream = new FileOutputStream(new File("D:\\1234.jpg")); fileOutputStream.write(bytes); fileOutputStream.close();//放finally中 }
删除图片:
//删除图片 @Test public void deleteFile() throws Exception{ //1.创建一个配置文件 用于配置连接到tracker server的ip地址和端口 //2.加载配置文件使其生效 ClientGlobal.init("D:\\XM1\\xxx\\xxx-parent\\changgou-service\\xxx-service-file\\src\\main\\resources\\fastDFS_client.conf"); //3.创建一个trackerClient TrackerClient trackerClient = new TrackerClient(); //4.获取tracker server TrackerServer trackerServer = trackerClient.getConnection(); //5.创建storageServer 赋值为空 //6.创建storageClient (提供了很多图片操作的API) //参数1 指定track server //参数2 指定storage server StorageClient storageClient = new StorageClient(trackerServer,null); int group1 = storageClient.delete_file("group1", "M00/00/00/wKjThF-rCGGAQaYRAAFNwOGrCrc492.png"); if(group1==0){ System.out.println("成功"); }else{ System.out.println("成仁"); }
以上方式只是简单使用的api,还有许多地方都是写死的,所以一般在开发中,都会封装一下,实现动态。:
创建一个Util类,以及一个封装文件操作数据的pojo:
/** * 文件上传、下载、删除的工具类,调用可实现。 * */ public class FastDFSClientUtil { //1.加载配置文件使其生效,用静态代码块实现单例模式,它只需要加载一次 //不再写死加载文件路径,用类路径动态获取 static { try { //创建对象,动态获取类路径下指定文件名的配置文件 ClassPathResource classPathResource = new ClassPathResource("fastDFS_client.conf"); ClientGlobal.init(classPathResource.getPath()); } catch (Exception e) { e.printStackTrace(); } } /** * 文件上传 * 返回值:String【】:这个数组【0】元素是文件存储的组名,【1】元素是文件名 * 参数:封装的存储前端请求代来的文件相关信息 * */ public static String[] uploadFile(FastDFSFile fastDFSFile) throws Exception { //3.创建一个trackerClient TrackerClient trackerClient = new TrackerClient(); //4.获取tracker server TrackerServer trackerServer = trackerClient.getConnection(); //5.创建storageServer 赋值为空 //6.创建storageClient (提供了很多图片操作的API) //参数1 指定track server //参数2 指定storage server,如果你知道要上传到哪个storage server的ip地址和端口,那就填,但是这里并不知道 StorageClient storageClient = new StorageClient(trackerServer,null); //7.执行上传图片,这里也需要更改,不再传文件路径,而是传文件内容(字节数组) //参数1 指定文件本身内容(字节数组) //参数2 指定图片的后缀 去掉 "点" //参数3 指定图片的元数据 :高度 宽度 像素 作者 拍摄日期 文件大小......... NameValuePair[] meta_list = new NameValuePair[]{ new NameValuePair(fastDFSFile.getName()) }; String[] pngs = storageClient.upload_file(fastDFSFile.getContent(), fastDFSFile.getExt(),meta_list); return pngs; } /** * 下载文件 * 返回值:字节数组:文件内容 * 参数: 要下载的文件存在Storage server 中的组名、文件名 * */ public static byte[] downFile(String groupName,String fileName) throws Exception { //3.创建一个trackerClient TrackerClient trackerClient = new TrackerClient(); //4.获取tracker server TrackerServer trackerServer = trackerClient.getConnection(); //5.创建storageServer 赋值为空 //6.创建storageClient (提供了很多图片操作的API) //参数1 指定track server //参数2 指定storag eserver StorageClient storageClient = new StorageClient(trackerServer,null); //7.执行下载图片 //参数1 组名 参数2 文件名 byte[] bytes = storageClient.download_file(groupName, fileName); //8.返回字节数组 return bytes; } /** * 删除文件 * 返回值: 删除结果,true删除成功,false者删除失败 * 参数:文件存储的组名和文件存储的文件名 * */ public static boolean deleteFile(String groupName,String fileName) throws Exception { //3.创建一个trackerClient TrackerClient trackerClient = new TrackerClient(); //4.获取tracker server TrackerServer trackerServer = trackerClient.getConnection(); //5.创建storageServer 赋值为空 //6.创建storageClient (提供了很多图片操作的API) //参数1 指定track server //参数2 指定storage server StorageClient storageClient = new StorageClient(trackerServer,null); //参数1 指定存储文件的组名 //参数2 指定存储文件的文件名 //返回值,返回值如果等于0则说明删除成功 int group1 = storageClient.delete_file(groupName, fileName); //如果下面的运算成立,则返回true,否则返回false return group1==0; }
/** * 封装文件属性 * */ public class FastDFSFile { //文件名字 private String name; //文件内容 private byte[] content; //文件扩展名 private String ext; //文件MD5摘要值 private String md5; //文件创建作者 private String author; 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; } 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() { } }
再然后就写controller接受请求,调用util实现文件上传,返回前端路径即可(以下代码没有完善,还有redis缓存和真正将路径存入数据库的操作没有体现):
/** * 图片上传、删除模块 * */ @RequestMapping("/file") @RestController public class FileController { @Value("${pic.url}") private String path; @PostMapping("/upload") public Result<String> uploadFile(MultipartFile multipartFile){ try { //判断,如果传过来的文件内容不为空 if (!StringUtils.isEmpty(multipartFile)){ //创建上传图片需要的数据pojo并赋值 FastDFSFile fastDFSFile = new FastDFSFile(); //给文件名赋值 fastDFSFile.setName(multipartFile.getOriginalFilename()); //给文件后缀(文件类型)赋值,用StringUtils,从文件名中截取到文件后缀 fastDFSFile.setExt(StringUtils.getFilenameExtension(multipartFile.getOriginalFilename())); //给文件本身赋值(文件的字节数组) fastDFSFile.setContent(multipartFile.getBytes()); //调用文件上传util执行文件上传操作,并将赋值完毕的pojo数据作为参数传过去 String[] strings = FastDFSClientUtil.uploadFile(fastDFSFile); //将fastDFS服务端返回的文件路径拼接成用户可浏览的路径: //将http:ip,端口号等前面的路径放在配置文件中,实现动态获取,然后拼接路径 String url = path +"/"+ strings[0] +"/"+ strings[1]; return new Result<String>(true, StatusCode.OK,"上传成功",url); } } catch (Exception e) { e.printStackTrace(); } return null; } }
上面这个controller显然还有一个地方,就是拼接用户可访问路径的时候,是实现了动态从配置文件中获取的,所以springboot 的application配置文件中需要有这个路径才能拼接:文件中的路径因为我没有域名,所以只能这样:
spring: servlet: multipart: max-file-size: 10MB # max-file-size是单个文件大小限制 max-request-size: 10MB # max-request-size是设置总上传的数据大小 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 pic: # 最好是通过域名来访问设置 url: http://192.168.211.132:8080