HTTP发展史在总结http2之前,我们先来回顾一下http的发展史。下面三张图来自JerryQuHTTP/0.9(1991)HTTP/1.0(1996)HTTP/1.1(1999)HTTP通信过程众所周知,http是基于tcp的应用层协议,也就是tcp连接后建立后,在tcp数据上建立链接。先建立TCP连接,三次握手,C--(SYN{k})-->S,S--(ACK{k+1}&SYN{j})-->C,C--ACK{j+1}-->S客户端发送ACK后,会发送HTTP请求。服务器收到ACK,确认TCP连接建立,然后收到HTTP请求,解析并返回结果给客户端。客户端收到HTTP请求结果。在HTTP/0.9和HTTP/1.0中,在第三步之后,服务端会关闭连接,也就是四波TCP,但是在HTTP/1.1之后,客户端在发送HTTP请求时可以在header中带上Connection:Keep-Alive是告诉服务器保持连接,不要关闭TCP。当Connection:Close时,服务器将关闭连接。HTTP2的通信过程无非就是这个过程,只是通过TCP传输的数据会有所不同,并且对客户端和服务端的行为有了新的规则。介绍了Connection、Stream、Message、Frame这四个概念,从下图可以看出它们之间的关系。Connection:其实就是一个TCP连接Stream:已建立连接上的双向字节流Message:请求或响应,由一个或多个帧组成Frame:Message中的二进制帧,HTTP/2通信的最小单位,新HTTP/2的特性后面会详细解释:BinaryframinglayerMultiplexingSingleconnectionperoriginStreamprioritizationHeaderCompressionFlowcontrol)ServerPush这些新特性的出现主要是为了解决之前的问题。我们对比一下之前的HTTP/1.1,看看解决了哪些问题。BinaryframinglayerBinaryframing就是将http数据按照指定的格式进行封装,类似于IP和TCP数据包,为了方便理解简单画一个承载HTTP2数据的以太网帧结构。通过wireshark抓包可以看到http2的结构。Length:无符号自然数,用24位表示,只表示帧净荷占用的字节数,不包括帧头占用的9字节。默认大小范围为0~16,384(2^14)。一旦超过默认最大值2^14(16384),发送方将不再允许发送,除非收到接收方定义的SETTINGS_MAX_FRAME_SIZE(一般这个取值范围为2^14~2^24)值通知。Type:8位表示,定义了帧载荷的具体格式和帧的语义。HTTP/2规范定义了10种帧类型,其中不包括实验类型帧和扩展类型帧。Flags:8位表示,服务于特定的Frame类型,默认值为0x0。有一个小技巧需要注意。一般来说,8位可以容纳8个不同的标志位。例如PADDED的值为0x8,二进制表示为00001000;END_HEADERS的值为0x4,二进制表示为00000100;为00000001。一个字节可以同时传送三种标志位,二进制表示为00001101,即0x13。因此,在下面的帧结构中,标志位一般用8位来表示。如果某位不确定,用问号?相反,表示可以在此处设置标志位。R:HTTP/2上下文中保留位,固定值为0X0StreamIdentifier:Unsigned31bits表示无符号自然数。值0x0表示该帧仅适用于连接,不属于单独的流。HTTP2frame中的类型如下:如果想了解每种类型的详细数据结构,可以参考我的另一篇文章http2frame类型详解通过GoogleDevelopers中的一张图,我们可以更好的理解HTTP2framing在网络数据它在哪里,以及它与HTTP/1.1有何不同。HTTP/1.1中的header变成了HEADERS类型的frame,requestbody/responsebody变成了DATA类型的frame。通过二进制分帧,传输的数据采用二进制方式,与文本方式相比减少了数据量;通过不同类型的框架实现流量控制、服务端推送等功能。多路复用(Multiplexing)&单连接(Oneconnectionperorigin)我们知道在HTTP2之前,如果要加快网页资源的加载速度,我们会采用同时建立多个连接的方式,但是效率每次建立一个TCP连接比较低,而且浏览器经常限制最大连接数(比如chrome的最大连接数是6)。另外,在HTTP/1.1中引入了Pipeline,可以在一个TCP连接中连续发送多个请求,而不管前一个响应是否到达,但是服务器必须按照收到请求的先后顺序进行响应,这样一旦前一个响应到达请求被阻塞,后续请求将不会得到及时响应。在HTTP2中,由于使用了新的二进制帧,很容易复用单个TCP连接。客户端和服务器可以将HTTP消息分解成独立的帧,交错发送,最后在另一端重新组合。还是GoogleDevelopers的图:可以看到我们可以并行交错发送多个响应和请求,并且使用同一个TCP连接,没有先后顺序,每一帧携带如何组装的信息,客户端会等待某个作业在所有需要的资源准备好后执行。流优先级(Streamprioritization)由于可以进行单连接多路复用,服务器和客户端的帧交错发送。对于发送到服务器的帧,为了解决哪些先处理,哪些后处理,引入了数据流。priority,服务器根据优先级分配资源。例如,优先级高的可以获得更多的CPU和带宽资源。那么优先级是如何标记的呢?请记住,先前的帧类型之一是优先级。这种帧是告诉服务器流的优先级。此外,HEADERS帧还包含优先级信息。HTTP/2通过父依赖和权重来标记优先级。每个流都会标记一个父流ID。如果没有标记,则默认为虚拟根流。这样就根据这个依赖关系构建了一个依赖树。树上层的流权重更高。高,同一层的流会有一个权重来区分资源分配比例。.上图是一些依赖树的例子,从左到右一共有四棵树。前两个流A和B不表示父流,默认依赖虚拟根节点。A和B在同一层,具有相同的优先级。资源按权重分配。A分到12/(12+4)=3/4资源,B得到1/4资源。第二个D和C有层次结构,C的父级是D,那么服务器会先处理资源完整的D,再处理C。第三个,服务器先处理D,再处理C,再处理C进程A和B。A分配了3/4的资源,B分配了1/4的资源。第四,先处理D,然后将资源分成两半处理E和C,然后按权重处理A和B。应当注意,流优先级不是强制约束。当优先级高的流被阻塞时,服务器必须处理低优先级的流头压缩(HeaderCompression)。由于现在的网站内容越来越复杂,单个页面的请求量基本都是几十个甚至上百个,而且每次请求都要带上客户端或者用户标识,比如UA,cookie等header数据,经过大量请求后,传输httpheaders消耗的流量也非常可观,而且大部分header数据都是雷同的,这是明目张胆的浪费。因此创建了标头压缩技术以节省流量。维护相同的静态字典(StaticTable),包括常见的头部名称,以及特别常见的头部名称和值的组合。维护相同的动态字典(DynamicTable),可以动态添加内容以支持基于静态的霍夫曼码表的HuffmanCoding静态字典。静态字典是将常用的头部映射到字节数较短的索引号上。静态表定义比如当header中有一个字段:method:GET,那么查表知道可以通过序号2来标识,所以这个字段的数据是0000010(2的二进制表示)动态字典静态字典可以表示头部数据,毕竟是有限的,压缩率不会高。但是对于一个站点来说,在和某个用户交互的时候会有很多请求,但是每个请求的header差别不大,会有很多重复的数据,因为用户和浏览器的标识符是不变的。然后我们可以生成一个动态字典,可以为一个HTTP2连接添加映射,这样动态字典中的序号就可以在后续的连接中使用。生成动态字典的过程其实就是通知对方添加一个映射。客户端可以通知服务器添加,反之亦然。具体的通知方式是按照协议规定的格式传输数据。霍夫曼编码的特点是出现频率越高,码长越短。在HTTP2协议中,基于大量的请求头数据样本生成了规范的霍夫曼编码,列在霍夫曼编码中。流量控制(Flowcontrol)HTTP/2流量控制的目标是在流量窗口初始值的约束下,赋予接收端全权控制自己想要接受的流量大小。算法:两端(发送端和接收端)都保持一个流量控制窗口(window)的初始值。发送方每次发送数据帧时,窗口都会按帧的大小递减。如果窗口小于框架大小,则必须拆分框架。如果窗口等于0,则不能发送任何帧。接收方可以向发送方发送WINDOW_UPDATE帧。发送方将帧中指定的WindowSizeIncrement作为增量添加到窗口中。服务器推送(ServerPush)流程:在交换SETTINGS帧时,设置字段SETTINGS_ENABLE_PUSH(0x2)为1明确允许服务器推送,当服务器收到请求时,分析要推送的资源,首先发送一个PUSH_PROMISE帧给浏览器然后发送每个响应头和响应体Browse当服务器收到PUSH_PROMISE帧时,根据headerblockfragment字段中的url可以知道当前是否有缓存,从而判断是否接收。如果没有,浏览器会发送一个RST_STREAM来终止服务器推送问题:流量浪费。如果浏览器有缓存,不想这个推送,就会浪费流量,因为整个过程是异步的。当服务器收到RST_STREAM时,可能会部分或全部发送响应。HTTP/2的简单实践Okhttp是java生态中知名的http客户端。它支持http2,因为它易于使用且性能良好。下面就用这个工具来实践一下,因为我的博客已经在nginx上配置了http2,所以就用这个博客来做实验。publicclassHttp2Example{finalstaticOkHttpClientclient=newOkHttpClient.Builder().build();publicstaticvoidmain(String[]args){Requestrequest=newRequest.Builder().url("https://blog.fliaping.com").build();试试System.out.println(response.body().string());}catch(IOExceptione){e.printStackTrace();}}}用过Okhttp的同学会发现,这个方法和平时一样,有没有任何区别,是的,没错,没有任何区别。别的不多说,直接运行看看。不幸的是,你会发现协议仍然是http1.1,而不是h2。这是怎么回事?这是因为HTTP2新增了ALPN(ApplicationLayerProtocolNegotiation),从字面意思理解就是应用层协议协商,即双方商量使用哪种协议。可惜jdk8是2014年发布的,那个时候HTTP2协议还没有诞生。好在ALPN可以通过第三方jar包来支持。另外jdk9已经支持HTTP2了。虽然还没有正式发布,但我们可以试试JDK9Early-AccessBuilds。jdk7和jdk8通过添加jvm参数添加第三方alpn支持包,注意版本不能错,jdk7使用alpn-boot-7.*.jar,jdk8使用alpn-boot-8.*.jar,这里是版本对应alpn-versions#jdk8-Xbootclasspath/p:/home/payne/Downloads/alpn-boot-8.1.11.v20170118.jar#jdk7-Xbootclasspath/p:/home/payne/Downloads/alpn-boot-7.1.3.v20150130.jar#jdk9#使用jdk9平台时注意okhttp版本大于3.3.0#https://mvnrepository.com/artifact/org.mortbay.jetty.alpn/alpn-boot
