当前位置: 58彩票app下载 > 前端应用 > 正文

的协议协商机制,的一次学习实践

时间:2019-11-02 00:22来源:前端应用
谈谈 HTTP/2 的协议协商机制 2016/04/16 · 基础技术 ·HTTP/2 本文作者: 伯乐在线 -JerryQu。未经作者许可,禁止转载! 欢迎加入伯乐在线 专栏作者。 文章目录 HTTP Upgrade ALPN 扩展 小结 在过去

谈谈 HTTP/2 的协议协商机制

2016/04/16 · 基础技术 · HTTP/2

本文作者: 伯乐在线 - JerryQu 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

文章目录

  • HTTP Upgrade
  • ALPN 扩展
  • 小结

在过去的几个月里,我写了很多有关 HTTP/2 的文章,也做过好几场相关分享。我在向大家介绍 HTTP/2 的过程中,有一些问题经常会被问到。例如要部署 HTTP/2 一定要先升级到 HTTPS 么?升级到 HTTP/2 之后,不支持 HTTP/2 的浏览器还能正常访问么?本文重点介绍 HTTP/2 的协商机制,明白了服务端和客户端如何协商出最终使用的 HTTP 协议版本,这两个问题就迎刃而解了。

图片 1

HTTP Upgrade

为了更方便地部署新协议,HTTP/1.1 引入了 Upgrade 机制,它使得客户端和服务端之间可以借助已有的 HTTP 语法升级到其它协议。这个机制在 RFC7230 的「6.7 Upgrade」这一节中有详细描述。

要发起 HTTP/1.1 协议升级,客户端必须在请求头部中指定这两个字段:

Connection: Upgrade Upgrade: protocol-name[/protocol-version]

1
2
Connection: Upgrade
Upgrade: protocol-name[/protocol-version]

客户端通过 Upgrade 头部字段列出所希望升级到的协议和版本,多个协议之间用 ,(0x2C, 0x20)隔开。除了这两个字段之外,一般每种新协议还会要求客户端发送额外的新字段。

如果服务端不同意升级或者不支持 Upgrade 所列出的协议,直接忽略即可(当成 HTTP/1.1 请求,以 HTTP/1.1 响应);如果服务端统一升级,那么需要这样响应:

HTTP/1.1 101 Switching Protocols Connection: upgrade Upgrade: protocol-name[/protocol-version] [... data defined by new protocol ...]

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Connection: upgrade
Upgrade: protocol-name[/protocol-version]
 
[... data defined by new protocol ...]

可以看到,HTTP Upgrade 响应的状态码是 101,并且响应正文可以使用新协议定义的数据格式。

如果大家之前使用过 WebSocket,应该已经对 HTTP Upgrade 机制有所了解。下面是建立 WebSocket 连接的 HTTP 请求:

GET ws://example.com/ HTTP/1.1 Connection: Upgrade Upgrade: websocket Origin: Sec-WebSocket-Version: 13 Sec-WebSocket-Key: d4egt7snxxxxxx2WcaMQlA== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

1
2
3
4
5
6
7
GET ws://example.com/ HTTP/1.1
Connection: Upgrade
Upgrade: websocket
Origin: http://example.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: d4egt7snxxxxxx2WcaMQlA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

这是服务端同意升级的 HTTP 响应:

HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: websocket Sec-WebSocket-Accept: gczJQPmQ4Ixxxxxx6pZO8U7UbZs=

1
2
3
4
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: gczJQPmQ4Ixxxxxx6pZO8U7UbZs=

在这之后,客户端和服务端之间就可以使用 WebSocket 协议进行双向数据通讯,跟 HTTP/1.1 没关系了。可以看到,WebSocket 连接的建立就是典型的 HTTP Upgrade 机制。

显然,这个机制也可以用做 HTTP/1.1 到 HTTP/2 的协议升级。例如:

GET / HTTP/1.1 Host: example.com Connection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings:

1
2
3
4
5
GET / HTTP/1.1
Host: example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings:

在 HTTP Upgrade 机制中,HTTP/2 的协议名称是 h2c,代表 HTTP/2 ClearText。如果服务端不支持 HTTP/2,它会忽略 Upgrade 字段,直接返回 HTTP/1.1 响应,例如:

