基于 JSch 实现 Scp 文件传输
概述
JSch 是一个纯 Java 实现的 SSH2 类库,支持连接到一个 sshd 服务器。这里记录一下如何通过 JSch 实现 Scp 传输文件的逻辑。
注意,如果要传输一个文件夹的的话,递归调用这些方法即可。传输文件夹的时候,如果这个文件夹里面的文件数量比较多,那么有可能会比较慢,可以选用 Sftp [链接]来实现上传下载。如果只传输一个文件的情况下, Scp 和 Sftp 的传输速度差不太多;如果文件数量特别大的情况下,Sftp 要远远快于 Scp。
上传文件和下载文件是一个相反的过程,可以对照着看。
以下代码中的 IOStreamx、Filex 等工具类由 central-framework [链接]提供。
上传文件
总体流程
使用 JSch 实现文件下传需要通过以下流程实现。
- 连接到服务器
- 读取 ack(确认服务器已准备好)
- 发送
C0644 {filesize} {filename}\n
- 读取 ack (确认服务器已准备好传输)
- 发送文件流
- 发送 ack (通知服务器文件已完成传输)
- 读取 ack(确认服务器已完成传输)
实现代码
java
/**
* 将本地文件传输到远程服务器的指定路径
*
* @param host 主机
* @param username 用户名
* @param port 端口
* @param password 密码
* @param localFile 本地文件
* @param remotePath 远程路径
*/
public int scpFileTo(String host, String username, int port, String password, File localFile, Path remotePath) throws JSchException {
var factory = new JSch();
// 创建会话
var session = factory.getSession(username, host, port);
// 设置连接密码
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
try {
// 连接服务器
session.connect();
// 传输频道
var channel = (ChannelExec) session.openChannel("exec");
// 将文件保存到服务器的指定路径
channel.setCommand("scp -t " + remotePath.toString().replace(" ", "\\ ").replace("(", "\\(").replace(")", "\\)"));
try (var output = IOStreamx.buffered(channel.getOutputStream()); var input = IOStreamx.buffered(channel.getInputStream())) {
channel.connect();
// 读取 ack
if (readAck(input) != 0) {
return -1;
}
// 写入文件大小和文件名
IOStreamx.writeLine(output, Stringx.format("C0644 {} {}", localFile.length(), localFile.getName()), StandardCharsets.UTF_8);
// 读取 ack
if (readAck(input) != 0) {
return -1;
}
// 写入文件内容
try (var stream = IOStreamx.buffered(Files.newInputStream(localFile.toPath(), StandardOpenOption.READ))) {
IOStreamx.transfer(stream, output);
}
// 发送 ack
sendAck(output);
// 读取 ack
if (readAck(input) != 0) {
return -1;
}
// 结束
return 0;
} catch (JSchException | IOException ex) {
throw new RuntimeException(ex.getLocalizedMessage(), ex);
} finally {
// 结束连接
channel.disconnect();
}
} finally {
session.disconnect();
}
}
/**
* 读取 ack 信号
*/
protected int readAck(InputStream stream) throws IOException {
// 读 1 个字节,用于确认
int b = stream.read();
if (b == 0) {
// 成功
return b;
}
if (b == -1) {
// 失败
return b;
}
if (b == 1 || b == 2) {
// 读取异常信息
var err = IOStreamx.readLine(stream, StandardCharsets.UTF_8);
throw new ShellException(b, err);
}
return b;
}
/**
* 发送 ack 信息
*/
protected void sendAck(OutputStream stream) throws IOException {
stream.write(0);
stream.flush();
}
下载文件
总体流程
使用 JSch 实现文件上传需要通过以下流程实现。
- 连接到服务器
- 发送 ack(告诉服务器本地已准备好)
- 接收
C0644 {filesize} {filename}\n
- 发送 ack (告诉服务器已准备好传输数据流)
- 接收文件流
- 读取 ack
- 发送 ack(告诉服务器已完成传输)
实现代码
java
/**
* 将服务器的文件传输到本地路径
*
* @param host 主机
* @param username 用户名
* @param port 端口
* @param password 密码
* @param remoteFile 远程文件
* @param localPath 本地路径
*/
public int scpFileFrom(String host, String username, int port, String password, Path remoteFile, Path localPath) throws JSchException {
var factory = new JSch();
// 创建会话
var session = factory.getSession(username, host, port);
// 设置连接密码
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
try {
// 连接服务器
session.connect();
// 传输频道
var channel = (ChannelExec) session.openChannel("exec");
// 传输服务器指定文件
channel.setCommand("scp -f " + remoteFile.toString().replace(" ", "\\ ").replace("(", "\\(").replace(")", "\\)"));
try (var output = IOStreamx.buffered(channel.getOutputStream()); var input = IOStreamx.buffered(channel.getInputStream())) {
channel.connect();
channel.start();
// 发送 ack
sendAck(output);
// 读取文件大小和文件名
// C0644 {filesize} {filename}\n
var meta = IOStreamx.readLine(input, StandardCharsets.UTF_8);
var metas = meta.split(" ");
// 解析文件大小
var fileSize = Long.parseLong(metas[1]);
// 解析文件名
var filename = metas[2];
// 发送 ack
sendAck(output);
var localFile = new File(localPath.toFile(), filename);
Filex.delete(localFile);
Assertx.mustTrue(localFile.createNewFile(), RuntimeException::new, "无法访问文件: " + localFile.getAbsolutePath());
// 读取指定长度的数据流,并写入文件
try (var stream = IOStreamx.buffered(Files.newOutputStream(localFile.toPath(), StandardOpenOption.WRITE))) {
IOStreamx.transfer(input, stream, fileSize);
}
// 读取 ack
if (readAck(input) != 0) {
return -1;
}
// 发送 ack
sendAck(output);
// 结束
return 0;
} catch (JSchException | IOException ex) {
throw new RuntimeException(ex.getLocalizedMessage(), ex);
} finally {
// 结束连接
channel.disconnect();
}
} finally {
session.disconnect();
}
}
/**
* 读取 ack 信号
*/
protected int readAck(InputStream stream) throws IOException {
// 读 1 个字节,用于确认
int b = stream.read();
if (b == 0) {
// 成功
return b;
}
if (b == -1) {
// 失败
return b;
}
if (b == 1 || b == 2) {
// 读取异常信息
var err = IOStreamx.readLine(stream, StandardCharsets.UTF_8);
throw new ShellException(b, err);
}
return b;
}
/**
* 发送 ack 信息
*/
protected void sendAck(OutputStream stream) throws IOException {
stream.write(0);
stream.flush();
}