服务器编写_编写下载服务器。 第六部分:描述您发送的内容(内容类型等)...
服務器編寫
就HTTP而言,客戶端下載的只是一堆字節。 但是,客戶真的很想知道如何解釋這些字節。 它是圖像嗎? 還是ZIP文件? 本系列的最后一部分描述了如何向客戶端提示她下載的內容。
設置
內容類型描述了返回的資源的MIME類型 。 此標頭指示Web瀏覽器如何處理從下載服務器流出的字節流。 如果沒有此標頭,瀏覽器將無法得知其實際接收到的內容,只會像顯示文本文件一樣顯示內容。 不用說二進制PDF(請參見上面的屏幕截圖),像文本文件一樣顯示的圖像或視頻看起來并不好。 最難的部分是實際上以某種方式獲取媒體類型。 幸運的是,Java本身有一個工具可以根據資源的擴展名和/或內容來猜測媒體類型:
import com.google.common.net.MediaType; import java.io.*; import java.time.Instant;public class FileSystemPointer implements FilePointer {private final MediaType mediaTypeOrNull;public FileSystemPointer(File target) {final String contentType = java.nio.file.Files.probeContentType(target.toPath());this.mediaTypeOrNull = contentType != null ?MediaType.parse(contentType) :null;}請注意,使用Optional<T>作為類字段不是慣用的,因為它不是可Serializable ,并且避免了潛在的問題。 知道媒體類型后,我們必須在響應中返回它。 注意,這一小段代碼使用了JDK 8和Guava中的Optional以及Spring框架和Guava中的MediaType類。 多么糟糕的類型系統!
private ResponseEntity<Resource> response(FilePointer filePointer, HttpStatus status, Resource body) {final ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.status(status).eTag(filePointer.getEtag()).contentLength(filePointer.getSize()).lastModified(filePointer.getLastModified().toEpochMilli());filePointer.getMediaType().map(this::toMediaType).ifPresent(responseBuilder::contentType);return responseBuilder.body(body); }private MediaType toMediaType(com.google.common.net.MediaType input) {return input.charset().transform(c -> new MediaType(input.type(), input.subtype(), c)).or(new MediaType(input.type(), input.subtype())); }@Override public Optional<MediaType> getMediaType() {return Optional.ofNullable(mediaTypeOrNull); }保留原始文件名和擴展名
當您直接在Web瀏覽器中打開文檔時,雖然Content-type效果很好,但是可以想象您的用戶將該文檔存儲在磁盤上。 瀏覽器是決定顯示還是存儲下載的文件不在本文的討論范圍之內,但是我們應該為兩者做好準備。 如果瀏覽器只是將文件存儲在磁盤上,則必須使用某種名稱進行保存。 默認情況下,Firefox將使用URL的最后一部分,在本例中,該部分恰好是資源的UUID。 不太用戶友好。 Chrome是好一點-知道根據MIME類型Content-type報頭,將試探性地加入適當的擴展名,例如.zip中的情況下, application/zip 。 但是文件名仍然是隨機的UUID,而用戶上傳的文件可能是cats.zip 。 因此,如果您的目標是瀏覽器而不是自動化客戶端,則最好使用真實名稱作為URL的最后一部分。 我們仍然希望使用UUID在內部區分資源,避免沖突并且不公開我們的內部存儲結構。 但是在外部,我們可以重定向到用戶友好的URL,但為了安全起見保留UUID。 首先,我們需要一個額外的端點:
@RequestMapping(method = {GET, HEAD}, value = "/{uuid}") public ResponseEntity<Resource> redirect(HttpMethod method,@PathVariable UUID uuid,@RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt,@RequestHeader(IF_MODIFIED_SINCE) Optional<Date> ifModifiedSinceOpt) {return findExistingFile(method, uuid).map(file -> file.redirect(requestEtagOpt, ifModifiedSinceOpt)).orElseGet(() -> new ResponseEntity<>(NOT_FOUND)); }@RequestMapping(method = {GET, HEAD}, value = "/{uuid}/{filename}") public ResponseEntity<Resource> download(HttpMethod method,@PathVariable UUID uuid,@RequestHeader(IF_NONE_MATCH) Optional<String> requestEtagOpt,@RequestHeader(IF_MODIFIED_SINCE) Optional<Date> ifModifiedSinceOpt) {return findExistingFile(method, uuid).map(file -> file.handle(requestEtagOpt, ifModifiedSinceOpt)).orElseGet(() -> new ResponseEntity<>(NOT_FOUND)); }private Optional<ExistingFile> findExistingFile(HttpMethod method, @PathVariable UUID uuid) {return storage.findFile(uuid).map(pointer -> new ExistingFile(method, pointer, uuid)); }如果仔細觀察,甚至沒有使用{filename} ,它只是瀏覽器的提示。 如果需要更高的安全性,可以將提供的文件名與映射到給定UUID文件名進行比較。 這里真正重要的是,僅要求提供UUID重定向我們:
$ curl -v localhost:8080/download/4a8883b6-ead6-4b9e-8979-85f9846cab4b > GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b HTTP/1.1 ... < HTTP/1.1 301 Moved Permanently < Location: /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b/cats.zip而且您需要進行一次額外的網絡行程來獲取實際文件:
> GET /download/4a8883b6-ead6-4b9e-8979-85f9846cab4b/cats.zip HTTP/1.1 ... > HTTP/1.1 200 OK < ETag: "be20c3b1...fb1a4" < Last-Modified: Thu, 21 Aug 2014 22:44:37 GMT < Content-Type: application/zip;charset=UTF-8 < Content-Length: 489455該實現很簡單,但是為了避免重復,對其進行了一些重構:
public ResponseEntity<Resource> redirect(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {if (cached(requestEtagOpt, ifModifiedSinceOpt))return notModified(filePointer);return redirectDownload(filePointer); }public ResponseEntity<Resource> handle(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {if (cached(requestEtagOpt, ifModifiedSinceOpt))return notModified(filePointer);return serveDownload(filePointer); }private boolean cached(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {final boolean matchingEtag = requestEtagOpt.map(filePointer::matchesEtag).orElse(false);final boolean notModifiedSince = ifModifiedSinceOpt.map(Date::toInstant).map(filePointer::modifiedAfter).orElse(false);return matchingEtag || notModifiedSince; }private ResponseEntity<Resource> redirectDownload(FilePointer filePointer) {try {log.trace("Redirecting {} '{}'", method, filePointer);return ResponseEntity.status(MOVED_PERMANENTLY).location(new URI("/download/" + uuid + "/" + filePointer.getOriginalName())).body(null);} catch (URISyntaxException e) {throw new IllegalArgumentException(e);} }private ResponseEntity<Resource> serveDownload(FilePointer filePointer) {log.debug("Serving {} '{}'", method, filePointer);final InputStreamResource resource = resourceToReturn(filePointer);return response(filePointer, OK, resource); }您甚至可以進一步使用高階函數來避免重復:
public ResponseEntity<Resource> redirect(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {return serveWithCaching(requestEtagOpt, ifModifiedSinceOpt, this::redirectDownload); }public ResponseEntity<Resource> handle(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt) {return serveWithCaching(requestEtagOpt, ifModifiedSinceOpt, this::serveDownload); }private ResponseEntity<Resource> serveWithCaching(Optional<String> requestEtagOpt, Optional<Date> ifModifiedSinceOpt, Function<FilePointer, ResponseEntity<Resource>> notCachedResponse) {if (cached(requestEtagOpt, ifModifiedSinceOpt))return notModified(filePointer);return notCachedResponse.apply(filePointer); }顯然,額外的重定向是每次下載都必須支付的額外費用,因此這是一個折衷方案。 您可以考慮基于User-agent啟發式(如果是瀏覽器,則為重定向;如果是自動客戶端,則為服務器),以避免非人工客戶端的重定向。 這樣就結束了我們有關文件下載的系列文章。 HTTP / 2的出現必將帶來更多的改進和技術,例如確定優先級。
編寫下載服務器
- 第一部分:始終流式傳輸,永遠不要完全保留在內存中
- 第二部分:標頭:Last-Modified,ETag和If-None-Match
- 第三部分:標頭:內容長度和范圍
- 第四部分:有效地執行HEAD操作
- 第五部分:油門下載速度
- 第六部分:描述您發送的內容(內容類型等)
- 這些文章中開發的示例應用程序可在GitHub上找到。
翻譯自: https://www.javacodegeeks.com/2015/07/writing-a-download-server-part-vi-describe-what-you-send-content-type-et-al.html
服務器編寫
總結
以上是生活随笔為你收集整理的服务器编写_编写下载服务器。 第六部分:描述您发送的内容(内容类型等)...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 常用的ddos软件下载(常用的ddos软
- 下一篇: 人民法院备案(法院备案单)