1. 程式人生 > >spring boot 學習筆記 (6) 上傳檔案到 FastDFS

spring boot 學習筆記 (6) 上傳檔案到 FastDFS

一、什麼是 FastDFS

FastDFS 是一個開源的輕量級分散式檔案系統,它解決了大資料量儲存和負載均衡等問題,特別適合以中小檔案(建議範圍:4 KB < file_size < 500 MB)為載體的線上服務,如相簿網站、視訊網站等。在 UC 基於 FastDFS 開發向用戶提供了網盤、社群、廣告和應用下載等業務的儲存服務。

FastDFS 由 C 語言開發,支援 Linux、FreeBSD 等 UNIX 系統類 Google FS,不是通用的檔案系統,只能通過專有 API 訪問,目前提供了 C、Java 和 PHP API,為網際網路應用量身定做,解決了大容量檔案儲存問題,追求高效能和高擴充套件性,FastDFS 可以看做是基於檔案的 Key Value Pair 儲存系統,稱作分散式檔案儲存服務會更合適。

FastDFS 特性

  • 檔案不分塊儲存,上傳的檔案和 OS 檔案系統中的檔案一一對應
  • 支援相同內容的檔案只儲存一份,節約磁碟空間
  • 下載檔案支援 HTTP 協議,可以使用內建 Web Server,也可以和其他 Web Server 配合使用
  • 支援線上擴容
  • 支援主從檔案
  • 儲存伺服器上可以儲存檔案屬性(meta-data)V2.0 網路通訊採用 libevent,支援大併發訪問,整體效能更好

FastDFS 相關概念

FastDFS 服務端有三個角色:跟蹤伺服器(Tracker Server)、儲存伺服器(Storage Server)和客戶端(Client)。

  • Tracker Server:跟蹤伺服器,主要做排程工作,起負載均衡的作用。在記憶體記錄叢集中所有儲存組和儲存伺服器的狀態資訊,是客戶端和資料伺服器互動的樞紐。相比 GFS 中的 Master 更為精簡,不記錄檔案索引資訊,佔用的記憶體量很少。
  • Storage Server:儲存伺服器(又稱儲存節點或資料伺服器),檔案和檔案屬性(Meta Data)都儲存到儲存伺服器上。Storage Server 直接利用 OS 的檔案系統呼叫管理檔案。
  • Client:客戶端,作為業務請求的發起方,通過專有介面,使用 TCP/IP 協議與跟蹤器伺服器或儲存節點進行資料互動。FastDFS 向使用者提供基本檔案訪問介面,如 upload、download、append、delete 等,以客戶端庫的方式提供給使用者使用。

通過一張圖來看一下 FastDFS 的執行機制:

Tracker 相當於 FastDFS 的大腦,不論是上傳還是下載都是通過 Tracker 來分配資源;客戶端一般可以使用 Ngnix 等靜態伺服器來呼叫或者做一部分的快取;儲存伺服器內部分為卷(或者叫做組),卷與卷之間是平行的關係,可以根據資源的使用情況隨時增加,卷內伺服器檔案相互同步備份,以達到容災的目的。

上傳機制

首先客戶端請求 Tracker 服務獲取到儲存伺服器的 IP 地址和埠,然後客戶端根據返回的 IP 地址和埠號請求上傳檔案,儲存伺服器接收到請求後生產檔案,並且將檔案內容寫入磁碟並返回給客戶端 file_id、路徑資訊、檔名等資訊,客戶端儲存相關資訊上傳完畢。

下載機制

客戶端帶上檔名資訊請求 Tracker 服務獲取到儲存伺服器的 IP 地址和埠,然後客戶端根據返回的 IP 地址和埠號請求下載檔案,儲存伺服器接收到請求後返回檔案給客戶端。

二、Spring Boot 整合 FastDFS

本節示例專案會在上一節專案的基礎上進行構建開發,從前端上傳檔案到後臺後,直接傳遞到 FastDFS 叢集中,並返回檔案儲存的地址。

pom 包配置

引入 FastDFS 的 Java 客戶端:

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

fastdfs-client-java 為 FastDFS 的 Java 客戶端,用來和 FastDFS 叢集進行互動。

FastDFS 配置

專案 resources 目錄下新增fdfs_client.conf檔案:

