502无法解析服务器标头_编写下载服务器。 第二部分:标头:Last-Modified,ETag和If-None-Match...

502无法解析服务器标头

客户端缓存是万维网的基础之一。 服务器应通知客户端资源的有效性,客户端应尽可能快地对其进行缓存。 如我们所见,如果不缓存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
 
    @Autowired
    public 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 {
 
    @Override
    protected 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);
        }
    }
 
    @Override
    public InputStream open() {
        try {
            return new BufferedInputStream(new FileInputStream(target));
        } catch (FileNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }
 
    @Override
    public String getEtag() {
        return "\"" + tag + "\"";
    }
 
    @Override
    public boolean matchesEtag(String requestEtag) {
        return getEtag().equals(requestEtag);
    }
}

在这里,您将看到FileSystemPointer实现,该实现直接从文件系统读取文件。 关键部分是缓存标记,而不是在每次请求时都重新计算标记。 上面的实现的行为符合预期,例如,Web浏览器不会再次下载资源。

3.使用

ETagIf-None-Match标头类似,还有Last-ModifiedIf-Modified-Since 。 我想它们很容易解释:第一台服务器返回Last-Modified响应标头,指示给定资源的最后修改时间( duh! )。 客户端缓存此时间戳,并将其与后续请求一起传递给If-Modified-Since请求标头中的相同资源。 如果同时未更改资源,则服务器将响应304,从而节省带宽。 这是一个后备机制,同时实现ETagLast-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-SinceIf-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

有许多内置工具,例如过滤器,可以为您处理缓存。 但是,如果需要确保在服务器端流传输文件而不是对其进行预缓冲,则需要格外小心。

编写下载服务器

翻译自: https://www.javacodegeeks.com/2015/06/writing-a-download-server-part-ii-headers-last-modified-etag-and-if-none-match.html

502无法解析服务器标头

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值