漫谈HTTP

目录

一、HTTP协议发展史

二、HTTP 1.0

2.1 报文结构

2.2 典型特征

2.3 典型问题

2.4 Keep-Alive

三、HTTP 1.1

3.1 Keep-Alive变成缺省机制

3.2 Chunk机制

3.3 线头阻塞问题

3.4 Pipeline机制

3.5 性能优化举措

3.6 实现“一来多回”

3.7 断点续传

3.8 协议开销大

3.9 安全因素

四、HTTP/2

4.1 背景

4.2 与HTTP1.1的兼容问题

4.3. 二进制帧层

4.4 解决线头阻塞问题

4.5 请求和响应多路复用

4.6. Header 压缩

4.7. Server Push

在数字化的世界里,HTTP协议如同无形的纽带,从最初的简单请求响应模型到现在支撑复杂交互和安全通信的基石,它连接着互联网的每一个角落。接下来我们将深入浅出解析HTTP协议的演变历程,以及从HTTP1.0到HTTP/2的技术突破与挑战,一窥网络通信的心脏。


一、HTTP协议发展史

HTTP主要有三个版本,分别是HTTP 1.0、HTTP 1.1和HTTP/2,HTTPS是在HTTP基础之上的安全协议。

  • HTTP 1.0于1996年发布;

  • HTTP 1.1紧随其后,在1999年推出,并长期占据主导地位;

  • HTTP/2于2015年发布,尽管它是最新版,但目前远没有达到普及的程度,在过去的近20年间,主流协议一直都还是HTTP 1.1。


二、HTTP 1.0

2.1 报文结构
  • 起始行:用来标记当前报文是一个http request还是http response;

  • header:可以自定义存储多个K-V类型的键值对信息;

  • body:一个字符串;需要注意的是,如果是GET类型的请求,那么body为空。对于网页而言,body就是一个HTML串,用来被浏览器解析展示;。

图片

2.2 典型特征
  • HTTP 1.0是一个明文的文本协议,而非二进制协议,也就是说,我们可以直接看懂协议内容。

  • “一来一回”机制,即客户端发起一个TCP连接,然后在连接上发一个http request到服务器,服务端返回一个http response,然后关闭连接。在此过程中,每来一个请求就要建立一个连接,请求结束后再关闭连接。

2.3 典型问题
连接复用问题

连接无法复用会导致每次请求都经历三次握手和慢启动,建立和断开连接都会耗费时间。考虑到网页中HTML之外,JS、CSS、图片等资源也需要各自的HTTP请求,一个页面可能包含几个到几十个这样的资源。若每个请求都要开启新的TCP连接,会显著延长加载时间,同时,由于可用连接数量有限,大量并发连接请求会极大地消耗资源。

服务器推送问题

不支持“一来多回”机制,即服务器不能在未收到客户端请求时主动发送信息。然而,许多应用程序有需求,比如在特定事件完成后,服务器能即时通知客户端。

2.4 Keep-Alive

为了解决重复建立连接导致的性能瓶颈,HTTP引入了Keep-Alive特性。当客户端在HTTP请求头中设置Connection: Keep-Alive时,表明它希望保持连接开放。服务器响应时也会包含此字段,允许在同一连接上处理多个请求,从而实现连接复用。

然而,考虑到服务器资源有限,服务器设定了一个Keep-Alive-Timeout值。如果在指定时间内连接上没有新的请求,服务器会自动关闭该连接,以防止资源耗尽。

以前,由于每个连接仅用于单次请求和响应,客户端可以通过服务器关闭连接来判断请求是否已完成。但在Keep-Alive模式下,连接保持开启,客户端需要有其他方式来确定响应是否完整。

为解决这个问题,HTTP响应头中包含了一个Content-Length字段,它指示了响应体的字节数。客户端在接收到指定数量的字节后,便认为已经接收完了整个响应。


三、HTTP 1.1

3.1 Keep-Alive变成缺省机制