HTTP/1.1 200 OK Content-Length: 243 Content-Type: text/html ...

1
2
3
4
5
HTTP/1.1 200 OK
Content-Length: 243
Content-Type: text/html
 
...

如果服务端支持 HTTP/2,那就可以回应 101 状态码及对应头部,并且在响应正文中可以直接使用 HTTP/2 二进制帧:

HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: h2c [ HTTP/2 connection ... ]

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c
 
[ HTTP/2 connection ... ]

以下是通过 HTTP Upgrade 机制将 HTTP/1.1 升级到 HTTP/2 的 Wireshark 抓包(两张图可以对照来看):

图片 2

图片 3

根据 HTTP/2 协议中的描述,额外补充几点:

  • 41 号包中,客户端发起的协议升级请求中,必须通过 HTTP2-Settings 指定一个经过 Base64 编码过的 HTTP/2 SETTINGS 帧;
  • 45 号包中,服务端同意协议升级,响应正文中必须包含 HTTP/2 SETTING 帧(二进制格式,不需要 Base64 编码);
  • 62 号包中,客户端可以开始发送各种 HTTP/2 帧,但第一个帧必须是 Magic 帧(内容固定为 PRI * HTTP/2.0rnrnSMrnrn),做为协议升级的最终确认;

HTTP Upgrade 机制本身没什么问题,但很容易受网络中间环节影响。例如不能正确处理 Upgrade 头部的代理节点,很可能造成最终升级失败。之前我们统计过 WebSocket 的连通情况,发现大量明明支持 WebSocket 的浏览器却无法升级,只能使用降级方案。

前面的文章也提到了目前的移动端网络常见性能问题,以及对应的优化策略,如果把HTTP1.1 替换为 HTTP2.0,可以说是网络性能优化的一步大棋。这几天对 iOS HTTP2.0 进行了简单的调研、测试,在此做个简单的总结

ALPN 扩展

HTTP/2 协议本身并没有要求它必须基于 HTTPS(TLS)部署,但是出于以下三个原因,实际使用中,HTTP/2 和 HTTPS 几乎都是捆绑在一起:

  • HTTP 数据明文传输,数据很容易被中间节点窥视或篡改,HTTPS 可以保证数据传输的保密性、完整性和不被冒充;
  • 正因为 HTTPS 传输的数据对中间节点保密,所以它具有更好的连通性。基于 HTTPS 部署的新协议具有更高的连接成功率;
  • 当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2;

如果前面两个原因还不足以说服你,最后这个绝对有说服力,除非你的 HTTP/2 服务只打算给自己客户端用。

下面介绍在 HTTPS 中,浏览器和服务端之间怎样协商是否使用 HTTP/2。

基于 HTTPS 的协议协商非常简单,多了 TLS 之后,双方必须等到成功建立 TLS 连接之后才能发送应用数据。而要建立 TLS 连接,本来就要进行 CipherSuite 等参数的协商。引入 HTTP/2 之后,需要做的只是在原本的协商机制中把对 HTTP 协议的协商加进去。

Google 在 SPDY 协议中开发了一个名为 NPN(Next Protocol Negotiation,下一代协议协商)的 TLS 扩展。随着 SPDY 被 HTTP/2 取代,NPN 也被官方修订为 ALPN(Application Layer Protocol Negotiation,应用层协议协商)。二者的目标和实现原理基本一致,这里只介绍后者。如图:

图片 4

可以看到,客户端在建立 TLS 连接的 Client Hello 握手中,通过 ALPN 扩展列出了自己支持的各种应用层协议。其中,HTTP/2 协议名称是 h2

图片 5

如果服务端支持 HTTP/2,在 Server Hello 中指定 ALPN 的结果为 h2 就可以了;如果服务端不支持 HTTP/2,从客户端的 ALPN 列表中选一个自己支持的即可。

并不是所有 HTTP/2 客户端都支持 ALPN,理论上建立 TLS 连接后,依然可以再通过 HTTP Upgrade 进行协议升级,只是这样会额外引入一次往返。

