在分享本案例前,我们先来简要总结一下HTTP协议的相关知识。
1、最初的协议版本是HTTP1.0,不支持长连接,导致每个请求都需要建立单独的TCP连接。对于小文件来说,每次请求进行TCP建连的时间占整个文件响应时间的比重较大,即该协议版本对小文件的访问效率影响较大。
2、HTTP1.1解决了长连接的问题,但支持的是串行的方式,即第1个请求响应结束后才能发起第2个请求。虽然HTTP1.1支持pipeline,即在同一个TCP连接里同时发起多个请求,但需要保证按请求的顺序依次响应,否则无法区分响应对应的是哪个请求。因此这种方式存在队头堵塞的情况,若同时发起1、2、3、4四个请求,2响应异常则会堵塞后面3、4的响应。
3、HTTP2.0支持并发请求,且不存在队头堵塞的问题。因为HTTP2.0引入了帧,支持对每个HTTP请求打标识,这样就不要求顺序响应了;而且它支持对每个请求标记优先级,可以告知服务端哪个请求应该优先响应。
HTTP/2协议包含两种实现类型:基于常规的非加密信道建立的HTTP/2的连接即h2c;基于TLS协议建立起来的HTTP/2的连接即h2。 这2种协商的方式是不同的。
HTTP1.1中引入了Upgrade机制,使得客户端和服务端之间可以借助已有的HTTP语法升级到其他协议。对于h2c的升级,正是借助Upgrade来完成的。
问题描述:
客户反馈iphone无法访问https://3qys.com.cn/1.jpg
原因分析:
1、PC端谷歌浏览器测试正常。从抓包协议看,最终是响应h2。且可以发现,源站有响应upgrade: h2c
2、iphone访问,发现确实无法打开,复现到现象。
既然PC端谷歌浏览器访问正常,而iphone却访问异常,是不是2种环境下请求存在差异呢?希望通过对比两者请求的差异找到问题突破口。
疑问:iphone发起的HTTP协议版本是啥,最终服务端返回的协议版本又是啥?
验证:使用fiddler 代理抓包分析(具体方法可参考:https://www.jianshu.com/p/28cbc08b3e94)
Client Hello:客户端会发一个自己支持的 HTTP 协议列表,希望服务端优先按顺序支持。如下所示,ALPN 扩展字段携带着 h2 和 http/1.1 协议列表。
Server Hello:服务端选择支持的协议版本,返回给客户端。如下所示,最终协商出的协议版本为h2
通过抓包信息来看,协议传输上无差异,怀疑可能和响应内容有关。由于https是加密的,抓包看不到响应结果,于是考虑尝试使用curl测试看是否能复现。
3、curl测试报错。从报错信息 “http2 error: Invalid HTTP header field was received: frame type: 1, stream: 1, name: [upgrade], value: [h2c]” 看和头部upgrade: h2c 有关。查看协议规范,响应https h2传输时,响应头不能有upgrade h2c。此时,原因定位到!!!
ps:同样的场景,谷歌浏览器访问正常应该是由于谷歌客户端会兼容这种不符合协议规范的头部。后面确认发现,有些safari浏览器版本也可兼容。
测试https 2.0 curl -voa 'https://3qys.com.cn/1.jpg' --http2
测试https 1.1 curl -voa 'https://3qys.com.cn/1.jpg' --http1.1
解决方案:
若该域名只有https的请求,建议源站去掉upgrade: h2c头即可;
若该域名同时有http和https的请求,建议源站针对https去掉upgrade: h2c头,针对http请求仍可保留该头。
客户去掉该头部后,测试iphone访问正常,问题修复。
总结:
1、通过如下图可以加深大家对该案例的理解,并理解HTTPS ALPN协商的过程原理。
2、HTTP不同协议版本的区别
1.0 | 1.1 | 2.0 | |
---|---|---|---|
长连接 | 需要使用keep-alive 参数来告知服务端建立一个长连接 | 默认支持 | 默认支持 |
HOST域 | ✘ | ✔️ | ✔️ |
多路复用 | ✘ | ✘ | ✔️ |
头部压缩 | ✘ | ✘ | 使用HAPCK算法对header数据进行压缩,使数据体积变小,传输更快 |
服务器推送 | ✘ | ✘ | ✔️ |
3、h2c升级协商机制
客户端通过Upgrade头部传递需要升级的协议版本,同时需要传递一个HTTP2-Settings的头部。
如果服务器不支持HTTP/2,则会忽略Upgrade头部,直接响应HTTP1.1
如果服务器支持HTTP2,则会响应101状态码,此时协议升级为HTTP2。