在HTTP 1.1中,连接复用被设置为了默认属性。即使请求头中没有指定Connection: Keep-Alive,服务器在处理完请求后也会保持连接开放。只有当请求头中明确包含Connection: Close时,服务器才会关闭连接。虽然Keep-Alive机制可以复用一部分连接,但对某些场景(如域名分片)而言,可能仍需创建多个连接,增加了服务器负担。

3.2 Chunk机制

Content-Length字段的一个局限性在于,对于由动态语言生成的响应,服务器需要在返回数据前预先计算其长度,这在某些情况下可能不切实际或效率低下。为了解决这一问题,HTTP 1.1引入了Chunked编码,也称为Http Streaming。

Chunked编码会在响应头中设置Transfer-Encoding: chunked,告知客户端响应体是以块的形式传输的。每个块都有一个大小标识(以十六进制表示),最后一个块大小为0,表示响应结束。这种方式允许服务器在不知道确切响应体长度的情况下,逐块发送数据,客户端可以根据块大小指示逐块接收并解析。

下面展示了一个简单的具有Chunk机制的HTTP响应,头部没有Content-Length字段,而是Transfer-Encoding:chunked字段。该响应包含了4个chunk,数字25(16进制)表示第一个chunk的字节数,IC(16进制)表示第二个chunk的字节数….最后的数字0表示整个响应的末尾。

图片

3.3 线头阻塞问题

线头阻塞问题会导致带宽无法被充分利用,以及后续健康请求被阻塞。

对于http1.0的实现,在第一个请求没有收到回复之前,后续从应用层发出的请求只能排队,请求2,3只能等请求1的response回来之后才能逐个发出。而在http1.1中,引入了pipeline来解决线头阻塞问题。

线头阻塞问题在HTTP协议中表现为,如果前一个请求由于某种原因(如服务器延迟、网络拥堵等)而滞后,那么后续的所有请求即便健康且准备好发送,也无法立即发送,必须等待前面的请求完成。这限制了并发性和带宽利用率,导致整体性能下降。

3.4 Pipeline机制

HTTP 1.1中的Pipeline机制试图缓解线头阻塞问题。通过允许客户端在一个TCP连接上连续发送多个请求,而无需等待每个请求的响应,提高了请求处理的并发性。这种方式理论上可以提高效率,因为在等待第一个请求响应的同时,其他请求已经处于发送状态。

然而,Pipeline机制并未完全解决线头阻塞。尽管请求可以并行发送,但响应仍然必须按照请求的顺序(First-In, First-Out,FIFO)返回。这意味着如果第一个请求的响应被延迟,后续所有请求的响应都会被阻塞,因为客户端需要按顺序接收并匹配响应。因此,尽管Pipeline提高了效率,但仍然存在潜在的阻塞风险。

图片

3.5 性能优化举措

由于队头阻塞问题的存在,Pipeline不能用,另一方面,浏览器对于同一个域名限制只能开6~8个连接,然而一个网页可能要发几十个HTTP请求,该如何提高网页渲染的性能呢?

Spriting技术

对于多个小图标,可以将它们合并到一张大的图像文件(称为精灵图或CSS雪碧图),然后通过CSS定位显示需要的部分。这样,浏览器只需一次HTTP请求即可加载所有图标,减少了请求数量。

内联(Inlining)

对于小图片,可以使用Base64编码将其内联在CSS或HTML中,减少HTTP请求次数。不过,这种方法适用于较小的图片,因为较大的Base64编码会增加文件大小,可能导致页面加载时间延长。

图片

JS合并压缩

合并多个JavaScript文件到一个文件中,并进行压缩,可以减少HTTP请求数量和传输的数据量,加快页面加载速度。

CDN域名

使用CDN可以分散负载,将静态资源(如图片、CSS、JS)分发到全球各地的服务器上,用户可以从最近的服务器获取资源,降低延迟。此外,通过使用多个CDN域名,浏览器可以为每个域名创建多个连接,增加并发加载能力。

延迟加载

对于非首屏内容,如滚动时才会出现的图片,可以使用延迟加载技术,只在需要时才发起请求,这样可以优先加载重要内容,提升用户体验。