本文的大概思路是介绍 HTTP1.1 的弊端、HTTP2.0 的优势、HTTP2.0 的协商机制、iOS 客户端如何接入 HTTP2.0,以及如何对其进行调试。主要还是加深记忆、方便后期查阅,文末的资料相比本文或许是更有价值的。

小结

看到这里,相信你一定可以很好地回答本文开头提出的问题。

HTTP/2 需要基于 HTTPS 部署是当前主流浏览器的要求。如果你的 HTTP/2 服务要支持浏览器访问,那就必须基于 HTTPS 部署;如果只给自己客户端用,可以不部署 HTTPS(这个页面列举了很多支持 h2c 的 HTTP/2 服务端、客户端实现)。

支持 HTTP/2 的 Web Server 基本都支持 HTTP/1.1。这样,即使浏览器不支持 HTTP/2,双方也可以协商出可用的 HTTP 版本,没有兼容性问题。如下表:

浏览器 服务器 协商结果
不支持 HTTP/2 不支持 HTTP/2 不协商,使用 HTTP/1.1
不支持 HTTP/2 支持 HTTP/2 不协商,使用 HTTP/1.1
支持 HTTP/2 不支持 HTTP/2 协商,使用 HTTP/1.1
支持 HTTP/2 支持 HTTP/2 协商,使用 HTTP/2

当然,本文讨论的是通用情况。对于自己实现的客户端和服务端,如果打算使用 HTTP/2 ClearText,由于 HTTP Upgrade 协商会增加一次往返,可以要求双方必须支持 HTTP/2,直接发送 HTTP/2 数据,不走协商。

打赏支持我写出更多好文章,谢谢!

打赏作者

分享之前我还是要推荐下我自己建的iOS开发学习群:680565220,群里都是学ios开发的,如果你正在学习ios ,小编欢迎你加入,今天分享的这个案例已经上传到群文件,大家都是软件开发党,不定期分享干货(只有iOS软件开发相关的),包括我自己整理的一份2017最新的iOS进阶资料和高级开发教程,欢迎进阶中和进想深入iOS的小伙伴。

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

图片 6 图片 7

1 赞 1 收藏 评论

HTTP 1.1

关于作者:JerryQu

图片 8

专注 Web 开发,关注 Web 性能优化与安全。 个人主页 · 我的文章 · 2 ·   

图片 9

虽然 HTTP1.1 默认是开启 Keep-Alive 长连接的,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点,但是依然存在 head of line blocking,如果出现一个较差的网络请求,会影响后续的网络请求。为什么呢?如果你发出1、2、3 三个网络请求,那么 Response 的顺序 2、3 要在第一个网络请求之后,以此类推

针对同一域名,在请求较多的情况下,HTTP1.1 会开辟多个连接,据说浏览器一般是6-8 个,较多连接也会导致延迟增大,资源消耗等问题

HTTP1.1 不安全,可能存在被篡改、被窃听、被伪装等问题。当然,前阵子 Apple 推广 HTTPS 的时候,相信很多人已经接入 HTTPS

HTTP 的头部没有压缩,header 的大小也是传输的负担,带来更多的流量消耗和传输延迟。并且很多 header 是相同的,重复传输是没有必要的。

服务端无法主动推送资源到客户端

HTTP1.1的格式是文本格式,基于文本做一些扩展、优化相对比较困难,但是文本格式易于阅读和调试,但HTTPS之后,也变成二进制格式了,这个优势也不复存在

HTTP2.0

在 HTTP2.0中,上面的问题几乎都不存在了。HTTP2.0 的设计来源于 Google 的 SPDY 协议,如果对 SPDY 协议不了解的话,也可以先对 SPDY 进行了解,不过这不影响继续阅读本文

HTTP 2.0 使用新的二进制格式:基本的协议单位是帧,每个帧都有不同的类型和用途,规范中定义了10种不同的帧。例如,报头和数据帧组成了基本的HTTP 请求和响应;其他帧例如 设置,窗口更新(WINDOW_UPDATE), 和推送承诺(PUSH_PROMISE)是用来实现HTTP/2的其他功能。那些请求和响应的帧数据通过流来进行数据交换。新的二进制格式是流量控制、优先级、server push等功能的基础。

