在早期尝试Android开发的时候,我用了一些非常拙劣的方法来传输图片,现在想起来自己都想笑。
之前我的想法很粗暴,上传图片到后端处理不用多说,肯定就是把文件上传到后端的指定目录,然后数据库内存放文件的地址。前端需要用图片的时候就从数据库里面找到地址,找到对应的文件,再传给后端,后端拿到统一资源定位符后显示图片(在Android开发的流程中,这个步骤非常复杂,特别是Google现在采用了沙箱机制,导致能操作的目录非常有限)。
(正在想办法让WordPress支持SVG格式图片,所以目前还是会有一些不清晰,用的是PNG)
这个办法一直被我沿用了大概一年左右,直到我在Web开发中也产生了类似的需求(当然只有图片的预览,不涉及保存操作,前面提到的Android客户端也不涉及到保存图片需求,仅限预览),这个方法就不好使了,总不能让浏览器先保存图片吧。不过很快我就找到了解决方案,那就是搭建一个图片服务器。
方法很简单,无非就是用一个Nginx做一个反向代理。让Nginx从相应的目录找到图片后,直接返回相应的URL,这样就舒服多了。于是我把我开发的多个Android APP也做了相应的功能更新,不过之前写的代码也不能浪费啊,索性改成保存到本地这个功能好了,真省事儿(实际上没有,因为我懒到连按钮都不想加了)。
实际上这样就已经完整可用了,毕竟咱做开发都是自娱自乐,东西除了自己用之外,就没别人的了。但实际应用中,文件的存储都不是保存在单个服务器的某个目录中的,而是通过DFS来存储。
这是一篇为了在课上演示而写的文章,所以我在这里简单介绍一下DFS系统的常见工作原理。
我们以HDFS为例。
我在这里引用一下官方的一部分介绍。
Namenode 和 Datanode
HDFS采用master/slave架构。一个HDFS集群是由一个Namenode和一定数目的Datanodes组成。Namenode是一个中心服务器,负责管理文件系统的名字空间(namespace)以及客户端对文件的访问。集群中的Datanode一般是一个节点一个,负责管理它所在节点上的存储。HDFS暴露了文件系统的名字空间,用户能够以文件的形式在上面存储数据。从内部看,一个文件其实被分成一个或多个数据块,这些块存储在一组Datanode上。Namenode执行文件系统的名字空间操作,比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体Datanode节点的映射。Datanode负责处理文件系统客户端的读写请求。在Namenode的统一调度下进行数据块的创建、删除和复制。
应该没多少人看,实际上写的还挺好的。我在这里简单的解释一下。
Namenode即负责管理文件的服务器,由于服务器集群往往结构复杂,而且文件是分块存储的,每一个Datanode存放的部分不尽相同,这个时候就需要Namenode服务器做统一的管理,其管理的数据可以被称为“MetaData”,即“元数据”。数据是指文件中的实际数据,即文件的实际内容;而元数据是用来描述一个文件特征的系统数据,诸如访问权限、文件拥有者以及文件数据块的分布信息等等。如果文件是一张图片,元数据就是图片的宽,高等等。
而Datanode主要是负责存储数据和处理读写请求的。就这么简单。听着原理就知道这很分布,给你把文件都拆了。
我在实践部分介绍的是FastDFS,原理与这个大差不差,但实际上并没有把文件拆分成很多块,原因我现在来举个小例子说明一下。
在这里先提一下,在Hadoop2.X版本中,HDFS单个文件块的大小是128M。了解完这个再回过头来看我们的需求,在服务器里存图片。这个就很难办了,因为我们的图片在未拆分之前,体积大小往往远小于128M。再说另一个拆分文件带来的另一个问题,文件的拆分和合并往往会造成性能和时间的额外开销,如果我们浏览的网页存在大量的图片,加载这些图片并合并就会造成比较可观的开销。
所以在应付我们这个简单的需求时,这个分布式系统带来的缺点反而比优点多的多。FastDFS解决这个问题的方法很简单,干脆就不拆分了,直接存。当然了,我们还是将其视作一个分布式的存储系统,因为除了不拆文件外,它和其他DFS并没有太大的区别。
以FastDFS为例,讲一下实践过程。配置如下:
- Java 1.8
- Debian 11 on ARM64(FastDFS Server)
- macOS 12.5.1(Client)
- Spring Boot 2.7.1
- Nginx 1.23.1
- Vue 3.0
首先还是放一张架构图(来源于GitHub项目主页,非常的不清晰就对了)
Tracker集群可以看作上述HDFS的Namenode,Storage集群可以看作Datanode,具体我就不多说了。
在项目的Wiki页面有详细的安装说明,自行移步GitHub:Wiki。写的比较详细,注意一下Nginx的配置就行,最好换成新的主线版本。
由于在家实在搞不出来服务器集群,所以我直接就单机部署了。
部署完成后利用以下命令查看服务器集群情况:
/usr/bin/fdfs_monitor /etc/fdfs/storage.conf
单机部署时,Tracker和Storage实际上为同一台服务器,IP地址相同。
下面通过Java API来实现文件的上传和下载功能。
作者在Github项目主页给出了Java Client的源码以及使用方法,利用此API以及SpringBoot搭建一个简单的后端服务器,利用Vue写一个简单的单页面应用。该用例会实现图片的上传和预览功能。
下面把重点放在后端服务器的搭建上,前端自己玩去,也没几行代码,放张图,非常简陋。
首先需要把基础的环境配好,创建一个Spring Boot项目,引入Spring Web包。我在这里给出Maven依赖。也可以直接创建标准的Maven项目,然后导入依赖即可。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>edu.njucm</groupId>
<artifactId>fastdfs_java</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>fastdfs_java</name>
<description>fastdfs_java</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/net.oschina.zcx7878/fastdfs-client-java -->
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
这里需要注意的是,最关键的client依赖用的并不是作者提供的,而是第三方的,原因非常简单,作者提供的依赖无法导入。
然后我们在main目录的resources文件夹内创建一个配置文件,命名为fastdfs-client.properties
,实际上这个配置文件的内容也可以在GitHub找到,我只是做了一些简单的替换,将IP地址替换为我的服务器地址。GitHub地址
fastdfs.connect_timeout_in_seconds = 5
fastdfs.network_timeout_in_seconds = 30
fastdfs.charset = UTF-8
fastdfs.http_anti_steal_token = false
fastdfs.http_secret_key = FastDFS1234567890
fastdfs.http_tracker_http_port = 80
fastdfs.tracker_servers = 192.168.31.33:22122
在这之后编写一些测试类,用作测试,当然是放在test目录。
首先编写一个合适的上传测试类,在这里我就不解释了,直接贴代码,注释有空再补。
@Test
public void testUpload() {
try {
ClientGlobal.initByProperties("/Users/zhangjin/Java/IdeaProjects/FastDFS_Test/Java/fastdfs_java/src/main/resources/config/fastdfs-client.properties");
System.out.println("network_timeout=" + ClientGlobal.g_network_timeout + "ms");
System.out.println("charset=" + ClientGlobal.g_charset);
TrackerClient tc = new TrackerClient();
TrackerServer ts = tc.getConnection();
if (ts == null) {
System.out.println("getConnection return null");
return; }
StorageServer ss = tc.getStoreStorage(ts);
if (ss == null) {
System.out.println("getStoreStorage return null");
}
StorageClient1 sc1 = new StorageClient1(ts, ss);
NameValuePair[] meta_list = null; //new NameValuePair[0];
String item = "/Users/zhangjin/Downloads/SpringBoot- Android传输流程.png";
String fileid;
fileid = sc1.upload_file1(item, "png", meta_list);
System.out.println("Upload local file " + item + " ok, fileid=" + fileid);
} catch (Exception ex) {
ex.printStackTrace();
}
}
运行后,可以看到,我们的文件已经传输成功了!仔细看打印的日志,给出了在Linux服务器中存储的实际位置,我们可以去那边看一下。为了演示清晰,这里用VNC连接到可视化页面查看那张图片。
下面测试分布式文件系统的查询功能,看看是否可以查询出我们刚才上传的那张图片的信息,依旧只给出代码,解释的话,有空再说。
@Test
public void testQueryFile() throws IOException, MyException {
ClientGlobal.initByProperties("/Users/zhangjin/Java/IdeaProjects/FastDFS_Test/Java/fastdfs_java/src/main/resources/config/fastdfs-client.properties");
TrackerClient tracker = new TrackerClient();
TrackerServer trackerServer = tracker.getConnection();
StorageServer storageServer = null;
StorageClient storageClient = new StorageClient(trackerServer,storageServer);
FileInfo fileInfo = storageClient.query_file_info("group1",
"M00/00/00/wKgfIWMUspCAcHSdAAA2rW7tWd8845.png");
System.out.println(fileInfo);
}
运行结果,非常简单明了,不做解释,该有的信息都有了。
最后需要提到的是最重要的下载文件,其实还是比较好办的,看代码。
@Test
public void testDownloadFile() throws IOException, MyException {
ClientGlobal.initByProperties("/Users/zhangjin/Java/IdeaProjects/FastDFS_Test/Java/fastdfs_java/src/main/resources/config/fastdfs-client.properties");
TrackerClient tracker = new TrackerClient();
TrackerServer trackerServer = tracker.getConnection();
StorageServer storageServer = null;
StorageClient1 storageClient1 = new StorageClient1(trackerServer,
storageServer);
byte[] result =
storageClient1.download_file1("group1/M00/00/00/wKgfIWMUspCAcHSdAAA2rW7tWd8845.png");
File file = new File("1.png");
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(result);
fileOutputStream.close();
}
实际运行后,我们的项目根目录就有了刚才上传的那份儿文件。
这样的话,基本的功能就测试完了。其实这部分是比较容易出问题的,测试通过后,下面与Web整合的部分就好办多了,只需要操心一下Nginx的配置问题,其他就没什么了。
写的太多了,估计课上讲不了这么多,所以我决定Web整合部分再水一篇文章,没了。