經(jīng)常會(huì)有同學(xué)問:文件的斷點(diǎn)上傳如何實(shí)現(xiàn)劳殖?
斷點(diǎn)上傳/下載凸郑,這是在客戶端經(jīng)常遇到的場景,當(dāng)我們需要上傳或下載一個(gè)大文件時(shí),都會(huì)考慮使用斷點(diǎn)續(xù)傳的方式辨赐。
斷點(diǎn)上傳相較于斷點(diǎn)下載來說优俘,最大的區(qū)別就在于斷點(diǎn)位置的記錄,上傳記錄在服務(wù)端掀序,下載記錄在客戶端帆焕,因此,客戶端需要在上傳前不恭,通過接口去拿到文件的斷點(diǎn)位置叶雹,然后在上傳時(shí),將文件輸入流跳轉(zhuǎn)到斷點(diǎn)位置
一换吧、準(zhǔn)備工作
對(duì)于文件上傳折晦,其實(shí)就是打開文件的輸入流,不停的讀取數(shù)據(jù)到byte數(shù)組中沾瓦,隨后寫出到服務(wù)端满着;那客戶端要做的就是跳過已經(jīng)上傳的部分,也就是直接跳到斷點(diǎn)位置贯莺,這樣就可以從斷點(diǎn)位置去讀取數(shù)據(jù)风喇,也就達(dá)到了斷點(diǎn)上傳的目的。
偽代碼如下:
String filePath = "...";
long skipSize = 100; //假設(shè)斷點(diǎn)位置是 100 byte
InputStream input = input = new FileInputStream(filePath);
input.skip(skipSize) //跳轉(zhuǎn)到斷點(diǎn)位置
然而缕探,OkHttp并沒有直接提供設(shè)置斷點(diǎn)的方法魂莫,所以需要客戶端自定義RequestBody
,取名為FileRequestBody
爹耗,如下:
//為簡化閱讀耙考,已省略部分代碼
public class FileRequestBody extends RequestBody {
private final File file;
private final long skipSize; //斷點(diǎn)位置
private final MediaType mediaType;
public FileRequestBody(File file, long skipSize, @Nullable MediaType mediaType) {
this.file = file;
this.skipSize = skipSize;
this.mediaType = mediaType;
}
@Override
public long contentLength() throws IOException {
return file.length() - skipSize;
}
@Override
public void writeTo(@NotNull BufferedSink sink) throws IOException {
InputStream input = null;
Source source = null;
try {
input = new FileInputStream(file);
if (skipSize > 0) {
input.skip(skipSize); //跳到斷點(diǎn)位置
}
source = Okio.source(input);
sink.writeAll(source);
} finally {
OkHttpCompat.closeQuietly(source, input);
}
}
}
為方便閱讀,以上省略部分源碼潭兽,FileRequestBody類完整源碼
有了FileRequestBody
類琳骡,我們只需要傳入一個(gè)斷點(diǎn)位置,剩下的工作就跟普通的文件上傳一樣讼溺。 接下來,直接進(jìn)入代碼實(shí)現(xiàn)最易。
二怒坯、代碼實(shí)現(xiàn)
2.1 獲取斷點(diǎn)位置
首先,需要服務(wù)端提供一個(gè)接口藻懒,通過userId
去查找該用戶未上傳完成的任務(wù)列表剔猿,代碼如下:
RxHttp.get("/.../getToUploadTask")
.add("userId", "88888888")
.asList<ToUploadTask>()
.subscribe({
//成功回調(diào),這里通過 it 拿到 List<ToUploadTask>
}, {
//異虫揖#回調(diào)
});
其中ToUploadTask
類如下:
//待上傳任務(wù)
data class ToUploadTask(
val md5: String, //文件的md5归敬,用于驗(yàn)證文件的唯一性
val filePath: String, //文件在客戶端的絕對(duì)路徑
val skipSize: Long = 0 //斷點(diǎn)位置
)
注:md5、filePath 這兩個(gè)參數(shù)需要客戶端在文件上傳時(shí)傳遞給服務(wù)端,用于對(duì)文件的校驗(yàn)汪茧,防止文件錯(cuò)亂
2.2 斷點(diǎn)上傳
有了待上傳任務(wù)椅亚,客戶端就可以執(zhí)行斷點(diǎn)上傳操作,OkHttp代碼如下:
fun uploadFile(uploadTask: ToUploadTask) {
//1.校驗(yàn)文件是否存在
val file = File(uploadTask.filePath)
if (!file.exists() && !file.isFile) return
//2.校驗(yàn)文件的 md5 值
val fileMd5 = FileUtils.getFileMD5ToString(file)
if (!fileMd5.equals(uploadTask.md5)) return
//3.構(gòu)建請(qǐng)求體
val fileRequestBody = FileRequestBody(file, uploadTask.skipSize, BuildUtil.getMediaType(file.name))
val multipartBody = MultipartBody.Builder()
.addFormDataPart("userId", "88888888")
.addFormDataPart("md5", fileMd5)
.addFormDataPart("filePath", file.absolutePath)
.addFormDataPart("file", file.name, fileRequestBody) //添加文件body
.build()
//4.構(gòu)建請(qǐng)求
val request = Request.Builder()
.url("/.../uploadFile")
.post(multipartBody)
.build()
//5.執(zhí)行請(qǐng)求
val okClient = OkHttpClient.Builder().build()
okClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
//異巢瘴郏回調(diào)
}
override fun onResponse(call: Call, response: Response) {
//成功回調(diào)
}
})
}
當(dāng)然,考慮到很少人會(huì)直接使用OkHttp扩灯,所以這里也貼出RxHttp的實(shí)現(xiàn)代碼媚赖,很簡單,僅需構(gòu)建一個(gè)UpFile對(duì)象即可珠插,就可很方便的監(jiān)聽上傳進(jìn)度惧磺,代碼如下:
fun uploadFile(uploadTask: ToUploadTask) {
//1.校驗(yàn)文件是否存在
val file = File(uploadTask.filePath)
if (!file.exists() && !file.isFile) return
//2.校驗(yàn)文件的 md5 值
val fileMd5 = FileUtils.getFileMD5ToString(file)
if (!fileMd5.equals(uploadTask.md5)) return
val upFile = UpFile("file", file, file.name, uploadTask.skipSize)
//3.直接上傳
RxHttp.postForm("/.../uploadFile")
.add("userId", "88888888")
.add("md5", fileMd5)
.add("filePath", file.absolutePath)
.addFile(upFile)
.upload(AndroidSchedulers.mainThread()) {
//上傳進(jìn)度回調(diào)
}
.asString()
.subscribe({
//成功回調(diào)
}, {
//異常回調(diào)
})
}
小結(jié)
斷點(diǎn)上傳相較普通的文件上傳捻撑,客戶端多了一個(gè)斷點(diǎn)的設(shè)置磨隘,大部分工作量在服務(wù)端,服務(wù)端不僅需要處理文件的拼接邏輯布讹,還需記錄未上傳完成的任務(wù)琳拭,并通過接口暴露給客戶端。