基于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(); } } }
然后,在RMI服务接口方法实现类中构建一个线程安全的集合,用来存储管理各个大文件的传输过程,代码如下:
/** * 上传文件状态监视器 */ private Hashtable<String,RFileUploadTransfer> uploadFileStatusMonitor = new Hashtable<String,RFileUploadTransfer>();
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(); }
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; }
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(); } }
实例界面截图如下所示:
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(); } } } }
然后,在RMI服务接口方法实现类中构建一个线程安全的集合,用来存储管理各个大文件的传输过程,代码如下:
/** * 下载文件状态监视器 */ private Hashtable<String,RFileDownloadTransfer> downloadFileStatusMonitor = new Hashtable<String,RFileDownloadTransfer>();
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; }
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; }
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(); } } }
实例界面截图如下所示:
【完】
作者:商兵兵
单位:河南省电力科学研究院智能电网所
QQ:52190634