HTTP]402- 深入了解 http2.0 协议,读到这里就够了!
本文字数:3825字
预计阅读时间:20分钟
导读
http2.0是一种安全高效的下一代http传输协议。安全是因为http2.0建立在https协议的基础上,高效是因为它是通过二进制分帧来进行数据传输。正因为这些特性,http2.0协议也在被越来越多的网站支持。据统计,截止至2018年8月,已经有27.9%的网站支持http2.0。
本文将从概述、原理、实战及检测等方面来详细介绍http2.0,希望能够加深你的理解。
什么是http2.0协议?
在http2.0官网①的描述是:
http/2 is a replacement for how http is expressed “on the wire.” It is not a ground-up rewrite of the protocol; http methods, status codes and semantics are the same, and it should be possible to use the same APIs as http/1.x (possibly with some small additions) to represent the protocol.
The focus of the protocol is on performance; specifically, end-user perceived latency, network and server resource usage. One major goal is to allow the use of a single connection from browsers to a Web site.
The basis of the work was SPDY, but http/2 has evolved to take the community’s input into account, incorporating several improvements in the process.
中文总结一下就是:
●对1.x协议语意的完全兼容
2.0协议是在1.x基础上的升级而不是重写,1.x协议的方法,状态及api在2.0协议里是一样的。
●性能的大幅提升
2.0协议重点是对终端用户的感知延迟、网络及服务器资源的使用等性能的优化。
http2.0优化内容
01
二进制分帧(Binary Format)- http2.0的基石
http2.0之所以能够突破http1.X标准的性能限制,改进传输性能,实现低延迟和高吞吐量,就是因为其新增了二进制分帧层。
帧(frame)包含部分:类型Type, 长度Length, 标记Flags, 流标识Stream和frame payload有效载荷。
消息(message):一个完整的请求或者响应,比如请求、响应等,由一个或多个 Frame 组成。
流是连接中的一个虚拟信道,可以承载双向消息传输。每个流有唯一整数标识符。为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务器端发起的流具有偶数ID。
流标识是描述二进制frame的格式,使得每个frame能够基于http2发送,与流标识联系的是一个流,每个流是一个逻辑联系,一个独立的双向的frame存在于客户端和服务器端之间的http2连接中。一个http2连接上可包含多个并发打开的流,这个并发流的数量能够由客户端设置。
在二进制分帧层上,http2.0会将所有传输信息分割为更小的消息和帧,并对它们采用二进制格式的编码将其封装,新增的二进制分帧层同时也能够保证http的各种动词,方法,首部都不受影响,兼容上一代http标准。其中,http1.X中的首部信息header封装到Headers帧中,而request body将被封装到Data帧中。
02
多路复用 (Multiplexing) / 连接共享
在http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量的限制,超过限制数目的请求会被阻塞。这也是为何一些站点会有多个静态资源 CDN 域名的原因之一。
而http2.0中的多路复用优化了这一性能。多路复用允许同时通过单一的http/2 连接发起多重的请求-响应消息。有了新的分帧机制后,http/2 不再依赖多个TCP连接去实现多流并行了。每个数据流都拆分成很多互不依赖的帧,而这些帧可以交错(乱序发送),还可以分优先级,最后再在另一端把它们重新组合起来。
http 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。http2连接可以承载数十或数百个流的复用,多路复用意味着来自很多流的数据包能够混合在一起通过同样连接传输。当到达终点时,再根据不同帧首部的流标识符重新连接将不同的数据流进行组装。
上图展示了一个连接上的多个传输数据流:客户端向服务端传输数据帧stream5,同时服务端向客户端乱序发送stream1和stream3。这次连接上有三个响应请求乱序并行交换。
上图就是http1.X和http2.0在传输数据时的区别。以货物运输为例再现http1.1与http2.0的场景:
http1.1过程:货轮1从A地到B地去取货物,取到货物后,从B地返回,然后货轮2在A返回并卸下货物后才开始再从A地出发取货返回,如此有序往返。
http2.0过程:货轮1、2、3、4、5从A地无序全部出发,取货后返回,然后根据货轮号牌卸载对应货物。
显然,第二种方式运输货物多,河道的利用率高。
03
头部压缩(Header Compression)
http1.x的头带有大量信息,而且每次都要重复发送。http/2使用encoder来减少需要传输的header大小,通讯双方各自缓存一份头部字段表,既避免了重复header的传输,又减小了需要传输的大小。
对于相同的数据,不再通过每次请求和响应发送,通信期间几乎不会改变通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。
事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么,首部开销就是零字节,此时所有首部都自动使用之前请求发送的首部。
如果首部发生了变化,则只需将变化的部分加入到header帧中,改变的部分会加入到头部字段表中,首部表在 http 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新。
需要注意的是,http 2.0关注的是首部压缩,而我们常用的gzip等是报文内容(body)的压缩,二者不仅不冲突,且能够一起达到更好的压缩效果。
http/2使用的是专门为首部压缩而设计的HPACK②算法。
从上图可以看到http1.X不支持首部压缩,而http2.0的压缩算法效果最好,发送和接受的数据量都是最少的。
04
压缩原理
用header字段表里的索引代替实际的header。
http/2的HPACK算法使用一份索引表来定义常用的http Header,把常用的 http Header 存放在表里,请求的时候便只需要发送在表里的索引位置即可。
例如 :method=GET 使用索引值 2 表示,:path=/index.html 使用索引值 5 表示,如下图:
完整的列表参考:HPACK Static Table③。
只要给服务端发送一个 Frame,该 Frame 的 Payload 部分存储 0x8285,Frame 的 Type 设置为 Header 类型,便可表示这个 Frame 属于 http Header,请求的内容是:
1GET /index.html
为什么是 0x8285,而不是 0x0205?这是因为高位设置为 1 表示这个字节是一个完全索引值(key 和 value 都在索引中)。
类似的,通过高位的标志位可以区分出这个字节是属于一个完全索引值,还是仅索引了 key,还是 key和value 都没有索引(参见:HTTP/2首部压缩的OkHttp3实现④)。
因为索引表的大小的是有限的,它仅保存了一些常用的 http Header,同时每次请求还可以在表的末尾动态追加新的 http Header 缓存,动态部分称之为 Dynamic Table。Static Table 和 Dynamic Table 在一起组合成了索引表:
HPACK 不仅仅通过索引键值对来降低数据量,同时还会将字符串进行霍夫曼编码来压缩字符串大小。
以常用的 User-Agent 为例,它在静态表中的索引值是 58,它的值是不存在表中的,因为它的值是多变的。第一次请求的时候它的 key 用 58 表示,表示这是一个 User-Agent ,它的值部分会进行霍夫曼编码(如果编码后的字符串变更长了,则不采用霍夫曼编码)。
服务端收到请求后,会将这个 User-Agent 添加到 Dynamic Table 缓存起来,分配一个新的索引值。客户端下一次请求时,假设上次请求User-Agent的在表中的索引位置是 62, 此时只需要发送 0xBE(同样的,高位置 1),便可以代表:User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36。
其过程如下图所示:
最终,相同的 Header 只需要发送索引值,新的 Header 会重新加入 Dynamic Table。
05
请求优先级(Request Priorities)
把http消息分为很多独立帧之后,就可以通过优化这些帧的交错和传输顺序进一步优化性能。每个流都可以带有一个31比特的优先值:0 表示最高优先级;2的31次方-1 表示最低优先级。
服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。高优先级的流都应该优先发送,但又不会绝对的。绝对地准守,可能又会引入首队阻塞的问题:高优先级的请求慢导致阻塞其他资源交付。
分配处理资源和客户端与服务器间的带宽,不同优先级的混合也是必须的。客户端会指定哪个流是最重要的,有一些依赖参数,这样一个流可以依赖另外一个流。优先级别可以在运行时动态改变,当用户滚动页面时,可以告诉浏览器哪个图像是最重要的,你也可以在一组流中进行优先筛选,能够突然抓住重点流。
●优先级最高:主要的html
●优先级高:CSS文件
●优先级中:js文件
●优先级低:图片
06
服务端推送(Server Push)
服务器可以对一个客户端请求发送多个响应,服务器向客户端推送资源无需客户端明确地请求。并且,服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。
正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。Server Push 让 http1.x 时代使用内嵌资源的优化手段变得没有意义;如果一个请求是由你的主页发起的,服务器很可能会响应主页内容、logo 以及样式表,因为它知道客户端会用到这些东西,这相当于在一个 HTML 文档内集合了所有的资源。
不过与之相比,服务器推送还有一个很大的优势:可以缓存!也让在遵循同源的情况下,不同页面之间可以共享缓存资源成为可能。
注意两点:
1、推送遵循同源策略;
2、这种服务端的推送是基于客户端的请求响应来确定的。
当服务端需要主动推送某个资源时,便会发送一个 Frame Type 为 PUSH_PROMISE 的 Frame,里面带了 PUSH 需要新建的 Stream ID。意思是告诉客户端:接下来我要用这个 ID 向你发送东西,客户端准备好接着。客户端解析 Frame 时,发现它是一个 PUSH_PROMISE 类型,便会准备接收服务端要推送的流。
http2.0性能瓶颈
启用http2.0后会给性能带来很大的提升,但同时也会带来新的性能瓶颈。因为现在所有的压力集中在底层一个TCP连接之上,TCP很可能就是下一个性能瓶颈,比如TCP分组的队首阻塞问题,单个TCP packet丢失导致整个连接阻塞,无法逃避,此时所有消息都会受到影响。未来,服务器端针对http 2.0下的TCP配置优化至关重要。
01
如何升级http2.0协议
nginx服务器升级http2.0协议需要满足如下条件:
1、nginx版本高于1.9.5;
2、--with-http_ssl_module 跟 --with-http_v2_module
--with-http_ssl_module模块是因为http2.0协议是一种https协议。
02
查看你的nginx配置
nginx -V
这个是已经添加了对应模块。没有这两个模块的需要手动编译安装。
03
找到nginx文件目录
04
编译安装nginx文件
1./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module
然后执行如下命令,进行编译安装。
1make
2make install
05
更改nginx配置
安装结束后将nginx.config文件中443端口添加http2;
06
启动nginx
最后一步,重启nginx nginx restart(注意不要直接 nginx -s reload )。这时候你的站点就升级为了http2.0协议了。
检测
升级完成后,怎么确定自己的站点是http2.0协议呢?一般有如下几种方法:
●chrome devtool
打开chrome调试工具,在network勾选protocol项,h2代表的是http2.0协议,可以看到笔者的网站已经都升级好了;
●网站
SSL lab⑤一个SSL服务器检测的网站,对网站进行安全评级,并将检测结果自动生成一个详细的评价报告;
●插件
http/2 and SPDY indicator 这是一款检测http2.0和SPDY协议(Google开发的基于TCP的会话层协议)的插件。
参考资料:
[1].https://http2.github.io/
[2].http://http2.github.io/http2-spec/compression.html
[3].http://http2.github.io/http2-spec/compression.html#rfc.section.A
[4].https://neyoufan.github.io/2017/01/06/android/OkHttp3中的HTTP2首部压缩/
[5].https://www.ssllabs.com/ssltest/analyze.html
回复“加群”与大佬们一起交流学习~
推荐阅读
-
HTTP]402- 深入了解 http2.0 协议,读到这里就够了!
-
HTTP]402- 深入了解 http2.0 协议,读到这里就够了!
-
go语言Socket编程-Socket编程 什么是Socket Socket,英文含义是插座、插孔,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。 套接字通讯原理示意 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 网络应用程序设计模式 C/S模式 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。 B/S模式 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。 优缺点 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。 C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。 B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。 简单的C/S模型通信 Server端:Listen函数 func Listen(network, address string) (Listener, error) network:选用的协议:TCP、UDP, 如:“tcp”或 “udp” address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000” Listener 接口: type Listener interface { Accept (Conn, error) Close error Addr Addr } Conn 接口: type Conn interface { Read(b byte) (n int, err error) Write(b byte) (n int, err error) Close error LocalAddr Addr RemoteAddr Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 参看 [<u>https://studygolang.com/pkgdoc</u>](https://studygolang.com/pkgdoc) 中文帮助文档中的demo: 示例代码:TCP服务器.go package main import ( "net" "fmt" ) func main { // 创建监听 listener, err:= net.Listen("tcp", ":8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close // 主协程结束时,关闭listener fmt.Println("服务器等待客户端建立连接...") // 等待客户端连接请求 conn, err := listener.Accept if err != nil { fmt.Println("accept err:", err) return } defer conn.Close // 使用结束,断开与客户端链接 fmt.Println("客户端与服务器连接建立成功...") // 接收客户端数据 buf := make(byte, 1024) // 创建1024大小的缓冲区,用于read n, err := conn.Read(buf) if err != nil { fmt.Println("read err:", err) return } fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。 }