package Ymodem; // 定义包名为Ymodem
|
import java.io.*; // 导入Java输入输出包
|
import java.nio.file.Files; // 导入NIO文件操作类
|
import java.nio.file.Path; // 导入NIO路径类
|
import java.nio.file.attribute.BasicFileAttributes; // 导入文件属性类
|
import java.util.Arrays; // 导入数组工具类
|
public class YModem { // 定义YModem类
|
private Modem modem; // 声明Modem对象
|
static int size; // 静态大小变量
|
|
/**
|
* 构造函数
|
* @param inputStream 流,用于从另一端读取接收到的数据
|
* @param outputStream 用于将数据写入另一端的流
|
*/
|
public YModem(InputStream inputStream, OutputStream outputStream) { // 构造函数
|
this.modem = new Modem(inputStream, outputStream); // 初始化Modem对象
|
}
|
|
// 进度回调接口
|
public interface ProgressCallback { // 定义进度回调接口
|
void onProgress(int currentBlock, int totalBlocks); // 进度回调方法
|
}
|
|
/**
|
* 发送文件。
|
* 此方法支持正确的线程中断,当线程中断时将发送"取消传输"。
|
* 因此,您可以将长传输移动到其他线程并根据您的算法中断它。
|
* @param file 文件路径
|
* @throws IOException
|
*/
|
public void send(Path file) throws IOException { // 发送文件方法
|
send(file, null); // 调用带进度回调的发送方法
|
}
|
|
/**
|
* 发送文件(带进度回调)。
|
* 此方法支持正确的线程中断,当线程中断时将发送"取消传输"。
|
* 因此,您可以将长传输移动到其他线程并根据您的算法中断它。
|
* @param file 文件路径
|
* @param progressCallback 进度回调
|
* @throws IOException
|
*/
|
public void send(Path file, ProgressCallback progressCallback) throws IOException { // 带进度回调的发送方法
|
|
try (DataInputStream dataStream = new DataInputStream(Files.newInputStream(file))) { // 创建文件输入流
|
|
Timer timer = new Timer(Modem.WAIT_FOR_RECEIVER_TIMEOUT).start(); // 创建并启动计时器
|
boolean useCRC16 = modem.waitReceiverRequest(timer); // 等待接收方请求并确定CRC类型
|
|
CRC crc; // 声明CRC对象
|
if (useCRC16) // 如果使用CRC16
|
crc = new CRC16(); // 创建CRC16对象
|
else // 否则
|
crc = new CRC8(); // 创建CRC8对象
|
|
// 计算总块数
|
long fileSize = Files.size(file); // 获取文件大小
|
int totalDataBlocks = (int) Math.ceil(fileSize / 128.0); // 计算数据块总数
|
int totalBlocks = 2 + totalDataBlocks; // 文件头块 + 数据块 + 结束块
|
|
//发送块0(文件头)
|
BasicFileAttributes readAttributes = Files.readAttributes(file, BasicFileAttributes.class); // 读取文件属性
|
String name=file.getFileName().toString(); // 获取文件名
|
String fileNameString = name + (char)0 + fileSize + " "+ Long.toOctalString(readAttributes.lastModifiedTime().toMillis() / 1000); // 构建文件名字符串
|
byte[] fileNameBytes = Arrays.copyOf(fileNameString.getBytes(), 128); // 将文件名转换为字节数组
|
modem.sendBlock(0, Arrays.copyOf(fileNameBytes, 128), 128, crc); // 发送文件头块
|
|
// 更新进度(文件头块)
|
if (progressCallback != null) { // 如果进度回调不为空
|
progressCallback.onProgress(1, totalBlocks); // 调用进度回调
|
}
|
|
modem.waitReceiverRequest(timer); // 等待接收方请求
|
byte[] block = new byte[128]; // 创建数据块数组
|
|
// 发送数据块
|
int blockNumber = 1; // 初始化块编号
|
int bytesRead; // 声明读取字节数变量
|
while ((bytesRead = dataStream.read(block)) != -1) { // 读取文件数据
|
if (bytesRead < 128) { // 如果读取字节数小于128
|
// 填充不足部分
|
Arrays.fill(block, bytesRead, 128, Modem.CPMEOF); // 用CPMEOF填充剩余部分
|
}
|
modem.sendBlock(blockNumber, block, 128, crc); // 发送数据块
|
|
// 更新进度(数据块)
|
if (progressCallback != null) { // 如果进度回调不为空
|
progressCallback.onProgress(1 + blockNumber, totalBlocks); // 调用进度回调
|
}
|
|
blockNumber++; // 块编号递增
|
}
|
|
|
modem.sendEOT(); // 发送传输结束信号
|
modem.waitReceiverRequest(timer); // 等待接收方请求
|
byte[] bytes = new byte[128]; // 创建空字节数组
|
modem.sendBlock(0, bytes, 128, crc); // 发送空块表示传输结束
|
|
// 更新进度(结束块)
|
if (progressCallback != null) { // 如果进度回调不为空
|
progressCallback.onProgress(totalBlocks, totalBlocks); // 调用进度回调
|
}
|
|
}
|
}
|
|
/**
|
* 批量发送文件。<br/>
|
* <p>
|
* 此方法支持正确的线程中断,当线程中断时将发送"取消传输"。
|
* 因此,您可以将长传输移动到其他线程并根据您的算法中断它。
|
*
|
* @param files
|
* @throws IOException
|
*/
|
public void batchSend(Path... files) throws IOException { // 批量发送文件方法
|
for (Path file : files) { // 遍历文件数组
|
send(file); // 发送每个文件
|
}
|
|
sendBatchStop(); // 发送批量传输结束信号
|
}
|
|
private void sendBatchStop() throws IOException { // 发送批量传输结束方法
|
Timer timer = new Timer(Modem.WAIT_FOR_RECEIVER_TIMEOUT).start(); // 创建并启动计时器
|
boolean useCRC16 = modem.waitReceiverRequest(timer); // 等待接收方请求并确定CRC类型
|
CRC crc; // 声明CRC对象
|
if (useCRC16) // 如果使用CRC16
|
crc = new CRC16(); // 创建CRC16对象
|
else // 否则
|
crc = new CRC8(); // 创建CRC8对象
|
|
//发送块0
|
byte[] bytes = new byte[128]; // 创建空字节数组
|
modem.sendBlock(0, bytes, bytes.length, crc); // 发送空块表示批量传输结束
|
}
|
|
/**
|
* 接收单个文件 <br/>
|
* <p>
|
* 此方法支持正确的线程中断,当线程中断时将发送"取消传输"。
|
* 因此,您可以将长传输移动到其他线程并根据您的算法中断它。
|
*
|
* @param directory 文件将保存的目录
|
* @return 创建的文件路径
|
* @throws IOException
|
*/
|
public Path receiveSingleFileInDirectory(Path directory) throws IOException { // 接收单个文件方法
|
return receive(directory, true); // 调用接收方法
|
}
|
|
/**
|
* 批量接收文件 <br/>
|
* <p>
|
* 此方法支持正确的线程中断,当线程中断时将发送"取消传输"。
|
* 因此,您可以将长传输移动到其他线程并根据您的算法中断它。
|
*
|
* @param directory 文件将保存的目录
|
* @throws IOException
|
*/
|
public void receiveFilesInDirectory(Path directory) throws IOException { // 批量接收文件方法
|
while (receive(directory, true) != null) { // 循环接收文件直到返回null
|
}
|
}
|
|
/**
|
* 接收路径 <br/>
|
* <p>
|
* 此方法支持正确的线程中断,当线程中断时将发送"取消传输"。
|
* 因此,您可以将长传输移动到其他线程并根据您的算法中断它。
|
*
|
* @param path 数据将保存的文件路径
|
* @return 文件路径
|
* @throws IOException
|
*/
|
public Path receive(Path path) throws IOException { // 接收文件方法
|
return receive(path, false); // 调用接收方法
|
}
|
|
private Path receive(Path path, boolean inDirectory) throws IOException { // 私有接收方法
|
DataOutputStream dataOutput = null; // 声明数据输出流
|
Path filePath; // 声明文件路径
|
try {
|
CRC crc = new CRC16(); // 创建CRC16对象
|
int errorCount = 0; // 初始化错误计数
|
|
// 处理块0
|
byte[] block; // 声明数据块数组
|
int character; // 声明字符变量
|
while (true) { // 循环处理块0
|
character = modem.requestTransmissionStart(true); // 请求传输开始
|
try {
|
// 从零块读取文件名
|
block = modem.readBlock(0, (character == Modem.SOH), crc); // 读取块0
|
if (inDirectory) { // 如果在目录中
|
StringBuilder sb = new StringBuilder(); // 创建字符串构建器
|
if (block[0] == 0) { // 如果块首字节为0
|
//这是批量文件传输的停止块
|
modem.sendByte(Modem.ACK); // 发送ACK
|
return null; // 返回null
|
}
|
for (int i = 0; i < block.length; i++) { // 遍历块数据
|
if (block[i] == 0) { // 如果遇到0字节
|
break; // 跳出循环
|
}
|
sb.append((char) block[i]); // 追加字符到字符串构建器
|
}
|
filePath = path.resolve(sb.toString()); // 解析文件路径
|
} else { // 否则
|
filePath = path; // 直接使用路径
|
}
|
dataOutput = new DataOutputStream(Files.newOutputStream(filePath)); // 创建文件输出流
|
modem.sendByte(Modem.ACK); // 发送ACK
|
break; // 跳出循环
|
} catch (TimeoutException | Modem.InvalidBlockException e) { // 捕获超时和无效块异常
|
errorCount++; // 错误计数增加
|
if (errorCount == Modem.MAXERRORS) { // 如果错误计数达到最大值
|
modem.interruptTransmission(); // 中断传输
|
throw new IOException("Transmission aborted, error count exceeded max"); // 抛出IO异常
|
}
|
modem.sendByte(Modem.NAK); // 发送NAK
|
} catch (Modem.RepeatedBlockException | Modem.SynchronizationLostException e) { // 捕获重复块和同步丢失异常
|
//致命传输错误
|
modem.interruptTransmission(); // 中断传输
|
throw new IOException("Fatal transmission error", e); // 抛出IO异常
|
}
|
}
|
|
//接收数据块
|
modem.receive(filePath, true); // 接收数据块
|
} finally {
|
if (dataOutput != null) { // 如果数据输出流不为空
|
dataOutput.close(); // 关闭数据输出流
|
}
|
}
|
return filePath; // 返回文件路径
|
}
|
|
public static int getSize() { // 获取大小方法
|
return size; // 返回大小
|
}
|
}
|