基础概念
什么是ETag
ETag 是 Entity Tag 的缩写,中文译过来就是实体标签的意思。在HTTP1.1协议中其实就是请求HEAD中的一个属性而已。
1 | 200 OK |
1 | <html> |
请注意第8行的 ETag: “3f80f-1b6-3e1cb03b”配置,那么为什么要用ETag呢?
ETag是HTTP1.1中才加入的一个属性,用来帮助服务器控制Web端的缓存验证。它的原理是这样的,当浏览器请求服务器的某项资源(A)时, 服务器根据A算出一个哈希值(3f80f-1b6-3e1cb03b)并通过 ETag 返回给浏览器,浏览器把”3f80f-1b6-3e1cb03b” 和 A 同时缓存在本地,当下次再次向服务器请求A时,会通过类似 If-None-Match: “3f80f-1b6-3e1cb03b” 的请求头把ETag发送给服务器,服务器再次计算A的哈希值并和浏览器返回的值做比较,如果发现A发生了变化就把A返回给浏览器(200),如果发现A没有变化就给浏览器返回一个304未修改。这样通过控制浏览器端的缓存,可以节省服务器的带宽,因为服务器不需要每次都把全量数据返回给客户端。
注:HTTP中并没有指定如何生成ETag,哈希是比较理想的选择。
通常情况下,ETag更类似于资源指纹(fingerprints),如果资源发生变化了就会生成一个新的指纹,这样可以快速的比较资源的变化。在服务器端实现中,很多情况下并不会用哈希来计算ETag,这会严重浪费服务器端资源,很多网站默认是禁用ETag的。有些情况下,可以把ETag退化,比如通过资源的版本或者修改时间来生成ETag。
如果通过资源修改时间来生成ETag,那么效果和HTTP协议里面的另外一个控制属性(Last-Modified)就雷同了,使用 Last-Modified 的问题在于它的精度在秒(s)的级别,比较适合不太敏感的静态资源。
后记
在友盟的时候做过一个项目 - 在线参数,这个功能在很多游戏类App中非常流行,也有很多App会通过它来控制内置广告的开关,曾经一度流量过大以至于阿里那边发邮件过来要我们关闭这个服务区(这是个免费的服务)。为了继续造福群众,我们做了一些优化流量的措施:
减少SDK请求的次数
设计了一种服务器端缓存验证机制
前者是因为很多开发者把调用方法写在了Activity里面,这样每次Activity重建的时候都会导致给服务器发请求。但是对于SDK来说无法判断请求是开发者故意发起的还是由于Activity重建导致的,所以解决这个问题需要开发者配合 - 这明显是不可能的。考虑到在线参数并不会经常性的发生变化,SDK限制了请求间隔为10分钟 - 考虑大部分App的声明周期是3 - 10 分钟(对于读书、视频等媒体类应用例外),这等于App每次启动只能请求一次服务器,这种限制在开发者DEBUG阶段会造成奇怪的问题 - 明明更新了在线参数,本地却不起效果。
第二个措施其实是进一步优化,在最早设计在线参数的时候已经添加了服务器端的验证机制 - 每次请求数据的时候带上服务器返回的最后修改时间,如果服务器端发现数据没有变化,那么就不再返回数据。但是这个协议是在HTTP-Body里面通过私有协议实现的,每次请求服务器的时候依然会发送大量数据(<1K),所以优化的措施是每次先发送一个最简的协议给服务器如果数据有变化再发送一个标准的协议给服务器取回数据。
当时我对HTTP协议并不熟悉,这些设计都是通过私有协议实现的,其实完全可以通过HTTP来实现,这样可以较好的分离控制逻辑和业务数据
GZIP压缩
前言
在基于 HTTP 协议的网络传输中 GZip 经常被使用,Nginx 中也可以使用半行代码开启 GZip。GZip 压缩的原理是什么呢?本篇文章是我在网上阅读了一些文档后做的简单总结。
从 RFC 1952 看起
RFC 1952 是 GZIP file format specification version 4.3。该规范主要定义了 GZip 压缩的在数据格式方面的规范,以方便不同的操作系统、CPU、文件系统等之间进行文件传输交换。下面挑有意思的几个点说,感兴趣的可以阅读 RFC 1952 的原文。
GZIP 的文件格式在设计上其实是可以允许一个文件里有多个压缩数据集(compressed data sets)—— GZIP 压缩后的片段拼接而成的。但就我们大多数应用场景来说,基本上都是一个文件一个压缩数据集,如果是多个文件一起打包的话,也往往是将多个包合并成一个 tar 文件。
每个压缩数据集都是下面的结构:
| ID1 | ID2 | CM | FLG | MTIME(4字节) | XFL | OS | —-> more
| 与 | 之间是 1 byte,都是大端字节(Big Edian)
其中 ID1 和 ID2 分别是 0x1f 和 0x8b,用来标识文件格式是 gzip
CM 标识 加密算法,目前 0-7是保留字,8 指的是 deflate 算法
FLG 从低地址到高地址分别是 FTEXT、FHCRC、FEXTRA、FNAME、FCOMMENT、reserved、 reserved、reserved,这里每个 bit 被设置了之后有什么意义感兴趣的话可以详细参考 RFC 1952。比较有意思的是 FEXTRA,如果它被设置了表示存在额外的拓展字段。拓展字段的结构如下:
| SI1 | SI2 | LEN | … LEN bytes of subfield data … |
SI1、SI2 是对子域的 ID,由 ASCII 码组成。如果你需要使用的话,可以向他的维护者 Jean-Loup Gailly gzip@prep.ai.mit.edu 发邮件申请。目前 Apollo file 就有自己的专属 ID
MTIME 指的是源文件最近一次修改时间,存的是 Unix 时间戳
XFL 是给压缩算法传的一些参数,用来标识如何解压。defalte 算法中 2 表示使用压缩率最高的算法,4 表示使用压缩速度最快的算法
OS 标识压缩程序运行的文件系统,以处理 EOF 等的问题
more 后面是根据 FLG 的开启情况决定的,可能会有 循环冗余校验码、源文件长度、附加信息等多种其他信息
压缩核心之 Deflate
GZIP 的核心是 Deflate,在 RFC 1951 中被标准化,并且在当时作为 LZW 的替代品有了非常广泛的使用。
Deflate 是一个同时使用 LZ77 与 Huffman Coding 的算法,这里简单介绍下这两种算法的大致思路:
LZ77
LZ77 的核心思路是如果一个串中有两个重复的串,那么只需要知道第一个串的内容和后面串相对于第一个串起始位置的距离 + 串的长度。
比如: ABCDEFGABCDEFH → ABCDEFG(7,6)H。7 指的是往前第 7 个数开始,6 指的是重复串的长度,ABCDEFG(7,6)H 完全可以表示前面的串,并且是没有二义性的。
LZ77 用 滑动窗口(sliding-window compression)来实现这个算法。具体思路是扫描头从串的头部开始扫描串,在扫描头的前面有一个长度为 N 的滑动窗口。如果发现扫描头处的串和窗口里的 最长匹配串 是相同的,则用(两个串之间的距离,串的长度)来代替后一个重复的串,同时还需要添加一个表示是真实串还是替换后的“串”的字节在前面以方便解压(此串需要在 真实串和替换“串” 之前都有存在)。
实际过程中滑动窗口的大小是固定的,匹配的串也有最小长度限制,以方便 标识+两个串之间的距离+串的长度 所占用的字节是固定的 以及 不要约压缩体积越大。更加详细的实现可以参考:Standford Edu. lz77 algorithm、 LZ77 Compression Algorithm、 LZ77压缩算法编码原理详解(结合图片和简单代码)
这里通过这个压缩机制也就能比较容易的解释为啥 CSS BEM 写法 GZIP 压缩之后可以忽略长度以及 JPEG 图片 GZIP 之后可能会变大 的情况了
解压:GZIP 的压缩因为要在窗口里寻找重复串相对来说效率是比较低的(LZ77 还是通过 Hash 等系列方法提高了很多),那解压又是怎么个情况呢?观察压缩后的整个串,每个小串前都有一个标识要标记是原始串还是替换“串”,通过这个标识就能以 O(1)的复杂度直接读完并且替换完替换“串”,整体上效率是非常可观的。
Huffman Coding
Huffman Coding 是大学课本中一般都会提到的算法。核心思路是通过构造 Huffman Tree 的方式给字符重新编码(核心是避免一个叶子的路径是另外一个叶子路径的前缀),以保证出现频路越高的字符占用的字节越少。关于 Huffman Tree 的构造这里不再细说,不太清楚的可以参考:Huffman Coding。
解压:Huffman Coding 之后需要维护一张 Huffman Map 表,来记录重新编码后的字符串,根据这张表,还原原始串也是非常高效的。
Deflate 综合使用了 LZ77 和 Huffman Coding 来压缩文件,相对而言又提升了很多。详细可以参考 gzip原理与实现
网站中的使用
在 RFC 2016 中 GZIP 已经成为了规定的三种标准HTTP压缩格式之一。目前绝大多数的网站都在使用 GZIP 传输 HTML、CSS、JavaScript 等资源文件。
Nginx 开启
Nginx 的 ngx_http_gzip_module 也提供了开启 GZIP 压缩的方式,有下面的一些常用配置:
1 | # 开启 |
相关探测
Nginx 上开启 GZIP 之后,理论上会按照 GZIP 配置打开压缩。那如何检测是否开启成功了呢?
打开浏览器,访问你的网站,看 Chrome 的 Network,如果 Size 上有两个不一样大小的体积(如:222KB 和 613KB),则代表 GZIP 已经成功开启。
那浏览器又是如何和服务器配合的呢?
浏览器在请求资源的时候再 header 里面带上 accept-encoding: gzip 的参数。Nginx 在接收到 Header 之后,发现如果有这个配置,则发送 GZIP 之后的文件(返回的 header 里也包含相关的说明),如果没有则发送源文件。浏览器根据 response header 来处理要不要针对返回的文件进行解压缩然后展示。