Sftp文件上传、下载

Sftp协议下文件上传、下载

使用JSch进行Sftp连接

问题产生

我用common-net,ftp连接时使用21端口会超时,后来发现使用Xftp工具用21端口也超时

1
Connection timed out: connect

查了下百度,端口问题

  • ftp服务用的是20、21端口,客户端添加ftp信息的时候输入的是21端口

  • ssh服务用的是22端口,应用于远程ssh管理Linux服务器;

然后我换了22端口进行尝试。

1
2
 Could not parse response code.
Server Reply: SSH-2.0-OpenSSH_7.4

异常如上,百度了下,于是开始用sftp协议尝试。

参考文章:主要参考详细参考

问题解决

我制作了一个工具类,以供调用。

maven依赖

1
2
3
4
5
6
7
8
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jzlib</artifactId>
</dependency>

还有一些依赖,如springMVC的依赖。

简单上传

遇到了很多的问题,像如何简化、如何创建目录、如何检查目录是否存在、认证问题等,在注释里写得很详细。

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
package IO_Utils;

import com.jcraft.jsch.*;
import java.io.*;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Properties;
import java.util.Vector;

/*
* 我的sftp上传工具
* */

public class ImageSftp {

/**方法内属性说明
*
private static ChannelSftp Sftp = null;

//用户名(由外部传参)

private static String ImgServerUsername = "root";
//主机ip(由外部传参)
private static String ImgServerIp = "78.141.206.203";
//密码(由外部传参)
private static String ImgServerPassword = "123456";
//端口号(由外部传参)
private static int ImgServerPort = 22;
//上传到的服务器目录(由外部传参)
private static String ImgServerDirectory = "/data/images/";

//上传到服务器的文件命名为(由外部传参)
private static String ImgServerFileName="";

//要上传的本地文件
// private static File clientFile=null;

//下载到本地的目录(由外部传参)
private static String ClientDirectory = "D:\\aria2\\";



//下载到本地的文件命名为(由外部传参)
private static String ClientFileName ="";
*/
private Channel channel=null;
private Session sshSession=null;
private ChannelSftp sftp =null;

/*
* 获取连接对象的方法
* */
public ChannelSftp getConnect(String imgServerIp, int imgServerPort,String imgServerUsername,String imgServerPassword) {


JSch jsch = new JSch();
try {


//用户名、ip、端口号
sshSession = jsch.getSession(imgServerUsername,imgServerIp, imgServerPort);

//配置属性
Properties config = new Properties();
config.put("StrictHostKeyChecking","no");
config.put("PreferredAuthentications","password");
sshSession.setConfig(config);
//不检查主机严格密钥
// sshSession.setConfig("StrictHostKeyChecking", "no");
//关闭gssapi认证,只使用密码认证,减少耗时 //config.put("userauth.gssapi-with-mic", "no");
// sshSession.setConfig("PreferredAuthentications","password");

//给密码设值
sshSession.setPassword(imgServerPassword);
//设置多少毫秒超时(设了会报错)
// sshSession.connect(600);
// sshSession.setServerAliveInterval(92000);// 请求时长

System.out.println("正在与服务器建立连接");
//开启sshSession链接
// sshSession.connect();
sshSession.connect(5000);


//获取sftp通道
channel = sshSession.openChannel("sftp");
channel.connect();
ChannelSftp sftp = (ChannelSftp) channel;
System.out.println("已成功建立连接");
return sftp;
}catch (JSchException e){
e.printStackTrace();
System.out.println("建立连接失败");
return null;
}
}


/**
* 上传方法
* @param sftp 通过getConnect()方法获得的链接对象
* @param inputStream 要上传文件的输入流
* //@param serverDirectory 某类文件存放的目录,必须指明为根目录某处,如:/data/video/,斜杠必须带
* @param finalServerDirectory 上传文件最终所在的目录,=serverDirectory/nextDirectory
* @param serverFileName 为上传到服务器后的文件名
* */

public boolean upload(ChannelSftp sftp,InputStream inputStream ,String finalServerDirectory,String serverFileName) throws IOException, SftpException {
//连接服务器
// ChannelSftp sftp = getConnect();

if (sftp!=null) {


//进入要存储的服务器目录
// sftp.cd(serverDirectory);

SftpATTRS stat=null;
//判断文件夹是不是存在,这里要捕获异常,不然会卡住
try {


stat = sftp.stat(finalServerDirectory);
System.out.println("找到了目标文件(夹):"+stat+"\n\n\n");

}catch (Exception e){
System.out.println("找不到目标目录");
}

if (stat!=null){//stat有返回值,说明文件夹存在
//进入该文件夹
sftp.cd(finalServerDirectory);
System.out.println("进入文件夹");
}else {
//创建文件夹,然后进入
sftp.mkdir(finalServerDirectory);
sftp.cd(finalServerDirectory);

System.out.println("自动创建"+finalServerDirectory+"文件夹,并进入");
}


//本地文件,存到流,不需要,因为前台会直接收到MultipartFile类型的文件,并且能获得流
// File clientFile = new File(filePath);
// InputStream fileInputStream = new FileInputStream(clientFile);

//上传到服务器后的名字,由外部传参
// serverFileName="1.avi";

System.out.println("上传ing");
//获取文件大小(字节)
long size = inputStream.available();
//执行上传(断点续传方式)
sftp.put(inputStream, serverFileName,new SftpMonitor(size),ChannelSftp.RESUME);

System.out.println("上传完毕");


//交给单独方法去断开连接
// System.out.println("关闭连接");
//断开连接
// sftp.disconnect();
//
// System.out.println("连接是否已关闭"+sftp.isClosed());

return true;

}else {
return false;
}
}


/**
* 关闭连接
* */

public void close() throws Exception{
if (sftp!=null){
sftp.quit();
}
if (channel!=null){
channel.disconnect();
System.out.println("已关闭通道");
}
if (sshSession!=null){
sshSession.disconnect();
System.out.println("已关闭连接");
}

}

/*
* 下载方法
* 未写,因为暂时用不到
* 暂定
* */

// public static String download() throws JSchException, SftpException, FileNotFoundException {
// ChannelSftp sftp = getConnect();
//
// clientFileName="123.png";
// File clientFile=new File(clientDirectory+clientFileName);
// serverDirectory="/data/images/";
// serverFileName="123.png";
// sftp.get(serverDirectory+serverFileName, new FileOutputStream(clientFile));
// return clientDirectory+clientFileName;
//
// }



//测试

public static void main(String[] args) throws Exception {

String imgServerIp="78.141.206.203";
int imgServerPort=22;
String imgServerUsername="root";
String imgServerPWD="123456";

ImageSftp imageSftp=new ImageSftp();

ChannelSftp connect = imageSftp.getConnect(imgServerIp, imgServerPort, imgServerUsername, imgServerPWD);

// String filePath="C:\\Users\\Shirtiny\\Downloads\\masu.jpg";//要上传的文件的路径//现在是你直接给我个输入流

//获取输入流
String filePath="D:\\MY文档\\音乐\\折戸伸治 - 潮鳴り.mp3";
File file=new File(filePath);
// long fileSize = file.length();
// System.out.println("文件大小"+fileSize);
FileInputStream fileInputStream = new FileInputStream(file);

String finalServerDirectory="/data/music";//上传到的服务器目录,调用上传方法时,若找不到该目录,则程序自动创建。代表文件最终存放的目录
String serverFileName="潮鸣.mp3";//上传到服务器的文件命名为
// Vector ls = connect.ls(finalServerDirectory);
// connect.put("D:\\MY文档\\音乐\\折戸伸治 - 潮鳴り.mp3","123.mp3",new SftpMonitor(),ChannelSftp.OVERWRITE);

boolean flag = imageSftp.upload(connect, fileInputStream, finalServerDirectory, serverFileName);

System.out.println("是否完成:"+flag);
//关闭连接
connect.quit();
imageSftp.close();


}

}

