在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range
请求头指定下载范围:
实现原理
-
Range 请求头 :向服务器请求文件的特定字节范围(如
Range: bytes=1024-
) -
本地文件记录:保存已下载的字节位置
-
206 状态码处理:服务器返回部分内容(HTTP 206 Partial Content)
-
文件追加写入:从上次中断的位置继续写入文件
完整实现代码(Kotlin/Java)
kotlin
Kotlin
import okhttp3.*
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
class ResumeDownloader(
private val client: OkHttpClient = OkHttpClient()
) {
fun download(url: String, file: File, listener: ProgressListener? = null) {
// 1. 获取已下载字节数
val downloadedBytes = if (file.exists()) file.length() else 0L
// 2. 创建带Range头的请求
val request = Request.Builder()
.url(url)
.header("Range", "bytes=$downloadedBytes-")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
listener?.onError(e)
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
listener?.onError(IOException("Unexpected code: ${response.code}"))
return
}
// 3. 检查服务器是否支持断点续传
val isResumeSupported = response.code == 206 // Partial Content
val totalBytes = response.header("Content-Length")?.toLongOrNull() ?: -1L
val finalTotalBytes = if (isResumeSupported) {
downloadedBytes + (totalBytes)
} else {
totalBytes
}
// 4. 处理响应体
response.body?.use { body ->
RandomAccessFile(file, "rw").use { output ->
// 移动到文件末尾追加
output.seek(downloadedBytes)
val input = body.byteStream()
val buffer = ByteArray(8192)
var bytesRead: Int
var progress = downloadedBytes
// 5. 写入文件
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
progress += bytesRead
// 更新进度
listener?.onProgress(progress, finalTotalBytes)
}
listener?.onComplete(file)
}
}
}
})
}
interface ProgressListener {
fun onProgress(currentBytes: Long, totalBytes: Long)
fun onComplete(file: File)
fun onError(e: Exception)
}
}
Java 版本核心代码
java
java
// 创建带Range头的请求
long downloadedBytes = file.length();
Request request = new Request.Builder()
.url(url)
.addHeader("Range", "bytes=" + downloadedBytes + "-")
.build();
// 处理响应
try (Response response = client.newCall(request).execute()) {
if (response.code() == 206) { // Partial Content
try (RandomAccessFile output = new RandomAccessFile(file, "rw");
InputStream input = response.body().byteStream()) {
output.seek(downloadedBytes);
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
}
}
}
关键注意事项
-
服务器支持检查:
-
成功时返回 HTTP 206(部分内容)
-
失败时返回 200(完整文件)或 416(范围请求错误)
-
响应头需包含
Accept-Ranges: bytes
-
-
文件处理:
-
使用
RandomAccessFile
实现文件随机访问 -
通过
seek()
定位到文件末尾 -
避免覆盖已下载内容
-
-
进度跟踪:
-
总大小 = 已下载大小 +
Content-Length
-
实时计算:
currentBytes += bytesRead
-
-
异常处理:
-
网络中断时保存当前进度
-
重新下载时使用最新文件长度
-
增强功能建议
-
进度持久化:使用数据库记录下载状态
-
暂停/恢复:暴露下载控制接口
-
多线程下载:分割文件范围并行下载(需服务器支持)
-
完整性校验:下载完成后验证文件 MD5/SHA1
示例用法:
kotlin
Kotlin
val downloader = ResumeDownloader()
val file = File(context.filesDir, "largefile.zip")
downloader.download("https://example.com/largefile.zip", file,
object : ResumeDownloader.ProgressListener {
override fun onProgress(current: Long, total: Long) {
val percent = (current * 100 / total).toInt()
updateProgressBar(percent)
}
override fun onComplete(file: File) {
showToast("下载完成")
}
override fun onError(e: Exception) {
showError(e.message)
}
}
)
通过此实现,OkHttp 可在网络中断后自动从断点恢复下载,大幅提升大文件下载体验。