预加载和预读取

预加载可以提前加载用户可能需要的资源,预读取则可以提前解析但不下载,以加速后续导航。

3.6 实现“一来多回”

在Web环境中,无论是HTTP 1.0还是HTTP 1.1协议,它们都不支持服务器端主动推送功能,然而这种功能在实践中经常被需要。以下是几种应对策略:

客户端定期轮询

客户端设定周期(例如每5秒)向服务器发送请求,若有新内容,服务器则响应。然而,这种方法效率低下,且加重了服务器负担,现已较少使用。

FlashSocketWebSocket

这些技术是基于TCP而非HTTP,能实现双向通信,但Flash Socket已过时,WebSocket虽然更先进,但也有特定限制,我们在此不做详细讨论。

HTTP长轮询

客户端发起请求,服务器在有新数据时立即响应,否则保持连接不关闭。当服务器无新数据时,发送一个预设的空消息给客户端,客户端收到后关闭连接并再次请求。这种方法模拟了TCP长连接,目前在Web开发中广泛应用。

HTTP Streaming

服务器通过Transfer-Encoding: chunked在单个TCP连接上持续发送未结束的数据流。与长轮询不同,它仅需一个HTTP请求,避免了重复的HTTP头问题,但实现起来相对复杂一些。

3.7 断点续传

在HTTP 1.1中,断点续传功能允许客户端在下载文件时中断并在稍后从断点处继续。客户端在下载过程中记录已下载的数据量,如果下载中断,它会在新的请求中包含Range头字段,例如 Range: bytes=500-1000,指示服务器从字节500开始到字节1000处提供数据。这样,服务器只需发送未完成的部分,而不需要重新发送整个文件。

同时我们需要明确,HTTP1.1的这种特性只适用于断点下载。要实现断点上传,需要自行实现。

3.8 协议开销大

HTTP 1.x 的一个缺点是其协议开销较大,尤其是当请求头包含大量信息时。即使每次请求的头信息基本相同,也会占用额外的网络带宽。这在移动设备上尤其明显,因为移动网络的带宽通常比固定网络更为有限

3.9 安全因素

HTTP 1.x 协议在传输数据时不提供加密和身份验证,这意味着数据在传输过程中是明文,容易被第三方截获或篡改。为了解决这个问题,通常会使用HTTPS(HTTP over SSL/TLS),它提供了数据加密、服务器身份验证和消息完整性检查,从而提高了网络通信的安全性。


四、HTTP/2

4.1 背景

鉴于HTTP/1.1的Pipeline机制存在缺陷,网络开发社区纷纷探索提升HTTP 1.1性能的策略。然而,这些策略仅在应用级别上寻求解决方案,缺乏广泛适用性。于是,有志之士便萌生了从协议层面着手解决此问题的想法,这便是谷歌公司推出SPDY协议的初心所在。

作为谷歌旗下的一项试验性项目,SPDY协议自2009年中旬问世以来,得到了Chrome、Firefox及Opera等浏览器的支持。随着越来越多的重量级(例如Google、Twitter、Facebook)及众多小型网站开始在其架构中实施SPDY,HTTP工作组注意到了这一发展趋势,并决定将其列入议程。他们借鉴了SPDY的实践经验和教训,以此为基础,进一步发展出了HTTP/2协议。

之所以采用“HTTP/2”而非“HTTP 2.0”作为协议名称,是因为工作组认为该协议已经相当成熟,不太可能出现更多的次版本更新。若有新的版本更新,下一步将是HTTP/3。因此,在HTTP/2标准发布后,谷歌公司也随之淘汰了SPDY协议,全面过渡到HTTP/2。

4.2 与HTTP1.1的兼容问题

HTTP1.1已经成了当今互联网的主流,因此HTTP/2在设计过程中,首先要考虑的就是和HTTP1.1的兼容问题,所谓兼容,也就是意味着:

  • 不能改变http://、https://这样的URL范式;

  • 不能改变HTTP Request和Http Response的报文结构。HTTP协议是一来一回,一个Request对应一个Response,并且Reqeust和Response的结构有明确的规定;。