下载方法未写

下载时拒绝访问的问题

实例

我现在需要将一个文件(图片为例)上传到服务器,使用sftp协议,需求如下:

  • 点击上传按钮,选择文件后即可上传
  • 能看到上传的进度和速度
  • 能在上传成功、异常结束时得到反馈
  • 要求在上传后,程序自动显示该图片
  • 图片需要按照一定分类去存储,以方便管理,减少资源消耗
  • 图片名称不能重复,并且图片能正确显示
  • 全程支持中文
  • 支持断点续传

首先,进度监控、断点续传

很显然我们目前的这个工具类还不能满足我们的需求,它还需要两个功能:

1.实时监控,这样我们才能知道上传的速度、进度

2.断点续传,值得高兴的是,Jsch为我们提供了这个功能。

1. 实时监控

SftpProgressMonitor

Jsch提供了一个SftpProgressMonitor接口,包括了初始化时执行的init()方法、每传输一个数据块就会执行一次的count()方法、以及在传输结束时执行的end()方法。

基于这个接口,我们可以写出一个简单监控类:

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
package IO_Utils;

import com.jcraft.jsch.SftpProgressMonitor;

/**
* 上传进程监控
* */

public class SftpMonitor implements SftpProgressMonitor {

private long counted;//初始字节数,已经上传的字节数
private long fileSize;//最终文件大小
private long percent;//进度百分比值


public SftpMonitor() {
}

public SftpMonitor(long fileSize) {
this.fileSize = fileSize;
}

@Override
public void init(int op, String src, String dest, long errfileSize) {


System.out.println("初始化完成"+"文件大小为:"+fileSize);
}

@Override
public boolean count(long count) {

// System.out.println("之前已上传"+counted+"("+percent+"%)");
counted +=count;
// if (percent>=this.count/fileSize){
// return true;
// }
percent= counted*100/fileSize;
System.out.println("进度-----已传输:"+counted/1024+" kb/"+fileSize/1024+" kb"+"("+percent+"%)");

return true;
}

@Override
public void end() {
System.out.println("end结束");
}
}

