编写下载服务器。 第二部分:标头:Last-Modified,ETag和If-None-Match
客戶端緩存是萬維網的基礎之一。 服務器應告知客戶端資源的有效性,客戶端應盡可能快地對其進行緩存。 如我們所見,如果不緩存Web,將會非常慢。 只需在任何網站上Ctrl + F5并將其與普通F5進行比較-后者就會更快,因為它使用了已緩存的資源。 緩存對于下載也很重要。 如果我們已經獲取了幾兆字節的數據,并且它們沒有改變,則通過網絡推送它們是非常浪費的。
使用
HTTP ETag標頭可用于避免重復下載客戶端已有的資源。 服務器與第一響應服務器一起返回ETag標頭,該標頭通常是文件內容的哈希值。 客戶端可以保留ETag并在以后請求相同資源時將其發送(在If-None-Match請求標頭中)。 如果在此期間未更改,則服務器可以簡單地返回304 Not Modified響應。 讓我們從對ETag支持的集成測試開始:
def 'should send file if ETag not present'() {expect:mockMvc.perform(get('/download/' + FileExamples.TXT_FILE_UUID)).andExpect(status().isOk())}def 'should send file if ETag present but not matching'() {expect:mockMvc.perform(get('/download/' + FileExamples.TXT_FILE_UUID).header(IF_NONE_MATCH, '"WHATEVER"')).andExpect(status().isOk()) }def 'should not send file if ETag matches content'() {given:String etag = FileExamples.TXT_FILE.getEtag()expect:mockMvc.perform(get('/download/' + FileExamples.TXT_FILE_UUID).header(IF_NONE_MATCH, etag)).andExpect(status().isNotModified()).andExpect(header().string(ETAG, etag)) }有趣的是,Spring框架中內置了ShallowEtagHeaderFilter 。 安裝它會使所有測試通過,包括最后一個測試:
@WebAppConfiguration @ContextConfiguration(classes = [MainApplication]) @ActiveProfiles("test") class DownloadControllerSpec extends Specification {private MockMvc mockMvc@Autowiredpublic void setWebApplicationContext(WebApplicationContext wac) {mockMvc = MockMvcBuilders.webAppContextSetup(wac).addFilter(new Sha512ShallowEtagHeaderFilter(), "/download/*").build()}//tests...}我實際上插入了使用SHA-512而不是默認MD5的自己的Sha512ShallowEtagHeaderFilter 。 同樣由于某種原因,默認實現在哈希值前面加上0 :
public class ShallowEtagHeaderFilter {protected String generateETagHeaderValue(byte[] bytes) {StringBuilder builder = new StringBuilder("\"0");DigestUtils.appendMd5DigestAsHex(bytes, builder);builder.append('"');return builder.toString();}//... }與:
public class Sha512ShallowEtagHeaderFilter extends ShallowEtagHeaderFilter {@Overrideprotected String generateETagHeaderValue(byte[] bytes) {final HashCode hash = Hashing.sha512().hashBytes(bytes);return "\"" + hash + "\"";} }不幸的是,我們無法使用內置過濾器,因為它們必須首先完全讀取響應主體才能計算ETag 。 這基本上關閉了上一篇文章中介紹的主體流傳輸–整個響應都存儲在內存中。 我們必須自己實現ETag功能。 從技術上講, If-None-Match可以包含多個ETag值。 但是,谷歌瀏覽器和ShallowEtagHeaderFilter支持它,因此我們也將跳過它。 為了控制響應頭,我們現在返回ResponseEntity<Resource> :
@RequestMapping(method = GET, value = "/{uuid}") public ResponseEntity<Resource> download(@PathVariable UUID uuid,@RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt) {return storage.findFile(uuid).map(pointer -> prepareResponse(pointer, requestEtagOpt)).orElseGet(() -> new ResponseEntity<>(NOT_FOUND)); }private ResponseEntity<Resource> prepareResponse(FilePointer filePointer, Optional<String> requestEtagOpt) {return requestEtagOpt.filter(filePointer::matchesEtag).map(this::notModified).orElseGet(() -> serveDownload(filePointer)); }private ResponseEntity<Resource> notModified(String etag) {log.trace("Cached on client side {}, returning 304", etag);return ResponseEntity.status(NOT_MODIFIED).eTag(etag).body(null); }private ResponseEntity<Resource> serveDownload(FilePointer filePointer) {log.debug("Serving '{}'", filePointer);final InputStream inputStream = filePointer.open();final InputStreamResource resource = new InputStreamResource(inputStream);return ResponseEntity.status(OK).eTag(filePointer.getEtag()).body(resource); }該過程由可選的requestEtagOpt控制。 如果存在并且與客戶端發送的內容匹配,則返回304。否則照常發送200 OK。 本示例中介紹的FilePointer新方法如下所示:
import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; import com.google.common.io.Files;public class FileSystemPointer implements FilePointer {private final File target;private final HashCode tag;public FileSystemPointer(File target) {try {this.target = target;this.tag = Files.hash(target, Hashing.sha512());} catch (IOException e) {throw new IllegalArgumentException(e);}}@Overridepublic InputStream open() {try {return new BufferedInputStream(new FileInputStream(target));} catch (FileNotFoundException e) {throw new IllegalArgumentException(e);}}@Overridepublic String getEtag() {return "\"" + tag + "\"";}@Overridepublic boolean matchesEtag(String requestEtag) {return getEtag().equals(requestEtag);} }在這里,您將看到FileSystemPointer實現,該實現直接從文件系統讀取文件。 關鍵部分是緩存標記,而不是在每次請求時都重新計算標記。 上面的實現的行為符合預期,例如,Web瀏覽器不會再次下載資源。
3.使用
與ETag和If-None-Match標頭類似,還有Last-Modified和If-Modified-Since 。 我猜它們很容易解釋:第一個服務器返回Last-Modified響應標頭,指示給定資源的最后修改時間( duh! )。 客戶端緩存此時間戳,并將其與后續請求一起傳遞給If-Modified-Since請求標頭中的相同資源。 如果同時未更改資源,則服務器將響應304,從而節省帶寬。 這是一個后備機制,同時實現ETag和Last-Modified是一個很好的實踐。 讓我們從集成測試開始:
def 'should not return file if wasn\'t modified recently'() {given:Instant lastModified = FileExamples.TXT_FILE.getLastModified()String dateHeader = toDateHeader(lastModified)expect:mockMvc.perform(get('/download/' + FileExamples.TXT_FILE_UUID).header(IF_MODIFIED_SINCE, dateHeader)).andExpect(status().isNotModified()) }def 'should not return file if server has older version than the client'() {given:Instant lastModifiedLaterThanServer = FileExamples.TXT_FILE.getLastModified().plusSeconds(60)String dateHeader = toDateHeader(lastModifiedLaterThanServer)expect:mockMvc.perform(get('/download/' + FileExamples.TXT_FILE_UUID).header(IF_MODIFIED_SINCE, dateHeader)).andExpect(status().isNotModified()) }def 'should return file if was modified after last retrieval'() {given:Instant lastModifiedRecently = FileExamples.TXT_FILE.getLastModified().minusSeconds(60)String dateHeader = toDateHeader(lastModifiedRecently)expect:mockMvc.perform(get('/download/' + FileExamples.TXT_FILE_UUID).header(IF_MODIFIED_SINCE, dateHeader)).andExpect(status().isOk()) }private static String toDateHeader(Instant lastModified) {ZonedDateTime dateTime = ZonedDateTime.ofInstant(lastModified, ZoneOffset.UTC)DateTimeFormatter.RFC_1123_DATE_TIME.format(dateTime) }并執行:
@RequestMapping(method = GET, value = "/{uuid}") public ResponseEntity<Resource> download(@PathVariable UUID uuid,@RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt,@RequestHeader(IF_MODIFIED_SINCE) Optional<Date> ifModifiedSinceOpt) {return storage.findFile(uuid).map(pointer -> prepareResponse(pointer,requestEtagOpt,ifModifiedSinceOpt.map(Date::toInstant))).orElseGet(() -> new ResponseEntity<>(NOT_FOUND)); }private ResponseEntity<Resource> prepareResponse(FilePointer filePointer, Optional<String> requestEtagOpt, Optional<Instant> ifModifiedSinceOpt) {if (requestEtagOpt.isPresent()) {final String requestEtag = requestEtagOpt.get();if (filePointer.matchesEtag(requestEtag)) {return notModified(filePointer);}}if (ifModifiedSinceOpt.isPresent()) {final Instant isModifiedSince = ifModifiedSinceOpt.get();if (filePointer.modifiedAfter(isModifiedSince)) {return notModified(filePointer);}}return serveDownload(filePointer); }private ResponseEntity<Resource> serveDownload(FilePointer filePointer) {log.debug("Serving '{}'", filePointer);final InputStream inputStream = filePointer.open();final InputStreamResource resource = new InputStreamResource(inputStream);return response(filePointer, OK, resource); }private ResponseEntity<Resource> notModified(FilePointer filePointer) {log.trace("Cached on client side {}, returning 304", filePointer);return response(filePointer, NOT_MODIFIED, null); }private ResponseEntity<Resource> response(FilePointer filePointer, HttpStatus status, Resource body) {return ResponseEntity.status(status).eTag(filePointer.getEtag()).lastModified(filePointer.getLastModified().toEpochMilli()).body(body); }可悲的是,習慣上使用Optional不再看起來不錯,所以我堅持使用isPresent() 。 我們同時檢查If-Modified-Since和If-None-Match 。 如果兩者都不匹配,我們將照常提供文件。 只是為了讓您了解這些標頭的工作方式,讓我們執行一些端到端測試。 第一個要求:
> GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b HTTP/1.1 > ... > < HTTP/1.1 200 OK < ETag: "8b97c678a7f1d2e0af...921228d8e" < Last-Modified: Sun, 17 May 2015 15:45:26 GMT < ...帶有ETag后續請求(已縮短):
> GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b HTTP/1.1 > If-None-Match: "8b97c678a7f1d2e0af...921228d8e" > ... > < HTTP/1.1 304 Not Modified < ETag: "8b97c678a7f1d2e0af...921228d8e" < Last-Modified: Sun, 17 May 2015 15:45:26 GMT < ...如果我們的客戶僅支持Last-Modified :
> GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b HTTP/1.1 > If-Modified-Since: Tue, 19 May 2015 06:59:55 GMT > ... > < HTTP/1.1 304 Not Modified < ETag: "8b97c678a7f1d2e0af9cda473b36c21f1b68e35b93fec2eb5c38d182c7e8f43a069885ec56e127c2588f9495011fd8ce032825b6d3136df7adbaa1f921228d8e" < Last-Modified: Sun, 17 May 2015 15:45:26 GMT有許多內置工具,例如過濾器,可以為您處理緩存。 但是,如果您需要確保在服務器端流傳輸文件而不是對其進行預先緩沖,則需要格外小心。
編寫下載服務器
- 第一部分:始終流式傳輸,永遠不要完全保留在內存中
- 第二部分:標頭:Last-Modified,ETag和If-None-Match
- 第三部分:標頭:內容長度和范圍
- 第四部分:有效地實現HEAD操作
- 第五部分:油門下載速度
- 第六部分:描述您發送的內容(內容類型等)
- 這些文章中開發的示例應用程序可在GitHub上找到。
翻譯自: https://www.javacodegeeks.com/2015/06/writing-a-download-server-part-ii-headers-last-modified-etag-and-if-none-match.html
總結
以上是生活随笔為你收集整理的编写下载服务器。 第二部分:标头:Last-Modified,ETag和If-None-Match的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何安装谷歌服务框架?(Google三件
- 下一篇: mapreduce介绍_MapReduc