connect_timeout = 60 # 連線超時時間
network_timeout = 60 # 網路超時時間
charset = UTF-8 # 編碼格式
http.tracker_http_port = 8080 #tracker 埠
http.anti_steal_token = no #token 防盜鏈功能
http.secret_key = 123456 #金鑰
# tracer server 列表,多個 tracer server 的話,分行列出
tracker_server = 192.168.53.85:22122
tracker_server = 192.168.53.86:22122

配置檔案設定了連線的超時時間、編碼格式以及 tracker_server 地址等資訊。

詳細內容參考:fastdfs-client-java

封裝 FastDFS 上傳工具類

封裝 FastDFSFile,檔案基礎資訊包括檔名、內容、檔案型別、作者等。

public class FastDFSFile {
    private String name;
    private byte[] content;
    private String ext;
    private String md5;
    private String author;
    //省略getter、setter
}

接下來封裝 FastDFSClient 類,FastDFSClient 主要封裝最基礎的操作,包含上傳、下載、刪除等方法。FastDFSFile 任務可以是 FastDFS 上傳檔案的封裝,操作時每一個檔案對應一個例項。

首先在類載入的時候讀取配置資訊,並進行初始化。

static {
    try {
        String filePath = new ClassPathResource("fdfs_client.conf").getFile().getAbsolutePath();;
        ClientGlobal.init(filePath);
    } catch (Exception e) {
        logger.error("FastDFS Client Init Fail!",e);
    }
}

ClientGlobal.init 方法會讀取配置檔案,並初始化對應的屬性。

FastDFSClient 類中第一個方法——檔案上傳。

1.檔案上傳

使用 FastDFS 提供的客戶端 storageClient 來進行檔案上傳,最後將上傳結果返回。

public static String[] upload(FastDFSFile file) {
    logger.info("File Name: " + file.getName() + "File Length:" + file.getContent().length);

    //檔案屬性資訊
    NameValuePair[] meta_list = new NameValuePair[1];
    meta_list[0] = new NameValuePair("author", file.getAuthor());

    long startTime = System.currentTimeMillis();
    String[] uploadResults = null;
    StorageClient storageClient=null;
    try {
        //獲取 
        storageClient = getStorageClient();
        //上傳
        uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
    } catch (IOException e) {
        logger.error("IO Exception when uploadind the file:" + file.getName(), e);
    } catch (Exception e) {
        logger.error("Non IO Exception when uploadind the file:" + file.getName(), e);
    }
    logger.info("upload_file time used:" + (System.currentTimeMillis() - startTime) + " ms");

    //驗證上傳結果
    if (uploadResults == null && storageClient!=null) {
        logger.error("upload file fail, error code:" + storageClient.getErrorCode());
    }
    //上傳檔案成功會返回 groupName。
    logger.info("upload file successfully!!!" + "group_name:" + uploadResults[0] + ", remoteFileName:" + " " + uploadResults[1]);
    return uploadResults;
}

其中:

  • NameValuePair,主要儲存檔案的一些基礎屬性,如作者資訊、建立時間等;
  • getStorageClient(),封裝了獲取客戶端的方法。

首先獲取 TrackerServer 資訊,使用 TrackerServer 構建出每次操作的客戶端例項 StorageClient。詳細程式碼如下:

private static StorageClient getStorageClient() throws IOException {
    TrackerServer trackerServer = getTrackerServer();
    StorageClient storageClient = new StorageClient(trackerServer, null);
    return  storageClient;
}

下面為封裝獲取 TrackerServer 的方法:

private static TrackerServer getTrackerServer() throws IOException {
    TrackerClient trackerClient = new TrackerClient();
    TrackerServer trackerServer = trackerClient.getConnection();
    return  trackerServer;
}

2.獲取檔案

根據 groupName 和檔名獲取檔案資訊。group 也可稱為卷,同組內伺服器上的檔案是完全相同的,同一組內的 storage server 之間是對等的,檔案上傳、刪除等操作可以在任意一臺 storage server 上進行。

public static FileInfo getFile(String groupName, String remoteFileName) {
    try {
        storageClient = new StorageClient(trackerServer, storageServer);
        return storageClient.get_file_info(groupName, remoteFileName);
    } catch (IOException e) {
        logger.error("IO Exception: Get File from Fast DFS failed", e);
    } catch (Exception e) {
        logger.error("Non IO Exception: Get File from Fast DFS failed", e);
    }
    return null;
}

3.下載檔案