它的使用方式:

1
2
3
long size = inputStream.available();//通过流获取文件大小
//执行上传
sftp.put(inputStream, serverFileName,new SftpMonitor(size);//在执行put方法时初始化一个监控类

很抱歉,由于时间关系,我不能像以往那样详细说明,Jsch:put方法的重载

SftpProgressMonitor+TimerTask+Pojo

我们需要获得传输的速度,所以需要有个Timer来帮忙,对此不了解的可查看Timer的使用.

我们可以通过重写TimerTask类的run()方法,来实现我们需要的功能。我写了一个start()方法用于创建timer对象,新建计划任务,stop()方法用于终止计时。run()方法会按照我们设置的计划,每隔一段时间执行一次,用单独的线程来执行。

我把需要的信息封装到一个pojo类里,你也可以不封装。

由此便有了一个新的监控类:

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
144
145
146
147
148
149
150
151
152
153
154
155
156
package Sftp_service;

import com.jcraft.jsch.SftpProgressMonitor;
import org.springframework.beans.factory.annotation.Autowired;


import java.text.DecimalFormat;
import java.util.Timer;
import java.util.TimerTask;

public class mySftpTimerMonitor extends TimerTask implements SftpProgressMonitor {




private long fileSize;//文件总大小
private long counted;//已传输数据,单位字节
private long counted_Before=0;//上一秒的已传输数据,单位字节
private long i_ed=0;//已计时间,单位s
private Timer timer;//计时器对象
private long timeInterval=2*1000;//时间间隔,单位ms
private boolean timerIsStarted;//是否已经开始计时
private DecimalFormat format = new DecimalFormat( "#.##");//用于转换数据显示格式


private SftpSpeedInfo speedInfo=new SftpSpeedInfo();

public mySftpTimerMonitor() {
}



mySftpTimerMonitor(long fileSize) {
this.fileSize = fileSize;
}

@Override//监视器方法重写
public boolean count(long count) {//每传输一次数据块,就会执行一次count方法
if (!timerIsStarted){//只在无计时器时,开启计时器
timerStart();//开始计时
}

incrementCounted(count);//执行自增

return true;
}


//Timertask run
@Override//计时器方法重写
public void run() {//计时器控制的方法,每多少时间执行一次,用单独的线程执行。前提是启动计时器
/*
*i++;
*System.out.println(counted+"已用时间"+this.i+"s");
*可以这样。不过可能是考虑到多线程的原因应该这样做:
* */
//这样取值更好,其他线程需要等待这个线程取完值,才能去取值
long i1 =getI_ed();//这样就有个新问题,如何使i自增,直接在run内i++是不行的,因为getI()的值没有变
long i2 = incrementI_ed(i1);//新建个方法,用来使秒数自增一次,自憎后的值为i2
long counted_latest=getCounted();//同理拿到当前的,已传输数据量counted的值
long counted_before = getCounted_Before();//拿到上一次的已传数据量
setCounted_Before(counted_latest);//把这次的已传数据量存起来
double speed=(double)(counted_latest-counted_before)/(1024*(timeInterval/1000));//计算传输速度,单位kb/s,speed=当前已传输量-上次的已传输量/(1024*时间间隔/1000)
double percent=(double)counted_latest/(double)fileSize;

speedInfo.setPercent(format.format(percent*100)+"%");//百分比
speedInfo.setSpeed(format.format(speed)+"kb/s");//速度
speedInfo.setCounted(format.format((double)counted/1024)+"kb");//已传输量

speedInfo.setTimed(i2);//用时 s
System.out.println(speedInfo);

}



private void timerStart(){//自定义的计时器方法,启动计时器
if (timer!=null){
timer.cancel();//终止此计时器,丢弃所有当前已安排的任务。
timer.purge();//从此计时器的任务队列中移除所有已取消的任务。
}else {

timer=new Timer();
// 这个方法是调度一个task,在delay(ms)后开始调度,每次调度完后,最少等待period(ms)后才开始调度
timer.schedule(this,1000,timeInterval);
timerIsStarted=true;
System.out.println("Timer is started,计时器启动完成");
}
}




public void stop(){//自定义的计时器方法,停止计时器
if (timer != null) {
timer.cancel();
timer.purge();
timer = null;
timerIsStarted=false;
}
System.out.println("stop timer,停止计时");

}





@Override//监视器方法重写
public void init(int i, String s, String s1, long l) {

speedInfo.setFileSize(format.format((double)this.fileSize/1024)+"kb");//文件大小

}

@Override//监视器方法重写
public void end() {//传输结束
stop();//停止计时器,并清空数据
}


//使用synchronized关键字,线程排队调用,即线程同步


public synchronized long getFileSize() {
return fileSize;
}

private synchronized long getCounted_Before() {//取出上一秒的数据量
return counted_Before;
}

private synchronized void setCounted_Before(long counted_latest) {//把这一秒的已传输数据量记录给counted_Before,为下一秒服务
this.counted_Before = counted_latest;
}

private synchronized long getI_ed() {//上一秒i的值,已计秒数
return i_ed;
}

private synchronized long getCounted() {//此次已传数据量的值
return counted;
}


private synchronized void incrementCounted(long count){//已传数据量自增方法
counted=counted+count;
}
//
private synchronized long incrementI_ed(long i1){//已计秒数 自增方法
i_ed=i1+(timeInterval/1000);//自增一次,自增值为间隔时间(s)
return i_ed;
}


}

2.断点续传

好在Jsch提供了这个功能,我们不必再费脑筋。我们只需要在执行put()时,把mode的值改为ChannelSftp.RESUME即可。

1
2
 long size = inputStream.available();
sftp.put(inputStream, serverFileName,new mySftpTimerMonitor(size),ChannelSftp.RESUME);

OVERWRITE是覆盖,APPEND是扩展。

其次,文件夹分类、文件名随机

直接看代码即可,这是service层:

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
package com.SH.Service.ServiceImpl;

import ID_Utils.ID_Imghelper;
import com.SH.Service.IimgService;
import Sftp_service.ImageSftp;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;


@Service
public class imgServiceImpl implements IimgService {


@Value("${ImgServerIp}")
private String ImgServerIp;//图片服务器ip

@Value("${ImgServerPort}")
private int ImgServerPort;//端口号

@Value("${ImgServerUsername}")
private String ImgServerUsername;//用户名

@Value("${ImgServerPassword}")
private String ImgServerPassword;//密码

@Value("${ImgServerDirectory}")
private String ImgServerDirectory;//存储路径


@Override
public boolean Imgupload(InputStream inputStream,String suffix) throws Exception {

boolean flag;
ImageSftp imageSftp=new ImageSftp();

String fileName= ID_Imghelper.getImgID()+suffix;

ChannelSftp connect = imageSftp.getConnect(ImgServerIp, ImgServerPort, ImgServerUsername, ImgServerPassword);
//根据时间创建一个字符串作为文件夹的名字,方便管理
String nextDirectory = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
//最终文件存放的目录名
String finalServerDirectory=ImgServerDirectory + "/" + nextDirectory;

try {
flag= imageSftp.upload(connect,inputStream,finalServerDirectory,fileName);
System.out.println("上传返回值:"+flag);
} catch (IOException e) {
e.printStackTrace();
return false;
} catch (SftpException e) {
return false;
}

imageSftp.close();

return flag;
}

}

其中用imageSftp.properties存储服务器信息,使用@Value注解获取配置文件信息。

spring配置:

1
2
3
4
5
6
7
8
<bean id="dbproperties" class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
<property name="locations">
<array>
<!--数据库配置文件 --> <value>classpath:db.properties</value>
<!--图片服务器配置文件 --> <value>classpath:imageSftp.properties</value>
</array>
</property>
</bean>

imageSftp.properties:

1
2
3
4
5
ImgServerIp=78.141.206.203
ImgServerPort=22
ImgServerUsername=root
ImgServerPassword=123456
ImgServerDirectory=/data/images

ID_Imghelper,根据时间随机生成图片Id(文件名)的工具类:

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
package ID_Utils;

import java.util.Random;

public class ID_Imghelper {

public static String getImgID(){

long timeMillis = System.currentTimeMillis();
Random random = new Random();
int randomInt = random.nextInt(9999);

//%X 获得数字,把它转为16进制,大写字母
//%04X 增加的04,意思是,转化后的字符串占4个字符,不够用0填充
String imgID=timeMillis+String.format("%04X",randomInt);
/*其他转化:
* %x - 接受一个数字并将其转化为十六进制数格式, 使用小写字母
* %d, %i - 接受一个数字并将其转化为有符号的整数格式
* %s - 接受一个字符串并按照给定的参数格式化该字符串
* %e - 接受一个数字并将其转化为科学记数法格式, 使用小写字母e
*/
return imgID;
}

public static void main(String[] args) {
String imgID = ID_Imghelper.getImgID();
System.out.println(imgID);

}

}

最后,图片回显、结果反馈、中文支持等

图片回显只需要把服务器上的文件地址拼接出来即可。

这里给个参考,Controller:

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
package com.SH.Controller;

import ...

@Controller

public class testController {

@Autowired
private IimgService imgService;

@RequestMapping(value = "/upload",produces = "text/plain;charset=UTF-8")
//produces属性,设置响应格式
@ResponseBody
public String upload(MultipartFile file,Map<String,Object> map) throws Exception {


InputStream inputStream = file.getInputStream();
System.out.println("文件大小"+file.getSize());
String originalFilename = file.getOriginalFilename();

//截取字符串substring(start,stop),从下标为start值的位置(第start个字符)开始截取,省略stop会截取start以后得全部字符串
//注意stop值为要截取到字符的对应下标+1,如字符串123,下标为012,从如要截取出字符串12,比喻成区间(下标)为[0,1),写法为substring(0,2)
//lastIndexOf(".")字符串倒数第一个.的下标
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
System.out.println(originalFilename);
System.out.println(suffix);

System.out.println((file.getSize()/1024.00)+"kb");

boolean flag = imgService.Imgupload(inputStream, suffix);

//json转换对象
ObjectMapper MAPPER=new ObjectMapper();

if (flag){

map.put("上传成功",flag);
//把对象转换为json格式字符串
return MAPPER.writeValueAsString(map);

}else {
map.put("上传失败",flag);

return MAPPER.writeValueAsString(map);
}


}

}

参考文章:

https://blog.csdn.net/weixin_36910300/article/details/80532868

https://blog.csdn.net/qq_33390789/article/details/78614466

https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html

https://www.cnblogs.com/ssslinppp/p/6248763.html

https://blog.csdn.net/chaogewudi1/article/details/81629183

https://www.cnblogs.com/awkflf11/articles/5179156.html

https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html

https://blog.csdn.net/ecjtuxuan/article/details/2093757

https://blog.csdn.net/hl_java/article/details/79035237

https://blog.csdn.net/zjy15203167987/article/details/82531772

https://www.jb51.net/article/135720.htm