流:一个Stream是包含一条或多条信息、ID和优先级的双向通道

消息:消息由帧组成

帧:帧有不同的类型,并且是混合的。他们通过stream id被重新组装进消息中

图片 10

多路复用:也就是连接共享,刚才说到 HTTP1.1的 head of line blocking,那么在多路复用的情况下,blocking 已经不存在了。每个连接中 可以包含多个流,而每个流中交错包含着来自两端的帧。也就是说同一个连接中是来自不同流的数据包混合在一起,如下图所示,每一块代表帧,而相同颜色块来自同一个流,每个流都有自己的 ID,在接收端会根据 ID 进行重装组合,就是通过这样一种方式来实现多路复用。

图片 11

单一连接:刚才也说到 1.1 在请求多的时候,会开启6-8个连接,而 HTTP2 只会开启一个连接,这样就减少握手带来的延迟。

头部压缩:HTTP2.0 通过 HPACK 格式来压缩头部,使用了哈夫曼编码压缩、索引表来对头部大小做优化。索引表是把字符串和数字之间做一个匹配,比如method: GET对应索引表中的2,那么如果之前发送过这个值是,就会缓存起来,之后使用时发现之前发送过该Header字段,并且值相同,就会沿用之前的索引来指代那个Header值。具体实验数据可以参考这里:HTTP/2 头部压缩技术介绍

图片 12

Server Push:就是服务端可以主动推送一些东西给客户端,也被称为缓存推送。推送的资源可以备客户端日后之需,需要的时候直接拿出来用,提升了速率。具体的实验可以参考这里:iOS HTTP/2 Server Push 探索

图片 13

除了上面讲到的特性,HTTP2.0 还有流量控制、流优先级和依赖性等功能。更多细节可以参考:Hypertext Transfer Protocol Version 2

iOS 客户端接入HTTP 2.0

iOS 如何接入 HTTP 2.0呢?其实很简单:

保证服务端支持 HTTP2.0,并且留意下 NPN 或 ALPN

客户端系统版本 iOS 9 +

使用 NSURLSession 代替 NSURLConnection

客户端是使用 h2c 还是 h2,它们可以说是 HTTP2.0的两个版本,h2 是使用 TLS 的HTTP2.0协议,h2c是运行在明文 TCP 协议上的 HTTP2.0协议。浏览器目前只支持h2,也就是说必须基于HTTPS部署,但是客户端可以不部署HTTPS,因为我司早已部署HTTPS,所以我这里的实践都是基于h2的

HTTP 2.0的协商机制

上面说了一堆名次,什么NPN、ALPN呀,还有h2、h2c之类的,有点懵逼。NPN(Next Protocol Negotiation)是一个 TLS 扩展,由 Google 在开发 SPDY 协议时提出。随着 SPDY 被 HTTP/2 取代,NPN 也被修订为 ALPN(Application Layer Protocol Negotiation,应用层协议协商)。二者目标一致,但实现细节不一样,相互不兼容。以下是它们主要差别:

NPN 是服务端发送所支持的 HTTP 协议列表,由客户端选择;而 ALPN 是客户端发送所支持的 HTTP 协议列表,由服务端选择;

NPN 的协商结果是在 Change Cipher Spec 之后加密发送给服务端;而 ALPN 的协商结果是通过 Server Hello 明文发给客户端

同时,目前很多地方开始停止对NPN的支持,仅支持 ALPN,所以公司使用的话,最佳是直接使用 ALPN。

下面就直接来看看 ALPN 的协商过程是怎样的,ALPN 作为 TLS 的一个扩展,其过程可以通过 WireShark 查看 TLS握手过程来查看

图片 14

下面通过 WireShark 来进行调试,接入真机,然后终端输入

rvictl -s 设备 UDID来创建一个映射到 iPhone 的虚拟网卡,UUID 可以在 iTunes 中获取到,运行命令后会看到成功创建 rvi0 虚拟网卡的,双击 rvi0 开始调试。

图片 15

