基于RMI服務傳輸大文件的完整解決方案

allenye 8年前發布 | 18K 次閱讀 Java RMI 網絡技術

來自: http://www.cnblogs.com/shangbingbing/p/5221028.html

基于RMI服務傳輸大文件,分為上傳和下載兩種操作,需要注意的技術點主要有三方面,第一,RMI服務中傳輸的數據必須是可序列化的。第二,在傳輸大文件的過程中應該有進度提醒機制,對于大文件傳輸來說,這點很重要,因為大文件的傳輸時間周期往往比較長,必須實時告知用戶大文件的傳輸進度。第三,針對大文件的讀取方式,如果采用一次性將大文件讀取到byte[]中是不現實的,因為這將受制于JVM的可用內存,會導致內存溢出的問題。

筆者實驗的基于RMI服務傳輸大文件的解決方案,主要就是圍繞以上三方面進行逐步解決的。下面將分別就上傳大文件和下載大文件進行闡述。

1. 基于RMI服務上傳大文件

1.1. 設計思路

上傳大文件分為兩個階段,分別為“CS握手階段”和“文件內容上傳更新階段”。

CS握手階段,又可稱之為客戶端與服務端之間的基本信息交換階段。客戶端要確定待上傳的本地文件和要上傳到服務端的目標路徑。讀取本地文件,獲取文件長度,將本地文件長度、本地文件路徑和服務端目標路徑等信息通過接口方法傳遞到服務端,然后由服務端生成具有唯一性的FileKey信息,并返回到客戶端,此FileKey作為客戶端與服務端之間數據傳輸通道的唯一標示。這些信息數據類型均為Java基本數據類型,因此都是可序列化的。此接口方法就稱之為“握手接口”,通過CS握手,服務端能確定三件事:哪個文件要傳輸過來、這個文件的尺寸、這個文件將存放在哪里,而這三件剛好是完成文件上傳的基礎。

然后就是“文件內容上傳更新階段”。客戶端打開文件后,每次讀取一定量的文件內容,譬如每次讀取(1024 * 1024 * 2)byte的數據,并將此批數據通過“上傳更新”接口方法傳輸到服務端,由服務端寫入輸出流中,同時檢查文件內容是否已經傳輸完畢,如果已經傳輸完畢,則執行持久化flush操作在服務端生成文件;客戶端每次傳輸一批數據后,就更新“已傳輸數據總量”標示值,并與文件總長度進行比對計算,從而得到文件上傳進度,實時告知用戶。綜上所述,客戶端循環讀取本地文件內容并傳輸到服務端,直到文件內容讀取上傳完畢為止。

1.2. 功能設計

1.2.1 文件上傳相關狀態信息的管理

大文件上傳的過程中,在服務端,最重要的是文件上傳過程相關狀態信息的精確管理,譬如,文件總長度、已上傳字節總數、文件存儲路徑等等。而且要保證在整個上傳過程中數據的實時更新和絕對不能丟失,并且在文件上傳完畢后及時清除這些信息,以避免服務端累計過多失效的狀態數據。

