使用 java 开源资源实现通用可靠的下载组件
2009-09-12 00:00:00 来源:WEB开发网下载工具的需求
提供方便调用的接口。我们希望既能通过命令行调用,也可以方便的在其他程序代码如 Java 代码中调用此下载功能。
良好的异常处理,确保下载的完整性。异常经常发生在网络通讯,磁盘 I/O 上,导致下载过程终止。我们希望遇到这种异常后,下载进程只是挂起而不是退出,异常消除后能自动恢复下载。
提供良好可扩展性。待测软件可以通过诸如 FTP, SAMBA,HTTP 等多种协议发布,当新的协议被采用后,我们希望能以最快的时间来增加对协议的支持。
FTP 和 CIFS 协议简要描述
FTP
FTP(File Transfer Protocol) 的目标是提高文件的共享性,提供非直接使用远程计算机,使存储介质对用户透明和可靠高效地传送数据。 FTP 协议基于 client/server 模式,并且不同于使用单个连接的协议,FTP 使用双向的多连接,一个控制连接 (control connection) 和多个数据连接 (data connection) 。有主动和被动两种连接模式。
主动模式下,客户端随机打开一个大于 1024 的端口向服务器的命令端口 P 发起连接,同时开放 P+1 端口监听,并向服务器发出 PORT 命令,由服务器主动向客户端发起数据连接。
被动模式下,客户端随机打开一个大于 1024 的端口向服务器的命令端口 P 发起连接,同时开放 P+1 端口。然后向服务器发出 PASV 命令,服务器收到命令后,会开放一个大于 1024 的端口 N 进行监听,然后向客户端发出 PORT N 命令。客户端收到命令后,会通过 P+1 号端口连接服务器的端口 N,然后在两个端口之间进行数据传输。
被动模式下,客户端所有的端口将允许动态入站连接,这种模式通常应用在客户端处于防火墙之后的情况,因为防火墙通常配置为不允许外界访问防火墙之后主机。
CIFS
CIFS 协议的全称为 (Common Internet File System) 。 CIFS 的前身为 SMB 系统 (Server Message Block), 这个系统基于 NetBIOS 设定了一套文件共享协议,是 Microsoft 使用 NetBIOS 实现的一个网络文件 / 打印服务系统。随着 Internet 的流行,Microsoft 将原有缺乏技术文档的 SMB 协议进行整理,重新命名为 CIFS,并将它与 NetBIOS 相脱离,最终成为 Internet 上的一个标准协议。 CIFS/SMB 服务器能对外提供文件或打印服务,每个共享资源均有一个共享名。 SMB 协议一般使用广播的方式,但如果每次都使用广播的方式了解当前的网络资源,需要消耗大量的网络资源和浪费较长的查找时间。
Apache commons net 包和 JCIFS 包介绍
Apache commons net
Apache commons net 最开始是由 ORO 公司开发的一个叫 NetComponents 的商业 JAVA 包,于 1998 年贡献给 Apache 软件基金会,它目前支持多达 13 种网络协议,FTP/FTPS,NNTP ,SMTP ,POP3 ,Telnet ,TFTP ,Finger ,Whois ,rexec/rcmd/rlogin ,Time (rdate) and Daytime ,Echo ,Discard ,NTP/SNTP 。当前的最新版本为 2.0 。对于本文讨论的 FTP 协议,Apache commons net 通过与 FTP 站点建立的 Socket 连接,进行 FTP 命令和数据通信。它提供的主要 API 如下:
connect(String hostname, int port) | 与制定地址和端口的 FTP 站点建立 Socket 连接 |
login(String username, String password) | 使用制定用户名和密码登入 FTP 站点 |
logout() | 通过发送 QUIT 命令,登出 FTP 站点 |
disconnect() | 关闭和 FTP 站点的连接,并重置所有连接参数为初始值 |
sendNoOp() | 发送 NOOP 命令至 FTP 站点,与 noop() 类似。防止连接超时,也可以根据返回值检查连接的状态。 |
setBufferSize(int bufSize) | 设置内部缓冲区大小 |
setFileType(int fileType) | 设置文件传输的方式 |
enterLocalPassiveMode() | 在建立数据连接之前发送 PASV 命令至 FTP 站点,将数据连接模式设置为被动模式。 |
enterLocalActiveMode() | 在建立数据连接之前将数据连接模式设置为主动模式。 |
changeWorkingDirectory(String pathname) | 改变当前 FTP 会话的工作目录 |
listFiles() | 发送 LIST 命令至 FTP 站点,使用系统默认的机制列出当前工作目录的文件信息 |
makeDirectory(String pathname) | 在当前工作目录下新建子目录 |
retrieveFile(String remote, OutputStream local) | 取得 FTP 站点上的指定文件并写入指定的字节流中 |
storeFile(String remote, InputStream local) | 将指定的输入流写入 FTP 站点上的一个指定文件 |
JCIFS
JCIFS 是一个纯 JAVA 编写的实现 CIFS/SMB 协议的开源项目。它由 samba 组织负责维护开发。 JCIFS 是一个完整的,丰富的,具有可扩展能力且线程安全的客户端库。这一库可以应用于各种 JAVA 虚拟机访问遵循 CIFS/SMB 网络传输协议的网络资源,包括 Windows 下的共享资源和 Linux & Unix 下的 SAMBA 资源。
JCIFS 的开发方法类似 java 的文件操作功能,它的资源 url 定位:smb://{user}:{password}@{hostname}/{path},smb 为协议名,user 和 password 分别为共享文件机子的登陆名和密码,@ 之后是要访问的资源的主机名或 IP 地址。 path 是资源的共享文件夹名称和共享资源名。例如smb://administrator:password@9.125.242.49/buildFolder/responseFile.txt。
讨论使用 API 构建多线程和可靠的下载
一个简单的应用 apache commons net ftp 包的例子
清单 1:一个简单的应用 apache commons net ftp 包的例子
ftp = new FTPClient();
ftp.addProtocolCommandListener(new PrintCommandListener(
new PrintWriter(System.out)));
try
{
int reply;
ftp.connect(server);
System.out.println("Connected to " + server + ".");
reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply))
{
ftp.disconnect();
System.err.println("FTP server refused connection.");
System.exit(1);
}
}
catch (IOException e)
{
if (ftp.isConnected())
{
try
{
ftp.disconnect();
}
catch (IOException f)
{
// do nothing
}
}
System.err.println("Could not connect to server.");
e.printStackTrace();
System.exit(1);
}
try
{
if (!ftp.login(username, password))
{
ftp.logout();
error = true;
break __main;
}
System.out.println("Remote system is " + ftp.getSystemName());
if (binaryTransfer)
ftp.setFileType(FTP.BINARY_FILE_TYPE);
// Use passive mode as default because most of us are
// behind firewalls these days.
ftp.enterLocalPassiveMode();
if (storeFile)
{
InputStream input;
input = new FileInputStream(local);
ftp.storeFile(remote, input);
input.close();
}
else
{
OutputStream output;
output = new FileOutputStream(local);
ftp.retrieveFile(remote, output);
output.close();
}
ftp.logout();
}
catch (FTPConnectionClosedException e)
{
error = true;
System.err.println("Server closed connection.");
e.printStackTrace();
}
catch (IOException e)
{
error = true;
e.printStackTrace();
}
finally
{
if (ftp.isConnected())
{
try
{
ftp.disconnect();
}
catch (IOException f)
{
// do nothing
}
}
}
一个简单的应用 jcifs 包的例子
清单 2:一个简单的应用 jcifs 包的例子
try {
NtlmPasswordAuthentication auth = new NtlmPasswordAuthentication(
"hostname;username:password");
SmbFile smbFile = new SmbFile("smb://hostname/share/test.tar",auth);
int length = smbFile.getContentLength();
byte buffer[] = new byte[length];
SmbFileInputStream in = new SmbFileInputStream(smbFile);
while ((in.read(buffer)) != -1) {
System.out.write(buffer);
System.out.println(buffer.length);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
}
多线程下载
对于 FTP 来说,可以使用多个 FTPClient 实例同时连接到同一个 FTP 站点,也可以在线程之间共享一个 FTPClient 实例,但 FTPClient 不是线程安全的,这时需要额外处理 FTPClient 实例在临界资源上的同步访问。
首先定义并初始化一个任务队列,每个线程从这个队列中取得下载任务。
该任务队列采用线程安全类 LinkedBlockingQueue,它位于 JDK 的 java.util.concurrent 包中。是一个基于已链接节点的、范围任意的 blocking queue,是 Java Collections Framework 的成员。
每个线程从任务队列中取任务时,调用 LinkedBlockingQueue 的 poll 方法,添加任务时调用 put 方法。
清单 3:建立下载任务队列
LinkedBlockingQueue LBQueue = new LinkedBlockingQueue();
LBQueue.put(new buildFileInfo(...));
FTPClient ftpConnection = new FTPClient();
ftpConnection.setBufferSize(4096);
buildFileInfo fileInfo;
while ((fileInfo =LBQueue.poll()) != null) {
//TODO: ftpConnection.retrieveFile()
}
如何确保可靠的下载
可靠下载的第一个条件是准确的文件个数。以 FTP 协议为例,得到所有待下载文件的一个方法是使用栈的结构:
清单 4:确保可靠下载 1
Stack<String> dirStack = new Stack<String>();
FTPFile[] ftpFiles = null;
dirStack.push(latestBuildPath);
while (!dirStack.empty()) {
String currentDir = dirStack.pop();
...
try {
...
ftpFiles = ((FTPClient) connection).listFiles();
} catch (Exception e) {
...
}
for (FTPFile ftpFile : ftpFiles) {
if (ftpFile.isDirectory()) {
dirStack.push(currentDir + ftpFile.getName());
} else {
try {
lbQueue.put(...);//add download task
} catch (InterruptedException e) {
...
}
}
}//~for
}//~while
可靠下载的另一个条件是每一个文件都能完整下载。下面的代码片断描述了这个过程:
清单 5:确保可靠下载 2
while ((fileInfo = BC_FTP.lbQueue.poll()) != null) {
try {
if (!fileInfo.getWorkingObject().equals(workingDirectory)) {
workingDirectory = (String)fileInfo.getWorkingObject();
if(ftpConnection.changeWorkingDirectory(workingDirectory)){//1
throw new changeWDException("error when changing WD");
}
}
File localFolder = new File(fileInfo.getCurrentLocalDir());
localFolder.mkdirs();//2
File localFile = new File(fileInfo.getCurrentLocalDir()
+ fileInfo.getFileName());//3
if (localFile.exists()
&& localFile.length() == fileInfo.getFileSize()) {
return;
}
FileOutputStream fos = new FileOutputStream(localFile);
if (!ftpConnection.retrieveFile(fileInfo.getFileName(), fos)) {
throw new retrieveFileException("error when retrieve file");//4
}
fos.close();
BC_FTP.hasExceptionFlag = false;
} catch (Exception e) {
try {
BC_FTP.lbQueue.put(fileInfo);
} catch (InterruptedException e1) {
}
BC_FTP.hasExceptionFlag = true;
if (!BC_FTP.ftpIsAlive(ftpConnection)) {
ftpConnection = BC_FTP.getFTPConnection();
}
}
}
每个线程在下载一个单独的文件时,可能会遇到以下异常:向 FTP 服务器发送 CWD 命令失败,在本地文件系统读写文件失败,将 FTP 服务器的文件流写入本地文件失败。由于在这些异常抛出前,线程已经从任务队列中取得并删除了该任务,而一旦发生以上异常,将导致文件下载失败,所以,如果捕获到异常,则应当把当前任务重新送回任务队列。
构建可扩展的框架
以上只涉及了 FTP 和 SMB-CIFS 两种协议的软件发布方式,如果发布方式采用新的协议,我们如何让这个下载工具只做少量修改就能集成新的协议支持呢?我们可以将所有协议的下载过程抽象为一个工厂角色 (BC_Factory),每个具体的协议为具体的工厂角色 (BC_FTP)Factory, BC_SMB_Factory),增加对一个新的协议的支持只需要增加一个具体的协议工厂即可,不需要修改原来的代码。类图描述如下:
图 1. 下载组件 UML 类图
图片看不清楚?请点击这里查看原图(大图)。
总结
以上描述的使用 JAVA 开源包编写的文件下载组件涉及到 FTP, CIFS/SMB 两种协议,实际的测试项目中可能还会涉及到 HTTP 协议,由于采用的可扩展的结构,能很方便的实现对其它协议的支持。在我们的实际应用中,还涉及到下载状态提示(邮件,windows 信使),测试结果上传等,在此不再叙述。希望本文能给您的自动化测试工作提供帮助。
本文示例源代码或素材下载
赞助商链接