如何才能做到在不改变ReqeustResponse报文结构的情况下,发明出一个新的HTTP2协议呢?

HTTP2和HTTP1.1并不是处于同一层级的协议,而是处在HTTP1.1和TCP之间,可以理解为是在HTTP1.1和TCP之间多了一个转换层。

图片

4.3. 二进制帧层

二进制帧层是HTTP2性能增强的核心,它规定了HTTP如何在服务端和客户端之间封装和传输消息。这里所谓的“层”,指的是位于套接字接口与应用可见的高级 HTTP API 之间一个经过优化的新编码机制,HTTP 的语义都不受影响,不同的是传输期间对它们的编码方式变了。HTTP/1.x 协议以换行符作为纯文本的分隔符,而 HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码。

图片

二进制分帧机制改变了客户端与服务器之间交换数据的方式。为了说明这个过程,我们需要了解 HTTP/2 的三个核心概念:

  • 数据流 (stream):已建立的连接内的双向字节流,可以承载一条或多条消息。

  • 消息 (message):与逻辑请求或响应消息对应的完整的一系列帧。

  • 帧 (frame):HTTP/2 通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。

这些概念的关系总结如下:

  • 所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流。

  • 每条stream都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。

  • 每个message都是一条逻辑 HTTP 消息(例如请求或响应),包含一个或多个帧。

  • frame是最小的通信单位,承载着特定类型的数据,例如 HTTP 标头、消息负载等等。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

图片

简言之,HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些都在一个 TCP 连接内复用。 

图片

  • length定义了整个frame的开始到结束,

  • type定义frame的类型(一共10种);

  • flags用bit位定义一些重要的参数;

  • stream id用作流控制,一个request对应一个stream并分配一个id,这样一个连接上可以有多个stream,每个stream的frame可以随机的混杂在一起,接收方可以根据stream id将frame再归属到各自不同的request里面;

  • payload就是request的正文了;

虽然HTTP/2的协议格式与HTTP1.x看起来完全不同,但实际上,HTTP/2并没有改变HTTP1.x的语义。它只是通过对原来HTTP1.x的头部和主体部分进行重新封装,引入了一种称为"frame"的结构。在调试时,浏览器会自动将HTTP/2的帧恢复为HTTP1.x的格式。

对于HTTP1.x来说,通过设置TCP段中的重置标志来通知对端关闭连接。但这种方式会直接断开连接,下次再发请求就必须重新建立连接。而HTTP/2引入了一种名为RST_STREAM类型的帧,它可以在不断开连接的情况下取消某个请求的流,从而表现更为优越。

4.4 解决线头阻塞问题

帧是最小的通信单位,承载着特定类型的数据,例如 HTTP 标头、消息负载等等。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

有了二进制分帧后,虽然在TCP层面还是串行发送的,但在HTTP层面来看,请求就是同时发出、同时接收的。没有了HTTP1.1的Pipeline限制,请求之间虽然是按顺序发出的,但响应之间却可以乱序返回。

图片

但即使这样,也并没有彻底解决线头阻塞问题。

因为只要我们使用的是TCP协议,那么就永远绕不开线头阻塞问题,因为TCP协议就是先进先出的,如上图所示,如果队头的帧在网络上被阻塞,则服务器会一直处于等待状态,在此期间,后面的包都不会被成功接收。

因此进制分帧只是将线头阻塞的问题从http请求的粒度细化到了帧的粒度,从而降低了线头阻塞发生的概率。

为什么说是降低线头阻塞发生的概率呢?我们可以通过下面的示例来理解:假设服务器对队列中请求1的处理速度很快,但网络传输慢了,那么这个时候就要分两种情况来考虑:

  • 如果请求2和请求3的响应分帧后先于请求1的响应发出,此时请求2和请求3的响应不会被请求1阻塞,从而避免线头阻塞

  • 而如果请求1的响应分帧先于请求2和请求3的响应发出,那么此时仍然会发生线头阻塞

