斷點續傳核心是 HTTP/1.1 Range 標頭,客戶端聲明需下載的字節範圍,服務端返回 206 Partial Content 及對應數據塊。常見範圍格式:
bytes=0-499:前 500 字節bytes=500-:第 500 字節到結尾bytes=-500:最後 500 字節bytes=0-499,600-1099:多範圍(本文不支持)public class FileIoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
download(req, resp);
}
private void download(HttpServletRequest req, HttpServletResponse resp) throws IOException {
File file = getFile(req); // 解析 uri 參數獲取文件
long startPos = FileUtil.headerSetting(file, req, resp); // 處理 Range,返回起始位置
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
BufferedOutputStream out = new BufferedOutputStream(resp.getOutputStream())) {
raf.seek(startPos);
byte[] buffer = new byte[1024];
int len;
while ((len = raf.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.flush();
} catch (IOException e) {
// 客戶端斷開連接等忽略
}
}
private File getFile(HttpServletRequest req) throws UnsupportedEncodingException {
String uri = URLDecoder.decode(req.getParameter("uri"), "UTF-8");
if (uri.startsWith("file://")) return new File(uri.substring(7));
if (uri.startsWith("hbase://")) return new File(new URI(uri));
throw new RuntimeException("非法 URI 協議");
}
}
public class FileUtil {
public static long headerSetting(File file, HttpServletRequest req, HttpServletResponse resp) {
long fileLen = file.length();
String rangeHeader = req.getHeader("Range");
if (rangeHeader == null) { // 全量下載
setResponse(new RangeSettings(fileLen), file.getName(), resp);
return 0;
}
String range = rangeHeader.replace("bytes=", "");
RangeSettings settings = parseRange(fileLen, range);
setResponse(settings, file.getName(), resp);
return settings.getStart();
}
private static RangeSettings parseRange(long fileLen, String range) {
long start, end, contentLength;
if (range.startsWith("-")) { // -500 最後 N 字節
contentLength = Long.parseLong(range.substring(1));
end = fileLen - 1;
start = fileLen - contentLength;
} else if (range.endsWith("-")) { // 500- 從 N 到結尾
start = Long.parseLong(range.replace("-", ""));
end = fileLen - 1;
contentLength = fileLen - start;
} else { // 0-499 區間
String[] parts = range.split("-");
start = Long.parseLong(parts[0]);
end = Long.parseLong(parts[1]);
contentLength = end - start + 1;
}
return new RangeSettings(start, end, contentLength, fileLen);
}
private static void setResponse(RangeSettings s, String fileName, HttpServletResponse resp) {
resp.setHeader("Content-Disposition", "attachment; filename=\"" + IoUtil.toUtf8String(fileName) + "\"");
resp.setContentType(IoUtil.setContentType(fileName));
if (!s.isRange()) { // 全量
resp.setHeader("Content-Length", String.valueOf(s.getTotalLength()));
} else { // 斷點
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206
resp.setHeader("Content-Length", String.valueOf(s.getContentLength()));
resp.setHeader("Content-Range", "bytes " + s.getStart() + "-" + s.getEnd() + "/" + s.getTotalLength());
}
}
}
public class RangeSettings {
private long start, end, contentLength, totalLength;
private boolean range;
public RangeSettings(long totalLength) { // 全量構造
this.totalLength = totalLength;
this.range = false;
}
public RangeSettings(long start, long end, long contentLength, long totalLength) {
this.start = start; this.end = end; this.contentLength = contentLength; this.totalLength = totalLength;
this.range = true;
}
// Getter/Setter 省略...
}
public class IoUtil {
// 根據副檔名返回 MIME,此處省略完整映射表...
public static String setContentType(String fileName) { /* ... */ return "application/octet-stream"; }
/** RFC 5987 編碼,解決中文文件名跨瀏覽器亂碼 */
public static String toUtf8String(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c <= 0x7F) sb.append(c); // ASCII 直接保留
else {
byte[] bytes = Character.toString(c).getBytes(StandardCharsets.UTF_8);
for (byte b : bytes) sb.append('%').append(String.format("%02X", b));
}
}
return sb.toString();
}
}
| 下載工具/瀏覽器 | 斷點續傳 | 多線程 | 備註 |
|---|---|---|---|
| Chrome / Firefox / Edge | ✅ | ❌ | 單線程 Range |
| 迅雷 / IDM / FDM | ✅ | ✅ | 多範圍請求需額外支持 |
| wget -c / curl -C - | ✅ | ❌ | 命令行工具 |
注意:本實現僅支持單範圍 bytes=start-end,多範圍 bytes=0-499,600- 需返回 multipart/byteranges,複雜度較高,實際項目中建議使用成熟庫(如 Spring ResourceHttpRequestHandler)。
Transfer-Encoding: chunked 或 NIO 零拷貝 (FileChannel.transferTo)getFile() 中集成認證授權邏輯ETag/Last-Modified 支持條件請求此實現經過迅雷 7、Firefox、Chrome 實測,可滿足大多數業務場景的斷點續傳需求。