鑒于此,我們設計了一個類 RFileUploadTransfer 來實現上述功能。代碼如下:

   /**

  • Description: 文件上傳過程相關狀態信息封裝類。<br>
  • Copyright: Copyright (c) 2016<br>
  • Company: 河南電力科學研究院智能電網所<br>
  • @author shangbingbing 2016-01-01編寫
  • @version 1.0 */ public class RFileUploadTransfer implements Serializable { private static final long serialVersionUID = 1L; private String fileKey; //客戶端文件路徑 private String srcFilePath; //服務端上傳目標文件路徑 private String destFilePath; //文件尺寸 private int fileLength = 0; //已傳輸字節總數 private int transferByteCount = 0; //文件是否已經完整寫入服務端磁盤中 private boolean isSaveFile = false; private OutputStream out = null;

    public RFileUploadTransfer(String srcFilePath, int srcFileLength, String destFilePath) {

     this.fileKey = UUID.randomUUID().toString();
     this.srcFilePath = srcFilePath;
     this.fileLength = srcFileLength;
     this.destFilePath = destFilePath;
    
     File localFile = new File(this.destFilePath);
     if(localFile.getParentFile().exists() == false) {
         localFile.getParentFile().mkdirs();
     }
     try {
         this.out = new FileOutputStream(localFile);
     } catch (FileNotFoundException e) {
         e.printStackTrace();
     }
    

    }

    public String getFileKey() {

     return fileKey;
    

    } public String getSrcFilePath() {

     return srcFilePath;
    

    } public String getDestFilePath() {

     return destFilePath;
    

    } public boolean isSaveFile() {

     return isSaveFile;
    

    }

    public void addContentBytes(byte[] bytes) {

     try {
         if(bytes == null || bytes.length == 0) {
             return;
         }
         if(this.transferByteCount + bytes.length > this.fileLength) {
             //如果之前已經傳輸的數據長度+本批數據長度>文件長度的話,說明這批數據是最后一批數據了;
             //由于本批數據中可能會存在有空字節,所以需要篩選出來。
             byte[] contents = new byte[this.fileLength - this.transferByteCount];
             for(int i=0;i<contents.length;i++) {
                 contents[i] = bytes[i];
             }
             this.transferByteCount = this.fileLength;
             this.out.write(contents);
         } else {
             //說明本批數據并非最后一批數據,文件還沒有傳輸完。
             this.transferByteCount += bytes.length;
             this.out.write(bytes);
         }
         if(this.transferByteCount >= this.fileLength) {
             this.out.flush();
             this.isSaveFile = true;
             if(this.out != null) {
                 try {
                     this.out.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
     } catch (Exception ex) {
         ex.printStackTrace();
     }
    

    } }</pre>

    然后,在RMI服務接口方法實現類中構建一個線程安全的集合,用來存儲管理各個大文件的傳輸過程,代碼如下:

      /**

    • 上傳文件狀態監視器 */ private Hashtable<String,RFileUploadTransfer> uploadFileStatusMonitor = new Hashtable<String,RFileUploadTransfer>();</pre>

      1.2.2 CS握手接口設計

    CS握手接口名稱為 startUploadFile ,主要功能就是傳輸交換文件基本信息,構建文件上傳過程狀態控制對象。其在接口實現類中的代碼如下所示:

      @Override
    public String startUploadFile(String localFilePath, int localFileLength, String remoteFilePath) throws RemoteException {
     RFileUploadTransfer fileTransfer = new RFileUploadTransfer(localFilePath,localFileLength,remoteFilePath);
     if(this.uploadFileStatusMonitor.containsKey(fileTransfer.getFileKey())) {

     this.uploadFileStatusMonitor.remove(fileTransfer.getFileKey());
    

    } this.uploadFileStatusMonitor.put(fileTransfer.getFileKey(), fileTransfer); return fileTransfer.getFileKey(); }</pre>

    1.2.3 文件內容上傳更新接口設計

    文件內容上傳更新接口名稱為 updateUploadProgress ,主要功能是接收客戶端傳輸過來的文件內容byte[]信息。其在接口實現類中的代碼如下所示:

      @Override
    public boolean updateUploadProgress(String fileKey, byte[] contents) throws RemoteException {
     if(this.uploadFileStatusMonitor.containsKey(fileKey)) {

     RFileUploadTransfer fileTransfer = this.uploadFileStatusMonitor.get(fileKey);
     fileTransfer.addContentBytes(contents);
     if(fileTransfer.isSaveFile()) {
         this.uploadFileStatusMonitor.remove(fileKey);
     }
    

    } return true; }</pre>

    1.2.4 客戶端設計

    客戶端的主要功能是打開本地文件,按批讀取文件內容byte[]信息,調用RMI接口方法進行傳輸,同時進行傳輸進度的提醒。下面是筆者本人采用swing開發的測試代碼,采用JProgressBar進行進度的實時提醒。

      progressBar.setMinimum(0);
    progressBar.setMaximum(100);
    InputStream is = null;
    try {
     File srcFile = new File(localFilePath);
     int fileSize = (int)srcFile.length();
     String fileKey = getFileManageService().startUploadFile(localFilePath, fileSize, remoteFilePath);

    byte[] buffer = new byte[1024 1024 2]; int offset = 0; int numRead = 0; is = new FileInputStream(srcFile); while(-1 != (numRead=is.read(buffer))) {

     offset += numRead;
     getFileManageService().updateUploadProgress(fileKey, buffer);
     double finishPercent = (offset * 1.0 / fileSize) * 100;
     progressBar.setValue((int)finishPercent);
    

    } if(offset != fileSize) {

     throw new IOException("不能完整地讀取文件 " + localFilePath);
    

    } else {

     progressBar.setValue(100);
    

    } } catch (Exception ex) { ex.printStackTrace(); } finally { try {

     if(is != null) {
         is.close();
     }
    

    } catch (IOException e) {

     e.printStackTrace();
    

    } }</pre> 實例界面截圖如下所示:

    2. 基于RMI服務下載大文件

    2.1. 設計思路

    下載大文件分為兩個階段,分別為“CS握手階段”和“文件內容下載更新階段”。

    CS握手階段,又可稱之為客戶端與服務端之間的基本信息交換階段。服務端讀取待下載的文件,獲取文件長度,生成具有唯一性的FileKey信息,并將文件長度、FileKey信息傳輸到客戶端,此FileKey作為客戶端與服務端之間數據傳輸通道的唯一標示。這些信息數據類型均為Java基本數據類型,因此都是可序列化的。此接口方法就稱之為“握手接口”,通過CS握手,客戶端能確定兩件事:哪個文件要下載、這個文件的尺寸,而這兩件剛好是完成文件下載的基礎。

    然后就是“文件內容下載更新階段”。服務端打開文件后,每次讀取一定量的文件內容,譬如每次讀取(1024 * 1024 * 2)byte的數據,并將此批數據通過“下載更新”接口方法傳輸到客戶端,由客戶端寫入輸出流中,同時檢查文件內容是否已經傳輸完畢,如果已經傳輸完畢,則執行持久化flush操作在客戶端生成文件;客戶端每接收一批數據后,就更新“已下載數據總量”標示值,并與文件總長度進行比對計算,從而得到文件下載進度,實時告知用戶。綜上所述,客戶端循環獲取服務端傳輸過來的文件內容,直到服務端文件內容讀取傳輸完畢為止。

    2.2. 功能設計

    2.2.1 文件下載相關狀態信息的管理

    大文件下載的過程中,在服務端,最重要的是文件下載過程相關狀態信息的精確管理,譬如,文件總長度、已下載字節總數等等。而且要保證在整個下載過程中數據的實時更新和絕對不能丟失,并且在文件下載完畢后及時清除這些信息,以避免服務端累計過多失效的狀態數據。

    鑒于此,我們設計了一個類 RFileDownloadTransfer 來實現上述功能。代碼如下:

      /**

  • Description: 文件下載過程相關狀態信息封裝類。<br>
  • Copyright: Copyright (c) 2016<br>
  • Company: 河南電力科學研究院智能電網所<br>
  • @author shangbingbing 2016-01-01編寫
  • @version 1.0 */ public class RFileDownloadTransfer implements Serializable { private static final long serialVersionUID = 1L; private String fileKey; //服務端待下載文件路徑 private String srcFilePath; //待下載文件尺寸 private int fileLength = 0; private InputStream inputStream = null; //已經傳輸文件內容字節總數 private int transferByteCount = 0; //服務端待下載文件是否已經讀取完畢 private boolean isReadFinish = false;

    public RFileDownloadTransfer(String srcFilePath) {

     this.fileKey = UUID.randomUUID().toString();
     this.srcFilePath = srcFilePath;
     File srcFile = new File(srcFilePath);
     this.fileLength = (int)srcFile.length();
     try {
         this.inputStream = new FileInputStream(srcFile);
     } catch (FileNotFoundException e) {
         e.printStackTrace();
     }
    

    } public String getFileKey() {

     return fileKey;
    

    } public String getSrcFilePath() {

     return srcFilePath;
    

    } public int getFileLength() {

     return fileLength;
    

    } public boolean isReadFinish() {

     return isReadFinish;
    

    } public byte[] readBytes() {

     try {
         if(this.inputStream == null) {
             return null;
         }
         byte[] buffer = new byte[1024 * 1024 * 5];
         this.inputStream.read(buffer);
         this.transferByteCount += buffer.length;
         if(this.transferByteCount >= this.fileLength) {
             this.isReadFinish = true;
             this.transferByteCount = this.fileLength;
         }
         return buffer;
     } catch (Exception ex) {
         ex.printStackTrace();
         return null;
     }
    

    }

    public void closeInputStream() {

     if(this.inputStream != null) {
         try {
             this.inputStream.close();
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
    

    } }</pre>

    然后,在RMI服務接口方法實現類中構建一個線程安全的集合,用來存儲管理各個大文件的傳輸過程,代碼如下:

      /**

    • 下載文件狀態監視器 */ private Hashtable<String,RFileDownloadTransfer> downloadFileStatusMonitor = new Hashtable<String,RFileDownloadTransfer>();</pre>

      2.2.2 CS握接口設計

    CS握手接口名稱為 startDownloadFile ,主要功能就是傳輸交換文件基本信息,構建文件下載過程狀態控制對象。其在接口實現類中的代碼如下所示:

      @Override
    public Map<String, String> startDownloadFile(String srcFilePath) throws RemoteException {
     RFileDownloadTransfer fileTransfer = new RFileDownloadTransfer(srcFilePath);
     if(this.downloadFileStatusMonitor.containsKey(fileTransfer.getFileKey())) {

     this.downloadFileStatusMonitor.remove(fileTransfer.getFileKey());
    

    } this.downloadFileStatusMonitor.put(fileTransfer.getFileKey(), fileTransfer); Map<String,String> fileInfoList = new HashMap<String,String>(); fileInfoList.put("fileLength", String.valueOf(fileTransfer.getFileLength())); fileInfoList.put("fileKey", fileTransfer.getFileKey()); return fileInfoList; }</pre>

    2.2.3 文件內容下載更新接口設計

    文件內容下載更新接口名稱為 updateDownloadProgress ,主要功能是接收客戶端傳輸過來的文件內容byte[]信息。其在接口實現類中的代碼如下所示:

      @Override
    public byte[] updateDownloadProgress(String fileKey) throws RemoteException {
     if(this.downloadFileStatusMonitor.containsKey(fileKey)) {

     RFileDownloadTransfer fileTransfer = this.downloadFileStatusMonitor.get(fileKey);
     byte[] bytes = fileTransfer.readBytes();
     if(fileTransfer.isReadFinish()) {
         fileTransfer.closeInputStream();
         this.downloadFileStatusMonitor.remove(fileKey);
     }
     return bytes;
    

    } return null; }</pre>

    2.2.4 客戶端設計

    客戶端的主要功能是創建磁盤文件,逐批次獲取文件內容byte[]信息,并將其寫入輸出流中,同時進行傳輸進度的提醒,并最終生成完整的文件。下面是筆者本人采用swing開發的測試代碼,采用JProgressBar進行進度的實時提醒。

      Map<String,String> fileInfoList = getFileManageService().startDownloadFile(remoteFilePath);
    int fileLength = Integer.valueOf(fileInfoList.get("fileLength"));
    String fileKey = fileInfoList.get("fileKey");
    int transferByteCount = 0;

progressBar.setMinimum(0); progressBar.setMaximum(100);

OutputStream out = new FileOutputStream(localFilePath); while(true) { if(transferByteCount >= fileLength) { break; }

byte[] bytes = getFileManageService().updateDownloadProgress(fileKey);
if(bytes == null) {
    break;
}

if(transferByteCount + bytes.length > fileLength) {
    //如果之前已經傳輸的數據長度+本批數據長度>文件長度的話,說明這批數據是最后一批數據了;
    //那么本批數據中將會有空字節,需要篩選出來。
    byte[] contents = new byte[fileLength - transferByteCount];
    for(int i=0;i<contents.length;i++) {
        contents[i] = bytes[i];
    }
    transferByteCount = fileLength;
    out.write(contents);
} else {
    //說明本批數據并非最后一批數據,文件還沒有傳輸完。
    transferByteCount += bytes.length;
    out.write(bytes);
}

double dblFinishPercent = (transferByteCount * 1.0 / fileLength) * 100;
int finishPercent = (int)dblFinishPercent;
if(finishPercent > 100) {
    finishPercent = 100;
}
progressBar.setValue(finishPercent);

}

if(transferByteCount != fileLength) { LogInfoUtil.printLog("不能完整地讀取文件 " + remoteFilePath); } else { progressBar.setValue(100); out.flush(); if(out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } }</pre>

實例界面截圖如下所示:

【完】

作者:商兵兵

單位:河南省電力科學研究院智能電網所

QQ:52190634

主頁: http://www.cnblogs.com/shangbingbing

空間: http://shangbingbing.qzone.qq.com

 本文由用戶 allenye 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!