ujtrnmtquj 发表于 2016-6-10 10:13:50

Java实现FTP批量大文件上传下载

用Java实现FTP批量大文件上传下载 
 猪圈 | 温故知新 |  0
原文:http://tech.it168.com/j/2007-10-18/200710182058687.shtml
  本文介绍了在Java中,如何使用Java现有的可用的库来编写FTP客户端代码,并开发成Applet控件,做成基于Web的批量、大文件的上传下载控件。文章在比较了一系列FTP客户库的基础上,就其中一个比较通用且功能较强的j-ftp类库,对一些比较常见的功能如进度条、断点续传、内外网的映射、在Applet中回调JavaScript函数等问题进行详细的阐述及代码实现,希望通过此文起到一个抛砖引玉的作用。
一、        概述
  笔者在实施一个项目过程中出现了一种基于Web的文件上传下载需求。在全省(或全国)各地的用户,需要将一些文件上传至某中心的文件服务器上。这些文件是用于一些大型的工程建设,可能涉及到上千万甚至上亿的建设工程。文件具有三个鲜明的特征:一是文件大,可能达到50M;二是文件数量多,有可能15个左右;三是数据安全性方面要求数字签名及数据加密。
  首先考虑到是基于HTTP的传输方式。但笔者通过比较很快发现满足上面的需求:
  1:用HTTP协议上传,似乎更适合web编程的方便性;上传小于1M文件速度要比用FTP协议上传文件略快。但对于批量及大文件的传输可能无能为力。当然,它也有它的优势,如不像FTP那样,必须在服务器端启动一个FTP服务。
  2:用FTP协议上传文件大于1M的文件速度比HTTP快。文件越大,上传的速度就比HTTP上传的速度快数倍。而且用java编写程序;FTP比HTTP方便。
  笔者曾经使用VB也写过ActiveX控件来进行批量文件的上传下载,其功能也很强大。只是由于没有对CAB文件或OCX进行专门的数字签名,因此需要进行客户端烦琐的设置,如设置安全站点、降低客户端的安全级别等等,因而放弃了些方案。
  同时考虑到在需在客户端对文件进行数字签名及数据加密,决定采用Applet的方式实现。。文件上传之前,在客户端可以获取本地USBKEY密钥信息,完成对上传文件的加密和签名处理。虽然采用Applet要求在客户端安装JRE运行时环境,给客户端的管理及使用带来一度的不方便性,但是相对起如此大量的文件及文件的安全性,这也许已经算是比较小的代价了。
  总结一下运行的环境为:
  FTP服务器端:Serv-U,专业的FTP服务器端程序,网上有现成的软件下载,当然读者也可能自己写一个服务器端的FTP文件接收程序来进行解释。如果没有特殊要求或功能的话,Serv-U应该可以满足我们一般上传下载的需求了;
  客户端:Java applet,当年让Java大火了一把的号称与微软的ActiveX相提并论的技术当然,现在Java出了JavaFX,是不是Applet的替代品呢?
  应用环境:Internet网,最终目的。
二、        Java FTP客户端库的选择
  让我们设想这样一个情形--我们想写一个纯Java的从一个远程计算机上运行的FTP服务器上传下载文件的应用程序;我们还希望能够得到那些供下载的远程文件的基本文件信息,如文件名、数据或者文件大小等。
  尽管从头开始写一个FTP协议处理程序是可能的,并且也许很有趣,但这项工作也是困难、漫长并且存在着潜在的危险。因为我们不愿意亲自花时间、精力、或者金钱去写这样的一个处理程序,所以我们转而采用那些已经存在的可重用的组件。并且很多的库存在于网上。
  找一个优秀的适合我们需要的Java FTP 客户端库并不像看起来那么简单。相反这是一项非常痛苦复杂的工作。首先找到一个FTP客户端库需要一些时间,其次,在我们找到所有的存在的库后,我们该选哪一个呢?每个库都适合不同的需求。这些库在性能上是不等价的,并且它们的设计上有着根本上的差别。每个类库都各具特点并使用不同的术语来描述它们。因而,评价和比较FTP客户端库是一件困难的事情。
  使用可重用组件是一种值得提倡的方法,但是在这种情况下,刚开始往往是令人气馁的。后来或许有点惭愧:在选择了一个好的FTP库后,其后的工作就非常简单了,按简单的规则来就行了。目前,已经有很多公开免费的ftp客户端类库,如simpleftp、J-ftp等,还有很多其他的ftpclient。如下表所示,表中未能全部列出,如读者有更好的客户端FTP类库,请进行进一步的补充。
 