4.5 请求和响应多路复用

直白的说就是所有的请求都是通过一个 TCP 连接并发完成。尽管HTTP/1.x通过pipeline也能实现请求并发,但多个请求之间的响应会受阻,因此pipeline至今未被广泛应用。而HTTP/2实现了真正的请求并发,在流并发时,涉及到流的优先级和依赖,优先级高的流会被优先发送。

下图快照捕捉了同一个连接内并行的多个数据流。客户端正在向服务器传输一个 DATA 帧(数据流 5),与此同时,服务器正向客户端交错发送数据流 1 和数据流 3 的一系列帧。因此,一个连接上同时有三个并行数据流。

图片

将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP 2 最重要的一项增强:

  • 并行交错地发送多个请求,请求之间互不影响。

  • 并行交错地发送多个响应,响应之间互不干扰。

  • 使用一个连接并行发送多个请求和响应。

  • 不必再为绕过 HTTP/1.x 限制而做很多工作(例如级联文件、image sprites和域名分片)

  • 消除不必要的延迟和提高现有网络容量的利用率,从而减少页面加载时间。

    图片

4.6. Header 压缩

在HTTP/1.x中,我们使用文本形式传输头部,在携带cookie的情况下,可能需要重复传输大量数据。

为了减少资源消耗并提升性能,HTTP/2采取了头部压缩策略,使用首部表来跟踪和存储之前发送的键值对,在连接持续期内,客户端和服务器共同渐进地更新首部表;每个新的首部键值对要么追加到当前表的末尾,要么替换表中之前的值。这样,可以减少冗余数据,降低网络传输开销。

图片

4.7. Server Push

HTTP/2允许服务器主动推送资源给客户端,打破了严格的请求-响应模式。所有的服务器推送都由PUSH_PROMISE帧发起,表明服务器向客户端推送资源的意图,并要求在推送资源的响应数据传输之前发送。这一顺序很关键:客户端需要知道服务器计划推送哪些资源,以免创建重复请求。满足这个要求的简单策略是在父响应(即DATA帧)之前发送所有PUSH_PROMISE帧,其中包含承诺资源的HTTP标头。

图片

假设服务端接收到客户端对 HTML 文件的请求,决定用 server push 推送一个css文件。那么,服务端会构造一个请求,包括请求方法和请求头,填充到一个 PUSH_PROMISE 帧里发送给客户端,来告知客户端它已经代劳发出了这个请求。

当客户端收到这个 PUSH_PROMISE 帧的时候,它就知道服务端将要推送一个CSS文件回来。如果此时客户端需要请求这个文件,即便服务端还没推完,它也不会往服务端发送对CSS文件的请求。

在这个例子中,必须先发送 PUSH_PROMISE,再发送 HTML 的内容。这是因为 HTML 中存在对CSS的引用,一旦客户端发现了这个引用却还没收到 PUSH_PROMISE,它就会发起获取CSS文件请求。

客户端在接收到PUSH_PROMISE帧后,可以根据自身情况选择拒绝数据流(通过RST_STREAM帧)。相比之下,HTTP/1.x版本中使用资源内联等同于"强制推送",客户端无法选择拒绝、取消或单独处理内联资源。

在HTTP/2中,客户端仍然完全掌控服务器推送的使用方式。客户端可以限制并行推送的数据流数量,调整初始流控窗口来控制在数据流首次打开时推送的数据量,或者完全停用服务器推送。这些优先级通过SETTINGS帧在HTTP/2连接开始时传输,并可能随时更新。

每个推送的资源都是一个数据流,与内联资源不同,客户端可以逐一复用、设定优先级和处理推送的资源。唯一的安全限制是推送的资源必须符合同源政策:服务器对提供的内容必须具有权威性。


正如HTTP协议本身的演进一样,我们的探索和学习永无止境。在未来的数字时代,HTTP将继续演化,以适应不断变化的技术需求和社会需求,我们也将继续见证和参与这一过程,共同塑造我们的数字世界。

欢迎关注微信订阅号:技术勘察馆

  • 57
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值