背景
因?yàn)楣緲I(yè)務(wù)需求,對(duì)接一款終端設(shè)備,需要下載黑名單??墒窃O(shè)備的性能較差,每次下載的報(bào)文大小有限制。所以需要采用報(bào)文分段下載來實(shí)現(xiàn)。
當(dāng)然其實(shí)這種需求最好是用查詢分頁來達(dá)到分段下載是最好的,可是因?yàn)榻K端那邊實(shí)現(xiàn)問題還有以及其他各種原因,不得已采用這種分段下載方式。
找了很多帖子比較少有關(guān)于這種分段返回報(bào)文的內(nèi)容,可是有發(fā)現(xiàn)可以利用斷點(diǎn)下載的思想,去實(shí)現(xiàn)這個(gè)分段報(bào)文返回。有斷點(diǎn)下載經(jīng)驗(yàn)的同學(xué),應(yīng)該很快能理解本文介紹的內(nèi)容,其實(shí)都是很皮毛的東西。
正題
分段下載我們主要用到兩個(gè)http頭信息:
http 響應(yīng)頭 Accept-Ranges
http 請(qǐng)求頭 Range
服務(wù)器使用 HTTP 響應(yīng)頭Accept-Ranges標(biāo)識(shí)自身支持范圍請(qǐng)求(partial requests)。字段的具體值用于定義范圍請(qǐng)求的單位。當(dāng)瀏覽器/客戶端發(fā)現(xiàn) Accept-Ranges 頭時(shí),可以嘗試?yán)^續(xù)中斷了的下載,而不是重新開始。用法:Accept-Ranges: bytes
Range用來標(biāo)志本次請(qǐng)求,獲取數(shù)據(jù)的范圍。如:Range: bytes=100-200
步驟
客戶端請(qǐng)求頭都帶上Range,第一個(gè)請(qǐng)求Range:bytes=0-99,第二個(gè)Range:bytes=100-199,以此類推。
服務(wù)端除了響應(yīng)頭帶上Accept-Ranges: bytes,還需要根據(jù)請(qǐng)求中的Range來切割報(bào)文,返回Range范圍內(nèi)的數(shù)據(jù)。
http響應(yīng)碼設(shè)置為206
http響應(yīng)頭新增Content-Range,標(biāo)識(shí)本次響應(yīng)的內(nèi)容的范圍,和整個(gè)完整報(bào)文的長(zhǎng)度
需要注意一點(diǎn)的是,Range單位是byte而不是字符數(shù),所以計(jì)算長(zhǎng)度的時(shí)候要要將字符串換算成byte計(jì)算。
接下來展示controller層部分代碼:
// 省略業(yè)務(wù)代碼
//rsp為黑名單的響應(yīng)dto對(duì)象,全量的黑名單
String returnJson = JsonUtil.Object2Json(rsp);
httpResp.setContentType("application/json");
//無緩存
httpResp.setHeader("Cache-Control", "no-cache");
//設(shè)置UTF-8編碼
httpResp.setCharacterEncoding(Constant.CHARACTER_ENCODING);
//可以斷點(diǎn)下載
httpResp.setHeader("Accept-Ranges", "bytes");
//設(shè)置http響應(yīng)碼為206
httpResp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
//獲取頭信息Range,并且去掉bytes=
String range = httpReq.getHeader("Range").replaceAll("bytes=", "");
String[] rangeSplit = range.split("-");
//整段報(bào)文的總長(zhǎng)度,要用utf-8的編碼字節(jié)長(zhǎng)度計(jì)算
int returnJsonLength = returnJson.getBytes("UTF-8").length;
//offset 第一次請(qǐng)求默認(rèn)從0開始
int offset = Integer.parseInt(rangeSplit[0]);
// 初始化 length 和 endIndex
// length:本次報(bào)文返回的長(zhǎng)度;endIndex:本次報(bào)文結(jié)束的下標(biāo)位置,默認(rèn)是報(bào)文的總長(zhǎng)度所以是returnJsonLength - 1
int length = returnJsonLength - offset;
int endIndex = returnJsonLength - 1;
//設(shè)置 length 和 endIndex 值
if (rangeSplit.length > 1) {
//strSplit數(shù)組長(zhǎng)度大于1,說明Range是一個(gè)范圍,而不是一個(gè)固定值
endIndex = Integer.parseInt(rangeSplit[1]);
//endIndex不允許大于等于報(bào)文長(zhǎng)度
if (endIndex >= returnJsonLength) {
endIndex = returnJsonLength - 1;
}
//因?yàn)閑ndIndex最大值比報(bào)文長(zhǎng)度小于1的,而length是報(bào)文返回長(zhǎng)度,所以要+1
length = endIndex - offset + 1;
}
httpResp.setHeader("Content-Length", Long.toString(returnJsonLength));
//Content-Range:bytes[json的開始字節(jié)]-[json的結(jié)束字節(jié)]/[報(bào)文的總大小]
httpResp.setHeader("Content-Range", "bytes " + offset + "-" + endIndex + "/" + returnJsonLength);
// 獲得寫出流
try {
OutputStream outputStream = httpResp.getOutputStream();
//重點(diǎn)來了,輸出報(bào)文,只返回原完整報(bào)文的offset至offset + length的字節(jié)內(nèi)容
outputStream.write(returnJson.getBytes("utf-8"), offset, length);
outputStream.flush();
outputStream.close();
} catch (Exception e) {
LogUtil.addProclog(procid, "返回報(bào)文錯(cuò)誤:" + CommonUtil.getTrace(e));
}
缺點(diǎn):
每次請(qǐng)求只返回部分的報(bào)文,可是每次都是獲取全量的黑名單,數(shù)據(jù)庫查詢多次??梢杂胷edis緩存黑名單,減少數(shù)據(jù)庫查詢。僅以記錄本次的解決方案,如果有更好的思路不妨一起交流一下,不才獻(xiàn)丑了。