FTP客户端类库名
备注
J-ftp
J-ftp
simpleftp
HTTP://www.jibble.org/files/simpleftp.jar
ftpclient
com.enterprisedt.net.ftp.FTPClient
FTPProtocol
com.ibm.network.ftp.protocol.FTPProtocol
FtpConnection 
net.sf.jftp.net.FtpConnection
FTPClient
org.apache.commons.net.ftp.FTPClient
FTPClient 
jshop.jnet.FTPClient
FtpClient 
sun.net.ftp.FtpClient 
FTP 
com.cqs.ftp.FTP 
Ftp
cz.dhl.ftp.Ftp
FTPClient
org.globus.io.ftp.FTPClient
  在本文中,笔者采用是J-ftp。这个是个开源的且功能十分强大的客户端FTP类库。笔者很喜欢,同时也向各位读者推荐一下。算了免费为它做一个广告。
三、        基本功能
1、 登陆
  采用FTP进行文件传输,其实本质上还是采用Java.net.socket进行通信。以下代码只是类net.sf.jftp.net.FtpConnection其中一个login方法。当然在下面的代码,为了节省版面,以及将一些原理阐述清楚,笔者将一些没必要的代码去掉了,如日志等代码。完整的代码请参考J-ftp的源代码或是笔者所以的示例源代码,后面的代码示例也同理:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394int             .username = username;this        status = LOGIN_OK; jcon = JConnection(host, port); if                                (getLine(POSITIVE) == )                            false                                        (!getLine(loginAck).startsWith(POSITIVE))                            (success(POSITIVE))                                                                                    false                                                                                    (msg){Log.debug();ok = ;status = GENERIC_FAILED;}} if                    true                                     String[] advSettings = String[]; if"OS/2"0                            "LIST"                        (LIST.equals()){//just get the first item (somehow it knows first is the//FTP list command)advSettings = LoadSet.loadSet(Settings.adv_settings); //*** IF FILE NOT FOUND, CREATE IT AND SET IT TO LIST_DEFAULTifnull                                                        new                                                                     0                    (LIST == ){LIST = LIST_DEFAULT;}}} if"MVS"0                            "LIST"                                    this            this                                    thisnew                 status;}此登陆方法中,有一个JConnection类,此类负责建立socket套接字   ,同时,此类是一种单独的线程,这样的好处是为了配合界面的变化,而将网络的套接字连接等工作做为单独的线程来处理,有利于界面的友好性。下面是net.sf.jftp.net.JConnection类的run方法,当然,此线程的启动是在JConnection类的构造方法中启动的。
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950void                         new                               new new                                                                    new new                                             true                      (Exception ex){ex.printStackTrace();Log.out(+ host +":" ")"          false                                  ((s != ) && !s.isClosed()){s.close();} ifnull                                                            (in != ){in.close();}}catch                                      "WARNING: got more errors trying to close socket and streams"                      true  public handleUpload(String file, String realName){if                                   "spawning new thread for this upload."                        (realName != ){t = FtpTransfer(host, port, getLocalPath(), getCachedPWD(),file, username, password, Transfer.UPLOAD,handler, listeners, realName, crlf);}else{t = FtpTransfer(host, port, getLocalPath(), getCachedPWD(),file, username, password, Transfer.UPLOAD,handler, listeners, crlf);} lastTransfer = t; return                                     (Settings.getNoUploadMultiThreading()){Log.out();}else{Log.out();} return null        }  在多线程的情况下,有一个单独的类net.sf.jftp.net .FtpTransfer,当然,多线程情况下,此类肯定是一个单独的线程了。与JConnection相似,其线程的启动也是在构造方法中启动。而在它的run方法中,进行文件的读取及传输。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143void             (handler.getConnections().get(file) == ){handler.addConnection(file, );}else (!pause){Log.debug(+ file);work = ;stat = ; return                hasPaused = ; while                                                100                (listeners != ){forint 0                                                                                                                                                                                                                      1                                                    (!work){ifnull                                            (i = ; i < listeners.size(); i++){((ConnectionListener) listeners.elementAt(i)).updateProgress(file,REMOVED,-);}}}}catch                                    true                ((handler.getConnectionSize() >= Settings.getMaxConnections()) &&(handler.getConnectionSize() > ) && work){try{stat = ;runner.sleep(); ifnull                                    (i = ; i < listeners.size(); i++){((ConnectionListener) listeners.elementAt(i)).updateProgress(file,QUEUED,-);}}else{break                                        (Exception ex){ex.printStackTrace();}} if                    (listeners != ){forint 0                                                                                                                                                                                                      1                                                    3            ;} started = ; try{runner.sleep(Settings.ftpTransferThreadPause);}catch                        new                         status = con.login(user, pass); if                    new                         (type.equals(UPLOAD)){ifnull                                                                                                                                                                            this                            (!pause){handler.removeConnection(file);}}  至于下载的过程,因为它是上传的逆过程,与上传的方法及写法大同小异,在些出于篇幅的考虑,并没有将代码列出,但其思想及思路完全一样。请读者参考源代码。
四、进度条
  可以想象,如果在上传或是下载的过程中,没有任何的提示,用户根本没法判断任务是否完成或是任务是否死了,常常由于上传时间或下载时间过长而误导用户。因此,进度条就显得非常的重要与实用。
  进度条的实现,其实说起来很简单。就是在程序中开启两个线程,第一个线程用于动态的改变界面上进度条的value值,而第二个线程则在上传或是下载的过程中,做成一个循环,在此循环中,每次读取一定数量如8192字节数的数据。然后传完此数据后,调用第一个线程中的updateProgress方法,来更新界面进度条的value值。
  而上传或下载的过程中(见上一节的FtpTransfer类的run方法),可以查看,con.upload(file, newName)方法,代码如下所示,
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253int             true        "ftp upload started: " this        stat; ifnullnew                     true            0                                    true                        false                                                       ":" 1            this            this                                                                                        100                        (Exception ex){} fireActionFinished();fireDirectoryUpdate();} try{Thread.sleep();}catch                        stat;}  此方法进行负责上传一定字节数量的内容,其实就是调用rawUpload方法,这里没列出,请参考源代码,而当传完此字节数据后,通过调用fireActionFinished()方法来调用主线程中的updateProgressBar()方法。其实代码如下:
123456789101112void     percent = () ((() lFileCompleteSize / () lFileSize) * 10000F);pbFile.setValue(percent);// System.out.println("================================================="+percent);pbFile.setString(lFileCompleteSize / 1024L + + lFileSize / 1024L+ );percent = () ((() lTotalCompleteSize / () lTotalSize) * 10000F);pbTotal.setString(lTotalCompleteSize / 1024L + + lTotalSize / 1024L+ );pbTotal.setValue(percent);repaint();public run(){try{newLine = con.getCRLF(); if                                                                new                                                     (Exception ex){ok = ;debug(+ port);}}}catch                                    true        ok = ; RandomAccessFile fOut = ;BufferedOutputStream bOut = ;RandomAccessFile fIn = ; try{if                            retry = ; while5null                                                                                                                                                (IOException e){sock = ;debug(); if5                                                    "Connection failed, tried 5 times - maybe try a higher timeout in Settings.java"                                        true                        e;}finally{ssock.close();} debug();}} if                            [] buf = byte                                buflen = ; //---------------download,下载----------------------if                                    (!justStream){try{if                                                            new                                 new "rw"                                int                                int                                                                                                                    (localfile == ){localfile = file;} File f2 = File(Settings.appHomeDir);f2.mkdirs(); File f = File(localfile); if                                                                                                                                    new new                                                                                                                                             (Exception ex){debug(+ + ex + );ok = ;}}                    (ok){try{out = BufferedOutputStream(sock.getOutputStream());}catch                                                    false                            "Can't get OutputStream"                                                 if                                                                                                                len = skiplen;char                                  whiletrue                                                                    read;                                    (in != ){read = in.read(buf);}else{read = fIn.read(buf);}                                                                         //System.out.println(file + " " + type+ " " + len + " " + read);if1                                                                            ;}                                    (newLine != ) {byte                                        0                                                                                                                                                    0                                                                         con.fireProgressUpdate(file, type, len);                                    (time()){//   Log.debugSize(len, false, false, file);}                                    (read == StreamTokenizer.TT_EOF){break                                                                                                     out.flush();                                                                                        (IOException ex){ok = ;debug();con.fireProgressUpdate(file, FAILED, -);ex.printStackTrace();}}}}}}catch                    "Can't connect socket to ServerSocket"                                                                            (out != ){out.flush();out.close();}}catch                                                     try{ifnull                                                                                                (Exception ex){ex.printStackTrace();}                                        (fOut != ){fOut.close();}}catch                                                     try{ifnull                                                                      ifnull                                                                            (Exception ex){ex.printStackTrace();}}                                            (Exception ex){debug(ex.toString());}        (!Settings.getFtpPasvMode()){try{ssock.close();}catch                                                         finished = ;        (ok){con.fireProgressUpdate(file, FINISHED, -);}else{con.fireProgressUpdate(file, FAILED, -);}} 
六、FTP端口映射
  FTP的数据连接有PASV和PORT两种,如果你的FTP服务器位于内网中,需要做端口映射。笔者刚开始时对FTP的网外网映射也是不怎么了解,因此开始走了不少的弯路,开始一直以为是自己的程序有问题,浪费了不少时间,希望通过这段,能让大家在开发的时候少花或不花这些无谓的时间与精力。
  PCD上曾经有一篇文章介绍过一种直接访问内网的方法,其实我们只要用端口映射工具,就可轻松实现穿透内网的目的。“端口映射器”就是一款这样的工具,更值得一提的是,它摆脱了命令行模式,提供了图形界面的操作环境。
  为了让各位能更加明白,先说一下原理。假设现在有一个局域网,主机为A,局域网内除了主机外,还有一台机器为B,B机器当然是通过主机A上网的。另外还有一台可上网的机器为C,与A和B并不在一个局域网内。通常情况下,C机器只能访问到A主机,而无法穿透局域网,访问到B。而通过端口映射后,当C机器访问主机A的指定端口时,主机A上的“端口映射器”就起作用了,它会把指定端口上的数据转到局域网内另一台机器的指定端口上,从而实现访问内网机器的目的。这样说,大家明白了吧。至于具体的如何进行配置,笔者认为应该不是件很难的事情,再说,网上这样的图形解释很多,请大家参考网络上的文章进行设置。
  当然,实现直接访问内网的优点是显而易见的,别的不说,起码FTP资源是被充分利用了。不过必须提醒读者的是,直接访问内网可能使内网的安全性受到威胁。笔者相信大部分朋友对主机安全的重要性还是重视的,但往往会忽略内网机器的安全设置。一旦你实现了直接访问内网,那就必须像对待主机一样对待内网机器,否则你的整个网络将可能处于危险状态。
六、    访问客户端资源
 
  Java应用程序环境的安全策略,对于不同的代码所拥有的不同资源的许可,它由一个Policy对象来表达。为了让Applet(或者运行在 SecurityManager下的一个应用程序)能够执行受保护的行为,例如读写文件,Applet(或 Java应用程序)必须获得那项操作的许可,安全策略文件就是用来实现这些许可。
  Policy对象可能有多个实体,虽然任何时候只能有一个起作用。当前安装的Policy对象,在程序中可以通过调用getPolicy方法得到,也可以通过调用setPolicy方法改变。Policy对象评估整个策略,返回一个适当的Permissions对象,详细说明哪些代码可以访问哪些资源。策略文件可以储存在无格式的ASCII文件或Policy类的二进制文件或数据库中。本文仅讨论无格式的ASCII文件的形式。
  在实际使用中,我们可以不需要自己手动去编写那么复杂的java.policy文件,特别是在不使用数字签名时。这时,我们完全可以借鉴JRE提供给我们的现成的 C:\Program Files\Java\jre1.5.0_12\lib\security\java.policy文件,根据我们的需要做相应的修改,本文就针对不使用数字签名情况编写安全策略文件。下面,是一个完整的在Windows NT/XP下使用的java.policy文件。在文件中,分别使用注释的形式说明了每个“permission”记录的用途。当然,不同的程序对资源访问权限的要求可能不一样,可以根据项目需要进行调整与选择。
12345678910111213141516171819202122232425262728293031323334353637383940                                                                                                                                                                                                                                                        //引入netscape类netscape.javascript.JSObject; netscape.javascript.JSException; public callJavaScript(String callBackJavascript) {JSObject window = JSObject.getWindow();     "document"        "textForm"//访问JavaScript form对象 textField=(JSObject)form.getMember();访问JavaScript text对象 text=(String) textField.getMember();             null// 参数用数组的形式表示。}八、运行效果
1.上传
(1).启动上传上面

(3).上传中

2.下载
(1)下载文件的保存路径

 (3)下载中

九、小结
在本文中,笔者将在实际项目中的上传下载问题的解决方案进行了阐述,通过采用FTP协议,来达到批量的,基本Web的大文件的上传下载。同时通过Applet技术实现在客户端对本地资源进行访问。就一些大家经常遇到的实际功能如进度条、断点续传、FTP内外网映射等问题进行了初步的探讨。这是笔者基于一些FTP的Java客户端库的基础应用。希望对读者有一定的借鉴作用。对其中一些未尽事宜进行补充。还有一些比较容易而且网上都有说明或实例的内容在此没有一一列举。如FTP在服务器端Serv-U软件如何建立FTP服务、Applet在JSP页面中的嵌入方式及参数传递方法、在Eclipse或是NetBeans下开始Applet等内容,由于篇幅的限制,并没有详尽描述,请读者参考网上的例子或其他参考资料。
代码下载:/FTPTransfer-code.rar
  
    
页: [1]
查看完整版本: Java实现FTP批量大文件上传下载