(编辑:jimmy 日期: 2024/12/25 浏览:2)
前言
Tomcat 中的请求都是由 Servlet 处理,静态资源也不例外。在默认的 web.xml 中,配置了一个 DefaultServlet 用于处理静态资源,它支持缓存和断点续传。
DefaultServlet 的基本处理过程如下:
接下来主要分析资源缓存的设计和实现,以及 If 头域的处理。
1. 资源缓存的设计
访问磁盘的速度远远低于访问内存的速度,所以适当的缓存一部分静态资源能够让系统快速响应。
Tomcat 在 6.0.53 版本实现静态资源的处理时,借助了 JNDI 的一些 API(但在使用时感觉与 JNDI 的关系不大),相关类图及核心方法和属性如下:
缓存相关的类:
资源目录相关的类是:
默认情况下,缓存最大为 10 MB,单个缓存资源最大为 512 KB,缓存的 TTL 为 5s。
一般的,在 Mapper 映射到处理静态资源的 Wrapper 时,会引起资源的加载,基本的方法调用情况如下:
Mapper.map(MessageBytes, MessageBytes, MappingData) └─Mapper.internalMap(CharChunk, CharChunk, MappingData) └─Mapper.internalMapWrapper(Mapper$Context, CharChunk, MappingData) └─ProxyDirContext.lookup(String) └─ProxyDirContext.cacheLookup(String) └─ResourceCache.lookup(String) └─ResourceCache.find(CacheEntry[], String)
缓存资源插入内部数组时是有序的,find 方法就是通过资源名二分查找缓存,资源名就是请求路径,此时有两种情况,缓存命中和未命中。
缓存未命中,在 cacheLookup 方法中会新建一个 CacheEntry 对象,调用 cacheLoad 方法加入到 ResourceCache 的缓存数组中,加入前会对缓存条目进行以下操作:
缓存命中,会对缓存条目进行校验:
以上就是资源缓存简单的处理过程。
2. If 头域的处理
客户端接收并缓存请求的资源,,当再次请求此资源时,服务端根据特定的请求头域来验证资源是否修改,没有变动,则只返回一个 304 Not Modified 响应,否则返回资源的内容,从而节省带宽。
用于资源验证的头域有两种,分别是:Last-Modified+If-Modified-Since 和 ETag+If-None-Match。
Last-Modified+If-Modified-Since,单位是秒,这个容易理解,如果服务端资源的最后修改时间小于 If-Modified-Since 的值,表示资源无变动。与 If-Modified-Since 对应的有个 If-Unmodified-Since,它类似一个断言,小于此时间戳的资源才返回,大于等于的话会返回 412 Precondition Failed 的错误。
使用时间戳校验有几个弊端:
因此,HTTP 引入了 ETag。ETag(Entity Tags) 资源唯一标识,可看做服务端为资源生成的一个 Token,用于校验资源是否修改。HTTP 只规定 ETag 要放在双引号内,没有规定内容是什么或者要怎么实现,Tomcat 生成 ETag 的逻辑是 "W/\"" + contentLength + "-" + lastModified + "\""
,其中 'W/' 表示大小写敏感。
ETag+If-None-Match,If-None-Match 的值由一个或多个 ETag 组成,多个以逗号分割,如果服务端资源的 ETag 与其中的任何一个都不匹配,表示请求的资源有修改;否则无变动。它还有一个特殊值-星号(*),只在资源上传时使用,通常是 PUT 方法,检查是否已经上传过。
此外 If-None-Match 的优先级高于 If-Modified-Since,也就是说,存在 If-None-Match 就不对最后修改时间进行校验。与 If-None-Match 相对的有个 If-Match,它也类似断言,只有资源的 ETag 匹配时才认为没有修改,通常用于断点续传。
Tomcat 实现此部分的核心代码如下:
// 返回 true 是才认为资源有变动 protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,ResourceAttributes resourceAttributes) throws IOException { return checkIfMatch(request, response, resourceAttributes) && checkIfModifiedSince(request, response, resourceAttributes) && checkIfNoneMatch(request, response, resourceAttributes) && checkIfUnmodifiedSince(request, response, resourceAttributes); }
2.1 一次请求流程
以请求 /main.css 静态资源为例,第一次请求响应头信息如下:
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Accept-Ranges: bytes ETag: W/"72259-1557127244000" Last-Modified: Mon, 06 May 2019 07:20:44 GMT Content-Type: text/css Content-Length: 72259 Date: Mon, 06 May 2019 07:20:57 GMT
第二次请求时,首先看一下请求头域关键信息:
Cache-Control:max-age=0 Connection:keep-alive Host:localhost:8080 If-Modified-Since:Mon, 06 May 2019 07:20:44 GMT If-None-Match:W/"72259-1557127244000"
服务器收到请求后就会比对 ETag,这里匹配成功,表示资源没有修改,响应如下:
HTTP/1.1 304 Not Modified Server: Apache-Coyote/1.1 ETag: W/"72259-1557127244000" Date: Mon, 06 May 2019 07:21:46 GMT
注意:在复现时,要使用文本类型,如果使用 Chrome 浏览器,记得开启缓存。
2.2 Accept-Ranges
在上文的响应中,服务器设置了一个 Accept-Ranges: bytes 头,字面理解就是可以请求资源的一部分字节,客户端发现有这个头时,就可以尝试断点续传。
解析过程就是对 HTTP 规范的实现,这里不在具体分析了,规范详细信息可查看 RFC7233#section-2.3.
3. SendFile 的处理
检查是否支持 SendFile,NIO 模式下支持此操作,也就是零拷贝,此操作会减少一次到应用内存的拷贝,直接从内核将数据写入通道。Tomcat 在文件大小大于 48KB 时会尝试使用此方式发送。
4. 小结
Tomcat 对静态资源处理的实现还是比较完善的,但还是略逊色于 Nginx 这类 Web 服务器,因为它们能直接处理静态资源,而 Tomcat 还要多做一次映射。一般的都会进行动静分离,让 Tomcat 专注处理动态请求。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。