zsh_root
2025-12-10 8d662de2fd262b3a485f16e197cb4d0ca2a61cdf
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
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; // 返回大小
    }
}