承接上文HTTP请求的详细过程
http协议版本历史
http造就了万维网,http成就了互联网第三次信息技术革命并且影响着即将到来的第四次人工智能技术革命。
1989年第一个http协议,http0.9发布,发明了万维网,创建了世界第一个网页浏览器;
http1.0版本是在1996年正式发版的 ,这个时候才丰富了交互响应的文件类型,96年之前网页是不能播放音乐的;
97年发布http1.1版本,2015年才发布http2.0版本,源自谷歌的SPDY协议,目前现代浏览器完全支持http2.0,http2可以更好的表达设备的在线状态;
2022年6月,发布了http3 ,使用QUIC协议替代了TCP协议,作为位于应用层的通用传输协议,这是一种基于UDP构建的多路复用传输协议;
http3.0版本解决了TCP的一些问题,提高效率并保留了TCP的所有优点。
http2和http3这2个协议版本都是由谷歌主导的,并且第一个在chrome浏览器率先实现的;
http0.9到1.0版本见证了web1.0时代的开始,http1.1-2.0版本见证了web1.0并完成了它的使命和见证了web2.0时代发展到了今天。
这是开放式系统互联的OSI七层模型图,http协议位于应用程序层,需要经过传输层才能进行通信。
理论上传输层用哪个协议并不是那么重要,但一定要知道传输层的TCP和UDP两个协议到底是什么,着重介绍传输层的原因是因为它对http协议至关重要,后面的IP层和其他底层对TCP和UDP都很友好,而且对http各个版本的发展并没有影响。
TCP协议即传输控制协议,首先通过三次握手建立连接,然后传输数据,数据传输完毕,进入四次挥手关闭连接,这中间出现了数据丢失,则会重试保证数据传输可靠性。
可以看出TCP是一个面向连接的可靠的传输协议,而UDP协议是用户数据协议,是面向数据报文的,发出去就发出去了,发不出去就发不出去,发丢了消息也不会去重试,UDP不需要握手建立连接,发送消息的数据又小,所以速度更快。
为了保证传输的可靠性,http0.9到2.0版本都是采用tcp协议传输,到了3.0版本就抛弃了tcp改用udp传输,利用QUIC协议在应用层保证数据传输的可靠性;
http0.9是万维网第一个http协议,经过三次握手建立连接,客户端发起get请求,服务器仅能响应http文本类型的数据,但不能在客户端显示图片和播放音乐,客户端收到数据之后,四次挥手关闭连接,所以也叫单线协议,同时http0.9版本没有http标头,没有状态码、错误码,
如图所示仅仅使用一行请求信息,所以也叫它单行版本,就是因为0.9版本太简陋,服务器仅能响应html文件,比如制作个人网页,
于是就有了http1.0,请求加入了http协议标头,响应加了状态码和传输的内容类型,
也就是经常在服务器配置的MIME媒体类型,即多用途互联网邮件类型。
不为http服务器配置MIME类型,服务器就没办法解释这个内容类型是个啥,客户端也不认识就会出现问题,服务器配置好以后,就可以传输更多类型的文件内容数据了。
和0.9版本一样,1.0版本还是单线协议,一个连接只能处理一个请求一个响应,每次建立物理连接的成本很高,
并且服务器遭受不住太多客户端访问,否则服务器扛不住就会直接给你报503错误,为了减轻服务器压力,出现了升级的http1.1版本,该版本中引入了一个重要的概念,就是持久连接的概念,
即增加了keep-alive值的标头,就意味着建立连接后,指定时间内不会关闭这个连接,可以在保持的这个连接上发送多个请求,并且不会出现一个往返就关闭连接的情况,
节省了每次交互都会握手和挥手的时间,除此之外1.1版本还引入了流水线概念,在服务器响应前,客户端可以一次发送多个请求,服务器收到多个请求后,按顺序将准备好的数据再一个一个的回复给客户端,对比一下左边一次一个请求一个响应和右边一次多个请求逐个响应的过程,可以看到明显的节约了请求发送的时间,提高了传输的效率。
按顺序响应有没有问题?
线头阻塞问题
首先就会造成一个重要的问题,head of line blocking(线头阻塞即流水线头阻塞),
假如客户端处理了三个请求,第一个请求处理的时间太长,第二个第三个请求是不是得必须老实的排队,等待第一个请求处理完成呢?
这就造成了流水线的塞车现象,在这个版本中解决塞车的办法就是浏览器通常会保持6个左右的连接以缓解一个连接塞车,客户端可以通过其他连接继续发送请求。
http标头冗余问题
在连续发送请求的情况下,每次请求总是会带上相同基本不会变化的http标头,并且http1.1发送的数据还是纯文本的,传输数据不但大还啰嗦。
http2协议
以上2个问题就是http2要解决的,http2协议是http1.1版本的扩展而不是替换,因为它只是更好的解决了1.1版本的问题和功能补充,
b站网络请求就是使用的http2协议,
http2在应用层改变了传输方式,之前版本应用程序层传递的http标头和数据都是明文的,
http2在应用层增加了一个二进制帧数据处理层,
将http标头和数据明文消息拆分成二进制数据帧进行传输,
每个数据帧包含自己携带的数据并用stream id做标识,这也解释了什么叫数据流,就是将数据切开形成数据帧,然后将它们单独或者打包成独立流传输到目的地后,
再按照stream id将它们组装起来还原成原始数据。
好处就是OSI模型应用程序下的底层协议,更喜欢二进制数据帧,因为它们的大部分工作被应用程序层做了,它们的活少了,响应的网络传输速度也就提高了。
另一个好处就是再大的文件也给你剁的细碎,规避了大文件传输大小的限制,比如IIS、apache等http服务器都有对单个请求体又大小的限制。
客户端以流的方式请求,服务器以流的形式响应,就形成了双向数据流,数据帧都用stream id做标识,然后stream id将多个数据帧识别为一个消息,
保证了数据的完整性。
http2协议是客户端与服务器建立一个连接,再加上双向数据流,客户端和服务器的交互就可以重用它们建立的单个连接传递数据,就形成了http2的多路复用。
既然数据帧标明了stream id,那么传输数据流的顺序可以是乱序的,当然传输数据也可以设置优先权重的,
无论是客户端还是服器接收到的流数据先到的在缓冲区先组装,不用等待前一个请求数据处理完成再处理下一个,
就这解决了http1.1应用程序层的http线头阻塞的问题,http2是否真正解决了线头阻塞的问题?
之所以前面着重强调的是应用程序层的http线头阻塞,原因是http2仅在二进制帧处理层拿到完成的tcp数据后解决了线头阻塞问题,http2同样基于传输层的tcp协议传输的,
TCP标头格式
tcp协议为了保证消息可靠,顺序传递消息,就有可能造成塞车的情况,所以传输层的tcp线头阻塞问题并没有得到解决,为了说明这个问题,来模拟下传输层tcp传递数据包的视角。
当打开b站,我们和浏览器都明白我们向b站服务器请求获取js、css等文件资源,
http2通过前面讲的二进制帧数据处理层,将数据剁碎,加入stream id,用数据流的方式传递数据,其他的,http2并不关心,更不用说到达传输层的tcp协议了,甚至都不知道这是传输的是http请求、响应的信息,tcp只知道数据包的最大大小通常是1450字节。
http2跟踪传输的指定字节范围的数据部分,以便可以按正确顺序来还原原始数据,
假如tcp数据包2在传输过程中丢失了,
那么tcp数据包1和3会提前达到缓冲区,tcp就会发现数据包1和3之间缺少了数据包2的字节范围数据,哪怕http2知道tcp数据包1中有stream id为1的数据帧,加上tcp数据包3中stream为1的数据帧,可以组装成一个完整的消息,也要耐心等待tcp至少重返服务器一次,来重传丢包的数据2的数据副本到达才能继续解包其他tcp数据交给应用程序层,就是丢失的tcp数据包2阻塞了tcp数据包3导致了tcp的线头阻塞,所以结论就是http2并没有完全解决线头阻塞的问题。
接下来讲个http2重要的扩展功能:
http2服务器推送功能
首先正是因为http2具有双向流、多路复用的特点 ,才更有利于http2提供的服务器推送功能,比如直接打开b站 请求的是首页,
b站服务器可以将未经过请求的、首页所需的js、css等文件资源准备好之后直接推送给我们;
同时对于前端开发者,http2更加有益于html5套件的SSE功能(Server-Send Events即服务器发送事件),
服务器推送功能进一步减少了网络请求的数量,提高了交互的效率。
http2标头压缩功能
打开浏览器开发者工具,直接进入百度首页,
百度依然使用的是http1.1版本,
百度首页的请求标头是689字节,响应标头是598字节,点击标头标签可以看到请求和响应的所有标头信息,尝试刷新下页面,看到请求发送标头大小没有任何变化,说明http1.1来回总是在说车轱辘话。
b站使用的是http2协议,
b站首次请求的标头大小是387字节,响应的标头大小是417字节,进入标头标签可以详细看到http2请求和响应标头信息,
再次刷新页面,发现请求标头缩小至116kb,响应字节也小了,
再次刷新页面,发现b站请求和响应标头更小了,而达到这种节约网络请求的标头大小的效果就要归功于http2是使用HPACK来编解码http标头的。
HPACK的原理就是建立连接的客户端和服务器都维护着相同的61个条目的只读的静态表和可动态添加条目的空白的动态表,
当请求发送的时候先查找表中的名值,名值完全匹配直接提取该标头在表中对应的整数索引用于表示该名值对。
如果只匹配名称,则使用索引表示名称,值就用霍夫曼huffman算法进行编码后表示,
表中没有的,则按照顺序添加到动态表中,再使用霍夫曼算法编码名值对字符后传递给服务器;服务器端则反之, 使用表中的索引解码或者按顺序使用霍夫曼算法进行解码后添加到服务器动态表中,这样保证双方的表中保存的数据和整数索引是一致的,在之后的所有请求的标头会越来越小,减少了网络数据流量来提高交互的效率。
这是http2标准中已经定义的61个标头的静态表,
类似:path这样的标头名称,被称为伪标头,出现伪标头的原因是为了兼容之前的版本,
http2的标头都是名值对,之前版本的标头都存在标头行,http2将http1的标头行拆分成名值后的名称前加上了冒号,方便与http2的标头名称进行区分。
什么是霍夫曼算法
最优前缀码算法,利用二叉树将出现频率大的符号采用较短的有效编码,频率小的有效编码更长,解决等长编码解压时出现歧义的问题,
字节下的数字代表这个字母出现的频率,
霍夫曼算法会先从出现频率低的两个字母,按照左树代表0放出现频率小的字母
右树代表1放出现频率大的字母的原则合并组成一个新节点。
以此类推最终组成一个二叉树,
以上6个字母的最终编码如图所示,a=0直到f=1100,你是否认为http2每次都对未出现在表中的标头字符都这样算一遍?
http2标准按照标头字符的使用频率,专门准备了一张霍夫曼编码表,刚好256个条目,
对应ASCII编码表示的256个可能的字符,那么对应霍夫曼编码表,编码就不需要二叉树运算了,提高了编码的效率,例如在霍夫曼编码表中,位于索引47位置的符号/,由6位二进制编码011000组成,16进制编码值就是0x18。
http2留给我们的三个思考
了解HPACK原理之后 是不是可以更好的优化网络请求?
因为客户端对同一个网站交互越多,
动态表中积累的http标头条目就越多,标头压缩的效果就越好,所以这里有一个http2的优化技巧,交互最好是同域名或者同域名下不同二级域名指向的服务器ip是一样的,
而且使用的是同一个加密证书,这样会确保使用相同的静动态标头表,这也解释了为啥泛域名的SSL加密证书每年费用那么贵的原因。
HPACK是按顺序编解码标头的是不是也会造成线头阻塞问题?
是的,因为http2数据流可以是无序的,
请求数据流达到服务器后,请求携带编码后的标头中,
发现有个请求标头索引比服务器解码动态标头表中的最大索引值还大,就会造成无法解码,而导致使用该请求标头相关的数据流都会被阻塞,直到携带该索引表示的正确霍夫曼编码的数据流达到后,
才能解码之前的请求数据流,如果这条数据流丢包了,就会造成塞车时间更长,目前这个塞车问题只能留给http3解决了。
第三个思考是使用多路复用,它真的就比之前版本的按顺序传输更快吗
真正解决了交互事务中的线头阻塞问题了吗? 答案是不一定
不是说好了速度更快且没有http线头阻塞问题?
首先表象的看http2和现代浏览器强制了必须使用https加密超文本协议传输,那https是如何工作的?
相比于http1.1之前不强制使用https加密,http2在加密交换密钥的过程中,最少增加了2个往返时间即2RRT(Round-Trip Time),反而减慢了交互速度。
另一个是多路复用中数据被切碎成数据帧,并用stream id标识打包成数据流传输,这样就可以乱序传输了。
假如我们一次请求一个js或css文件,根据文件大小假如被切分成了5份和3分,来对比下顺序传输和乱序传输
,顺序传输的js文件的接收速度比乱序的js传输速度更快,
总的到达时间基本一致的,所以在http2中的多路复用,就需要根据实际情况考虑使用优先级控制优化了。
再比如乱序中某个数据的丢包,在缓存中排序组装消息的时候发现少了一帧,
就需要至少往返一次服务器,拿丢失的数据帧副本,虽然没阻塞其他的消息,但确实阻塞了自己,所以这个问题 不单单存在于http2,http3的多路复用也存在这个问题,虽然这种数据流的线头阻塞是概率问题,但确实存在。
http3
它是基于QUIC协议的升级版本,
已经知道了http2基于传输层的tcp协议传输,仅仅解决了在应用程序层的http线头阻塞的问题,
但又由于传输层的协议基本由系统实现,应用程序层无法干涉,所以http3就想,既然动不了你,干嘛不直接抛弃你,http3就直接使用了传输效率更高的UDP,虽然UDP不保证消息传输的可靠性,但在http2已经解决了应用程序层http的线头阻塞问题的基础之上,
http3在应用程序层适配UDP并保证数据传输的可靠性,就推出了基于UDP的QUIC协议,可以说它是tcp2.0,只不过是在应用程序层实现的,QUIC协议实现了tcp的可靠性、拥塞控制、流量控制、排序等功能和保留了http2基于流的多路复用及http2优化的其他功能。
http3将基于UDP的流作为一等公民和http2的流处理有2个重要区别
一个区别是http3使用动态标头编码QPACK和http2使用HPACK,QPACK和HPACK在编解码方式都是一样的,重要区别在与QPACK增加了静态表中的标头条目至98个。
另一个就是解决HPACK按顺序解码容易造成线头阻塞的问题,
QPACK的解决办法就是使用指令独立流,
提前讲好需要增加的标头项,使用添加指令发送给解码方,解码方收到后回复确认标头指令,从而双方利用标头动态表中标头项的状态,来确认标头是否完成同步,标头项状态未被确认的,请求的数据流就不会使用该项标头的索引编码标头以解决动态标头解码的塞车问题。
另一个区别就是因为QUIC协议实现了tcp协议的所有功能,那QUIC协议如何规避tcp线头阻塞的缺点?
http2存在tcp的线头阻塞,是因为tcp只知道跟踪携带的字节范围数据,并不知道我们前面讲的tcp线头阻塞例子中数据包1中的stream id为1的数据帧和数据包3中的stream id为1的数据帧可以组装成一个消息,因为tcp根本不知道传输的具体细节。
在http3中QUIC协议优化了帧的组成部分,脱掉了tcp外衣,直接在数据帧中加入了Offset字节范围帧头,
这样就能根据stream id和Offset帧头,判断哪些数据包中的帧数是同一个消息,因此例子中数据包1中的stream id为1的数据帧,
就可以和数据包3中的steram id为1的数据帧组成一个消息,因为它们的字节范围可以匹配的上,这样就解决了即使丢了数据包2,也不影响其他消息的情况,从而消除了tcp线头阻塞的缺点。
http3的三个重要优点
基于UDP有个明显的好处就是数据到达传输层是需要加入传输层协议的头部。
http2和之前的版本使用的是tcp协议,
需要20-60个字节的头部,
而http3采用传输层的UDP协议,头部仅仅8个字节,这样就进一步优化了交互数据的大小,提高了传输速度,QUIC协议基于UDP有个更大的好处就是0往返时间建立连接即0 RTT,
tcp建立连接是需要经历3次握手和四次挥手的,QUIC协议是基于UDP的,就不需要三次握手和四次挥手,名义上实现了0 RTT,但是http3也需要加密传输,直接将加密协商信息和请求数据一起发送给服务器,只需要1个RTT就可以完成建立连接,相比于http2的加密协商至少需要2个RTT来说,还少了一个RTT。
第三个优点:tcp是基于连接的,而QUIC协议基于UDP的数据传输,是基于报文的,那http3怎么识别当前连接并实现多路复用的?
使用tcp建立的连接会基于源IP、源端口、目的IP和目的端口四元组信息,来确认是否已经建立过该连接,假如说从wifi切换到5G至少会导致四元组信息中的源IP地址信息会发生改变,tcp就必须重新建立连接,
而QUIC协议使用的是随机数作为connection id即连接id,来识别客户端和同域名服务器建立连接,从而不受网络变化的影响,通过connection id确定了连接,当然就可以实现和http2一样的双向流多路复用的功能,基于这个优点,http3协议就非常适合移动互联网了。