處理 GB 級大文件分割、合併、斷點續傳,RandomAccessFile(RAF)是 Java IO 體系中的「特種兵」:
seek(pos) 直接定位文件指針,無需順序讀取read()/write(),模式 "r"/"rw"/"rws"/"rwd"seek() 繼續
// 創建
RandomAccessFile raf = new RandomAccessFile(file, "rw"); // 讀寫模式
// 定位
long pos = raf.getFilePointer(); // 當前指針位置
raf.seek(pos); // 移動指針
raf.seek(raf.length()); // 移動到末尾(追加模式)
// 讀寫
int b = raf.read(); // 讀取單字節,-1 表示 EOF
raf.read(byte[] buf); // 讀滿 buf 或 EOF
raf.read(byte[] buf, int off, int len);
raf.write(byte[] buf);
raf.write(byte[] buf, int off, int len);
raf.write(int b); // 寫入單字節
// 文件長度
long len = raf.length();
// 關閉
raf.close(); // 必須關閉,釋放文件鎖
源文件名.001、源文件名.002... 固定大小分片(最後一片可能較小)
public class FileSplitter {
private static final int BUFFER_SIZE = 64 * 1024; // 64KB 緩衝區
public void split(File source, long chunkSizeBytes, String destDir) throws IOException {
if (!source.exists()) throw new FileNotFoundException(source.getAbsolutePath());
if (chunkSizeBytes <= 0) throw new IllegalArgumentException("分片大小必須 > 0");
File destDirFile = new File(destDir);
destDirFile.mkdirs();
try (RandomAccessFile in = new RandomAccessFile(source, "r")) {
long fileSize = in.length();
int totalChunks = (int) ((fileSize + chunkSizeBytes - 1) / chunkSizeBytes);
byte[] buffer = new byte[BUFFER_SIZE];
for (int chunk = 1; chunk <= totalChunks; chunk++) {
long start = (chunk - 1) * chunkSizeBytes;
long end = Math.min(start + chunkSizeBytes, fileSize);
long remaining = end - start;
File chunkFile = new File(destDirFile, source.getName() + "." + String.format("%03d", chunk));
try (RandomAccessFile out = new RandomAccessFile(chunkFile, "rw")) {
in.seek(start);
while (remaining > 0) {
int readLen = (int) Math.min(BUFFER_SIZE, remaining);
int read = in.read(buffer, 0, readLen);
if (read == -1) break; // 理論上不會發生
out.write(buffer, 0, read);
remaining -= read;
}
}
System.out.printf("分片 %d/%d 完成: %s (%,d bytes)%n", chunk, totalChunks, chunkFile.getName(), end - start);
}
}
}
}
byte[] 循環使用,避免頻繁分配remaining 變量控制%03d 保證按名稱排序即為順序
public class FileMerger {
private static final int BUFFER_SIZE = 64 * 1024;
public void merge(File chunkDir, String chunkSuffix, File target) throws IOException {
File[] chunks = chunkDir.listFiles((dir, name) -> name.endsWith(chunkSuffix));
if (chunks == null || chunks.length == 0) {
throw new IllegalArgumentException("未找到分片文件: " + chunkSuffix);
}
// 關鍵:按文件名排序保證順序
Arrays.sort(chunks, Comparator.comparing(File::getName));
try (RandomAccessFile out = new RandomAccessFile(target, "rw")) {
byte[] buffer = new byte[BUFFER_SIZE];
for (File chunk : chunks) {
try (RandomAccessFile in = new RandomAccessFile(chunk, "r")) {
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
System.out.println("合併分片: " + chunk.getName());
}
}
System.out.printf("合併完成: %s (%,d bytes)%n", target.getAbsolutePath(), target.length());
}
}
// 分割時生成校驗文件
public void generateChecksum(File source, File checksumFile) throws IOException {
try (InputStream in = new FileInputStream(source);
MessageDigest md = MessageDigest.getInstance("SHA-256")) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) md.update(buffer, 0, read);
String hash = Hex.encodeHexString(md.digest());
Files.writeString(checksumFile.toPath(), hash + " " + source.getName());
}
}
// 合併後驗證
public boolean verifyChecksum(File target, File checksumFile) throws IOException {
String expected = Files.readString(checksumFile.toPath()).split("\\s+")[0];
String actual = calculateSHA256(target);
return expected.equalsIgnoreCase(actual);
}
分割/合併是靜態操作,斷點續傳是動態過程,核心狀態:
public class TransferState {
private final String fileId; // 文件唯一標識(如 SHA-256 + 大小)
private final long fileSize;
private final long chunkSize;
private final BitSet completedChunks; // 或 boolean[]
private long transferredBytes;
private Instant lastUpdate;
// 序列化為 JSON 保存進度
// 恢復時讀取,跳過 completedChunks=true 的分片
}
public void downloadWithResume(URL url, File target, TransferState state) throws IOException {
try (RandomAccessFile raf = new RandomAccessFile(target, "rw")) {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("User-Agent", "JavaDownloader/1.0");
for (int chunk = 0; chunk < state.totalChunks; chunk++) {
if (state.completedChunks.get(chunk)) continue; // 跳過已完成
long start = chunk * state.chunkSize;
long end = Math.min(start + state.chunkSize, state.fileSize) - 1;
conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
try (InputStream in = conn.getInputStream()) {
raf.seek(start);
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
raf.write(buffer, 0, read);
state.transferredBytes += read;
}
}
state.completedChunks.set(chunk);
saveState(state); // 定期保存進度
}
}
}
| 優化點 | 方案 | 效果 |
|---|---|---|
| 系統調用開銷 | 增加緩衝區(64KB-1MB),使用 BufferedInputStream/OutputStream 包裝 RAF | 減少上下文切換 90%+ |
| 磁盤順序寫入 | 分片文件預分配空間 raf.setLength(chunkSize) | 減少文件碎片,提升寫入吞吐 |
| 零拷貝 | Java 21+ FileChannel.transferTo/From、Linux sendfile | 用戶態/內核態拷貝消除,CPU 佔用降 50% |
| 並發分片 | 線程池並行下載/寫入不同分片(需服務端支持 Range) | 帶寬利用率最大化 |
| 進度持久化 | SQLite/LevelDB 存儲狀態,避免單文件鎖競爭 | 支持多任務、崩潰恢復可靠 |
// 使用 FileChannel + MappedByteBuffer(內存映射文件)
public void splitWithNIO(File source, long chunkSize, File destDir) throws IOException {
try (FileChannel in = FileChannel.open(source.toPath(), StandardOpenOption.READ)) {
long fileSize = in.size();
int total = (int) ((fileSize + chunkSize - 1) / chunkSize);
for (int i = 0; i < total; i++) {
long start = i * chunkSize;
long end = Math.min(start + chunkSize, fileSize);
long size = end - start;
File chunkFile = new File(destDir, source.getName() + "." + String.format("%03d", i+1));
try (FileChannel out = FileChannel.open(chunkFile.toPath(),
StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.READ)) {
// 內存映射:大文件分片映射需注意內存壓力
MappedByteBuffer mapped = in.map(FileChannel.MapMode.READ_ONLY, start, size);
out.write(mapped); // 直接寫入,極快
}
}
}
}
選擇建議:
RandomAccessFile 是 Java 處理大文件隨機讀寫的基石,掌握其 seek()、read/write(byte[], off, len)、length() 核心 API,配合緩衝區、分片邏輯、校驗機制、狀態持久化,即可構建生產級的文件分割、合併、斷點續傳系統。現代 NIO.2 提供了更高性能的替代選項,但 RAF 的精確控制特性在斷點續傳等精細場景中仍不可替代。