大檔案(GB 級)上傳不可一次性讀入記憶體,需分段讀取並以 multipart/form-data 串流上傳,支援斷點續傳、並行分片。
seek(offset) + read(bytes)public void uploadChunk(String actionUrl, File uploadFile, long startOffset, long chunkSize,
String fileName, String boundary) throws IOException {
URL url = new URL(actionUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setDoInput(true);
con.setDoOutput(true);
con.setUseCaches(false);
con.setRequestMethod("POST");
con.setRequestProperty("Connection", "Keep-Alive");
con.setRequestProperty("Charset", "UTF-8");
con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (DataOutputStream dos = new DataOutputStream(con.getOutputStream());
RandomAccessFile raf = new RandomAccessFile(uploadFile, "r")) {
// 定位到分段起始位置
raf.seek(startOffset);
long remaining = chunkSize;
byte[] buffer = new byte[8192]; // 8KB 緩衝
// 寫入 Multipart Header
dos.writeBytes("--" + boundary + "\r\n");
dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + fileName + "\"\r\n");
dos.writeBytes("Content-Type: application/octet-stream\r\n");
dos.writeBytes("Content-Range: bytes " + startOffset + "-" + (startOffset + chunkSize - 1) + "/" + uploadFile.length() + "\r\n");
dos.writeBytes("\r\n");
// 串流讀寫檔案內容
int read;
while (remaining > 0 && (read = raf.read(buffer, 0, (int)Math.min(buffer.length, remaining))) != -1) {
dos.write(buffer, 0, read);
remaining -= read;
}
// 結束 Boundary
dos.writeBytes("\r\n--" + boundary + "--\r\n");
dos.flush();
// 檢查回應碼
int respCode = con.getResponseCode();
if (respCode != 200) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getErrorStream()))) {
String line;
StringBuilder err = new StringBuilder();
while ((line = br.readLine()) != null) err.append(line);
throw new IOException("Upload failed: " + respCode + " " + err);
}
}
}
}
| 細節 | 說明 |
|---|---|
| Boundary | 隨機字串(如 UUID),前綴 --,結尾 --boundary-- |
| Content-Range | 標準 HTTP Range 標頭,伺服端據此合併分片 |
| RandomAccessFile | seek() 定位 O(1),支援任意位移讀取,斷點續傳核心 |
| 緩衝區大小 | 8KB-64KB 平衡系統呼叫次數與記憶體佔用 |
| Keep-Alive | 多片上傳複用連線,減少 TLS 握手開銷 |
| 錯誤處理 | 讀取 ErrorStream 獲取伺服端詳細錯誤訊息 |
// 簡單固定分片
long fileSize = file.length();
long chunkSize = 5 * 1024 * 1024; // 5MB
int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
for (int i = 0; i < totalChunks; i++) {
long offset = i * chunkSize;
long size = Math.min(chunkSize, fileSize - offset);
uploadChunk(url, file, offset, size, fileName, boundary);
// 記錄已上傳片段索引至本地 SQLite/LevelDB,支援中斷續傳
}
/tmp/uploads/{fileId}/part_{index}fileId, totalChunks, receivedChunks(bitmap/set)Files.copy(part, mergedFile) 合併分段上傳核心在於 RandomAccessFile 定位 + Multipart 串流組裝 + Content-Range 標準化。Java 原生 HttpURLConnection 雖較繁瑣,但零依賴、完全可控,適合嵌入式、Android 或無法引入 OkHttp/Apache HttpClient 的環境。生產系統建議封裝為 Uploader 元件,統一處理重試、限流、進度回調、校驗等橫切關注點。