服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

用java实现ftp批量大文件上传下载(一)


本文介绍了在java中,如何使用java现有的可用的库来编写ftp客户端代码,并开发成applet控件,做成基于web的批量、大文件的上传下载控件。文章比较了一系列详尽的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
http://jaist.dl.sourceforge.net/sourceforge/j-ftp/j-ftp-1.50.tar.gz
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的源代码或是笔者所以的示例源代码,后面的代码示例也同理:

public int login(string username, string password){this.username = username;this.password = password;int status = login_ok;jcon = new jconnection(host, port);if(jcon.isthere()){in = jcon.getreader();if(getline(positive) == null)//ftp220_service_ready) == null) {ok = false;status = offline;}if(!getline(loginack).startswith(positive))//ftp230_logged_in)) {if(success(positive))//ftp230_logged_in)) {}else {ok = false;status = wrong_login_data;}}}else {if(msg){log.debug("ftp not available!");ok = false;status = generic_failed;}}if(ok){connected = true;system();binary();string[] advsettings = new string[6];if(getostype().indexof("os/2") >= 0){list_default = "list";}if(list.equals("default")){//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_default if(advsettings == null){list = list_default;saveset s = new saveset(settings.adv_settings, list);}else {list = advsettings[0];if(list == null){list = list_default;}}}if(getostype().indexof("mvs") >= 0){list = "list";}//*** firedirectoryupdate(this);fireconnectioninitialized(this);}else {fireconnectionfailed(this, new integer(status).tostring());}return status;}
此登陆方法中,有一个jconnection类,此类负责建立socket套接字   ,同时,此类是一种单独的线程,这样的好处是为了配合界面的变化,而将网络的套接字连接等工作做为单独的线程来处理,有利于界面的友好性。下面是net.sf.jftp.net.jconnection类的run方法,当然,此线程的启动是在jconnection类的构造方法中启动的。

public void run(){try{s = new socket(host, port);localport = s.getlocalport();//if(time > 0) s.setsotimeout(time); out = new printstream(new bufferedoutputstream(s.getoutputstream(),settings.buffersize));in = new bufferedreader(new inputstreamreader(s.getinputstream()),settings.buffersize);isok = true;// } }catch(exception ex){ex.printstacktrace();log.out("warning: connection closed due to exception (" + host +":" + port + ")");isok = false;try{if((s != null) && !s.isclosed()){s.close();}if(out != null){out.close();}if(in != null){in.close();}}catch(exception ex2){ex2.printstacktrace();log.out("warning: got more errors trying to close socket and streams");}}established = true;}
此run方法中的socket这里说明一下,此类实现客户端套接字(也可以就叫“套接字”),套接字是两台机器之间的通信端点。套接字的实际工作由 socketimpl 类的实例执行。应用程序通过更改创建套接字实现的套接字工厂可以配置它自身,以创建适合本地防火墙的套接字。具体的说明请参考jdk5 的api说明,最好是中文的。呵呵。
 
    2、上传下载
文件的上传可以分成多线程及单线程,在单线程情况下比较简单,而在多线程的情况下,要处理的事情要多点,同时也要小心很多。下面是net.sf.jftp.net.ftpconnection的上传handleupload方法。已经考虑了单线程及多线程两种不同的类型。

public int handleupload(string file, string realname){if(settings.getenablemultithreading() &&(!settings.getnouploadmultithreading())){log.out("spawning new thread for this upload.");ftptransfer t;if(realname != null){t = new ftptransfer(host, port, getlocalpath(), getcachedpwd(),file, username, password, transfer.upload,handler, listeners, realname, crlf);}else{t = new ftptransfer(host, port, getlocalpath(), getcachedpwd(),file, username, password, transfer.upload,handler, listeners, crlf);}lasttransfer = t;return new_transfer_spawned;}else{if(settings.getnouploadmultithreading()){log.out("upload multithreading is disabled.");}else{log.out("multithreading is completely disabled.");}return (realname == null) ? upload(file) : upload(file, realname);}}
在多线程的情况下,有一个单独的类net.sf.jftp.net .ftptransfer,当然,多线程情况下,此类肯定是一个单独的线程了。与jconnection相似,其线程的启动也是在构造方法中启动。而在它的run方法中,进行文件的读取及传输。

public void run(){if(handler.getconnections().get(file) == null){handler.addconnection(file, this);}else if(!pause){log.debug("transfer already in progress: " + file);work = false;stat = 2;return;}boolean haspaused = false;while(pause){try{runner.sleep(100);if(listeners != null){for(int i = 0; i < listeners.size(); i++){((connectionlistener) listeners.elementat(i)).updateprogress(file,paused,-1);}}if(!work){if(listeners != null){for(int i = 0; i < listeners.size(); i++){((connectionlistener) listeners.elementat(i)).updateprogress(file,removed,-1);}}}}catch(exception ex){}haspaused = true;}while((handler.getconnectionsize() >= settings.getmaxconnections()) &&(handler.getconnectionsize() > 0) && work){try{stat = 4;runner.sleep(400);if(!haspaused && (listeners != null)){for(int i = 0; i < listeners.size(); i++){((connectionlistener) listeners.elementat(i)).updateprogress(file,queued,-1);}}else{break;}}catch(exception ex){ex.printstacktrace();}}if(!work){if(listeners != null){for(int i = 0; i < listeners.size(); i++){((connectionlistener) listeners.elementat(i)).updateprogress(file,removed,-1);}}handler.removeconnection(file);stat = 3;return;}started = true;try{runner.sleep(settings.ftptransferthreadpause);}catch(exception ex){}con = new ftpconnection(host, port, remotepath, crlf);con.setconnectionhandler(handler);con.setconnectionlisteners(listeners);int status = con.login(user, pass);if(status == ftpconnection.login_ok){file f = new file(localpath);con.setlocalpath(f.getabsolutepath());if(type.equals(upload)){if(newname != null){transferstatus = con.upload(file, newname);}else{transferstatus = con.upload(file);}}else{transferstatus = con.download(file,this.newname);}}if(!pause){handler.removeconnection(file);}}
至于下载的过程,因为它是上传的逆过程,与上传的方法及写法大同小异,在些出于篇幅的考虑,并没有将代码列出,但其思想及思路完全一样。请读者参考源代码。
四、进度条
 
可以想象,如果在上传或是下载的过程中,没有任何的提示,用户根本没法判断任务是否完成或是任务是否死了,常常由于上传时间或下载时间过长而误导用户。
因此,进度条就显得非常的重要与实用。
进度条的实现,其实说起来很简单。就是在程序中开启两个线程,第一个线程用于动态的改变界面上进度条的value值,而第二个线程则在上传或是下载的过程中,做成一个循环,在此循环中,每次读取一定数量如8192字节数的数据。然后传完此数据后,调用第一个线程中的updateprogress方法,来更新界面进度条的value值。
而上传或下载的过程中(见上一节的ftptransfer类的run方法),可以查看,con.upload(file, newname)方法,代码如下所示,

public int upload(string file, string realname, inputstream in){hasuploaded = true;log.out("ftp upload started: " + this);int stat;if((in == null) && new file(file).isdirectory()){shortprogress = true;filecount = 0;basefile = file;datatype = dataconnection.putdir;isdirupload = true;stat = uploaddir(file);shortprogress = false;//system.out.println(filecount + ":" + basefile); fireprogressupdate(basefile,dataconnection.dfinished + ":" + filecount, -1);fireactionfinished(this);firedirectoryupdate(this);}else {datatype = dataconnection.put;stat = rawupload(file, realname, in);try {thread.sleep(100);}catch(exception ex){}fireactionfinished(this);firedirectoryupdate(this);}try {thread.sleep(500);}catch(exception ex){}return stat;}
此方法进行负责上传一定字节数量的内容,其实就是调用rawupload方法,这里没列出,请参考源代码,而当传完此字节数据后,通过调用fireactionfinished()方法来调用主线程中的updateprogressbar()方法。其实代码如下:

protected void updateprogressbar() {int percent = (int) (((float) lfilecompletesize / (float) lfilesize) * 10000f);pbfile.setvalue(percent);// system.out.println("================================================="+percent); pbfile.setstring(lfilecompletesize / 1024l + "/" + lfilesize / 1024l + " kb");percent = (int) (((float) ltotalcompletesize / (float) ltotalsize) * 10000f);pbtotal.setstring(ltotalcompletesize / 1024l + "/" + ltotalsize / 1024l + " kb");pbtotal.setvalue(percent);repaint();}
上面用了两个进度条,第一个进度条表示当前文件的上传或下载进度,第二个进度条表示所有文件下载或上传的进度。同时,为了产生进度条的移动或变化进度幅度比较明显,通过pbfile.setmaximum(10000)及pbtotal.setmaximum(10000)将进度条的最大值设置成10000,而不是平时我们所设置的100。笔者认为这样比较好看,因为有的时候上传或下载的时候由于网络原因,可能变化比较小。若设置成100则变化不是特别明显。

扫描关注微信公众号