进入之后,在手机上访问页面会有源源不断的请求显示在 WireShark 的界面上,数据太多而不利于我们针对性调试,你可以过滤下域名,只关注你想测试的 ip 地址,比如: ip.addr==111.89.211.191 ,当然你的 ip 要支持 HTTP2.0才会有预想的效果哦

图片 16

下面,就开始通过查看 TLS 握手的过程分析HTTP2.0 的协商过程,刚才也说道 ALPN 协商结果是在 Client hello 和 Server hello 中显示的,那就先来看一下Client hello

图片 17

可以看到客户端在 Client hello 中列出了自己支持的各种应用层协议,比如 spdy3、h2。那么接着看 Server hello 是如何回复的

图片 18

服务端会根据 client hello 中的协议列表,发过去自己支持的网络协议,假如服务端支持 h2,则直接返回h2,协商成功,如果不支持 h2,则返回一个其他支持的协议,比如HTTP1.1、spdy3

这个是h2的协商过程,对于刚才提到的 h2c 的协商过程,与此不同,h2c 利用的是HTTP Upgrade 机制,客户端会发送一个 http 1.1的请求到服务端,这个请求中包含了 http2的升级字段,例如:

GET /default.htmHTTP/1.1Host: server.example.comConnection: Upgrade, HTTP2-Settings Upgrade: h2c HTTP2-Settings:

服务端收到这个请求后,如果支持 Upgrade 中 列举的协议,这里是 h2c,就会返回支持的响应:

HTTP/1.1101Switching Protocols Connection:Upgrade Upgrade:h2c [ HTTP/2connection ...

当然,不支持的话,服务器会返回一个不包含 Upgrade 的报头字段的响应。

我的客户端支持了吗?

一切准备就绪之后,也是时候对结果进行验证了,除了刚才提到的 WireShark 之外,你还可以使用下面的几个工具来对 HTTP 2.0 进行测试

Chrome 上的一个插件,HTTP/2 and SPDY indicator会在你访问 http2.0 的网页的时候,以小闪电的形式进行指示

图片 19

点击小闪电,会进入一个页面,列举了当前浏览器访问的全部 http2.0的请求,所以,你可以把你想要测试的客户端接口在浏览器访问,然后在这个页面验证下是否支持 http2.0

图片 20

charles:这个大家应该都用过,4.0 以上的新版本对 HTTP2.0做了支持,为了方便,你也可以在 charles 上进行调试,但是我发现好像存在 http2.0的一些 bug,目前还没搞清楚什么原因

使用 nghttp2 来调试,这是一个 C 语言实现的 HTTP2.0的库,具体使用方法可以参考:使用 nghttp2 调试 HTTP/2 流量

再者简单粗暴,直接在 iOS 代码中打印,_CFURLResponse 中包含了 httpversion,获取方法就是基于 CFNetwork 相关的 API 来做,这里直接丢出关键代码,完整代码可以参考getHTTPVersion

#import"NSURLResponse+Help.h"#import@implementationNSURLResponsetypedefCFHTTPMessageRef(*MYURLResponseGetHTTPResponse)(CFURLRefresponse);

  • (NSString*)getHTTPVersion {NSURLResponse*response =self;NSString*version;NSString*funName =@"CFURLResponseGetHTTPResponse"; MYURLResponseGetHTTPResponse originURLResponseGetHTTPResponse = dlsym(RTLD_DEFAULT, [funName UTF8String]); SEL theSelector =NSSelectorFromString(@"_CFURLResponse");if([response respondsToSelector:theSelector] &&NULL!= originURLResponseGetHTTPResponse) {CFTypeRefcfResponse =CFBridgingRetain([response performSelector:theSelector]);if(NULL!= cfResponse) {CFHTTPMessageRefmessage = originURLResponseGetHTTPResponse(cfResponse);CFStringRefcfVersion =CFHTTPMessageCopyVersion;if(NULL!= cfVersion) { version = (__bridgeNSString*)cfVersion;CFRelease(cfVersion); }CFRelease(cfResponse); } }if(nil== version ||0== version.length) { version =@"获取失败"; }returnversion;

编辑:前端应用 本文来源:的协议协商机制,的一次学习实践

关键词: