服务器之家

服务器之家 > 正文

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

时间:2020-06-08 12:10     来源/作者:ycb1689

本文介绍了在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类库,请进行进一步的补充。

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

在本文中,笔者采用是J-ftp。这个是个开源的且功能十分强大的客户端FTP类库。笔者很喜欢,同时也向各位读者推荐一下。算了免费为它做一个广告。

三、基本功能

 1、 登陆 
采用FTP进行文件传输,其实本质上还是采用Java.net.socket进行通信。以下代码只是类net.sf.jftp.net.FtpConnection其中一个login方法。当然在下面的代码,为了节省版面,以及将一些原理阐述清楚,笔者将一些没必要的代码去掉了,如日志等代码。完整的代码请参考J-ftp的源代码或是笔者所以的示例源代码,后面的代码示例也同理:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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类的构造方法中启动的。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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方法。已经考虑了单线程及多线程两种不同的类型。 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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方法中,进行文件的读取及传输。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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)方法,代码如下所示,

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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()方法。其实代码如下: 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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则变化不是特别明显。

以上就是FTP批量大文件上传下载的基础篇,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

标签:

相关文章

热门资讯

歪歪漫画vip账号共享2020_yy漫画免费账号密码共享
歪歪漫画vip账号共享2020_yy漫画免费账号密码共享 2020-04-07
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
沙雕群名称大全2019精选 今年最火的微信群名沙雕有创意
沙雕群名称大全2019精选 今年最火的微信群名沙雕有创意 2019-07-07
玄元剑仙肉身有什么用 玄元剑仙肉身境界等级划分
玄元剑仙肉身有什么用 玄元剑仙肉身境界等级划分 2019-06-21
男生常说24816是什么意思?女生说13579是什么意思?
男生常说24816是什么意思?女生说13579是什么意思? 2019-09-17
返回顶部