<script type="text/javascript"> </script> <script src="http://pagead2.googlesyndication.com/pagead/show_ads.js" type="text/javascript"></script>
在实施软件自动化测试时,我们一般首先需要得到一个完整的软件的待测版本。待测软件可以通过 FTP 服务, CIFS/SMB,SAMBA 服务等多种形式发布,如何确保自动化测试工具能完整的取得待测软件是我们要解决的一个问题。本文探讨了一种使用 java 开源软件包实现可扩展和可靠的下载工具的解决方案。
- 提供方便调用的接口。我们希望既能通过命令行调用,也可以方便的在其他程序代码如 Java 代码中调用此下载功能。
- 良好的异常处理,确保下载的完整性。异常经常发生在网络通讯,磁盘 I/O 上,导致下载过程终止。我们希望遇到这种异常后,下载进程只是挂起而不是退出,异常消除后能自动恢复下载。
- 提供良好可扩展性。待测软件可以通过诸如 FTP, SAMBA,HTTP 等多种协议发布,当新的协议被采用后,我们希望能以最快的时间来增加对协议的支持。
FTP 和 CIFS 协议简要描述
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 协议的全称为 (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 最开始是由 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 是一个纯 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。
一个简单的应用 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 } } } |
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 方法。
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 协议为例,得到所有待下载文件的一个方法是使用栈的结构:
Stack dirStack = new Stack(); 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 |
可靠下载的另一个条件是每一个文件都能完整下载。下面的代码片断描述了这个过程:
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),增加对一个新的协议的支持只需要增加一个具体的协议工厂即可,不需要修改原来的代码。类图描述如下:
以 上描述的使用 JAVA 开源包编写的文件下载组件涉及到 FTP, CIFS/SMB 两种协议,实际的测试项目中可能还会涉及到 HTTP 协议,由于采用的可扩展的结构,能很方便的实现对其它协议的支持。在我们的实际应用中,还涉及到下载状态提示(邮件,windows 信使),测试结果上传等,在此不再叙述。希望本文能给您的自动化测试工作提供帮助。