根據 storageClient 的 API 獲取檔案的位元組流並返回:

public static InputStream downFile(String groupName, String remoteFileName) {
    try {
        StorageClient storageClient = getStorageClient();
        byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
        InputStream ins = new ByteArrayInputStream(fileByte);
        return ins;
    } catch (IOException e) {
        logger.error("IO Exception: Get File from Fast DFS failed", e);
    } catch (Exception e) {
        logger.error("Non IO Exception: Get File from Fast DFS failed", e);
    }
    return null;
}

4.刪除檔案

根據檔名和組刪除對應的檔案。

public static void deleteFile(String groupName, String remoteFileName)
        throws Exception {
    StorageClient storageClient = getStorageClient();
    int i = storageClient.delete_file(groupName, remoteFileName);
    logger.info("delete file successfully!!!" + i);
}

當使用 FastDFS 時,直接呼叫 FastDFSClient 對應的方法即可。

編寫上傳控制類

從 MultipartFile 中讀取檔案資訊,然後使用 FastDFSClient 將檔案上傳到 FastDFS 叢集中,封裝一個 saveFile() 方法用來呼叫上面封裝的 FastDFS 工具類,將 MultipartFile 檔案上傳到 FastDFS 中,並返回上傳後文件的地址資訊。

public String saveFile(MultipartFile multipartFile) throws IOException {
    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);
    try {
        fileAbsolutePath = FastDFSClient.upload(file);  //upload to fastdfs
    } catch (Exception e) {
        logger.error("upload file Exception!",e);
    }
    if (fileAbsolutePath==null) {
        logger.error("upload file failed,please upload again!");
    }
    String path=FastDFSClient.getTrackerUrl()+fileAbsolutePath[0]+ "/"+fileAbsolutePath[1];
    return path;
}

當上傳請求傳遞到後端時,呼叫上面方法 saveFile()。

@PostMapping("/upload") 
public String singleFileUpload(@RequestParam("file") MultipartFile file,
                               RedirectAttributes redirectAttributes) {
    if (file.isEmpty()) {
        redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
        return "redirect:uploadStatus";
    }
    try {
        String path=saveFile(file);
        redirectAttributes.addFlashAttribute("message",
                "You successfully uploaded '" + file.getOriginalFilename() + "'");
        redirectAttributes.addFlashAttribute("path","file path url '" + path + "'");
    } catch (Exception e) {
        logger.error("upload file failed",e);
    }
    return "redirect:/uploadStatus";
}

上傳成功之後,將檔案的路徑展示到頁面,效果圖如下:

在瀏覽器中訪問此 URL,可以看到成功通過 FastDFS 

 

在實際專案使用中可以給 Tracker 配置好固定域名,將返回的地址資訊儲存到資料庫中,前端業務呼叫時直接獲取地址展示即可。

 

整體上傳邏輯:在頁面上傳檔案後臺由 MultipartFile 接收,接著將 MultipartFile 檔案轉發為 FastDFS 檔案封裝類 FastDFSFile,呼叫 FastDFSClient 類的封裝方法,將檔案(FastDFSFile)上傳到 FastDFS 叢集中,成功後集群中檔案儲存位置返回到頁面。

FastDFS 是一款非常優秀的中小檔案儲存系統,結合 Spring Boot 的相關特性很容易將 FastDFS 整合到專案中,方便前端業務進行處理

 

 

 

三、採坑記錄

1解決Maven無法下載fastdfs-client-java依賴

因為fastdfs-client-java-1.27-SNAPSHOT.jar這個依賴包在maven中央倉庫是沒有的,

需要自己編譯原始碼成jar本地安裝到maven 的本地倉庫,安裝完以後就能正常引用了(注意:本地必須安裝了Maven,並配置好Maven環境變數)

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

1.下載fastdfs-client-java開發工具包

https://github.com/happyfish100/fastdfs-client-java

 

2.需要把fastdfs-client-java開發工具包打包到本地的Maven倉庫

  2.1解壓fastdfs-client-java-master

  2.2進入fastdfs-client-java目錄,在此處開啟命令視窗 cmd 

  2.3輸入 mvn clean install

3.構建一小會,如出現以下。則成功把fastdfs-client-java打包到本地的Maven倉庫

  

這時在本地你配置的的maven倉庫中就有fastdfs的包了。  

更新專案Maven,pom.xml檔案就不會出現找不到fastdfs-client-java依賴了。