HTTP权威指南

HTTP头部

通用头部

通用信息头部

通用缓存头部

请求头部

响应头部

实体头部

HTTP连接管理

TCP连接

TCP 为 HTTP 提供了一条可靠的比特传输管道。从 TCP 连接一端填入的字节会从另一端以原有的顺序、正确地传送出来。

TCP 会按序、无差错地承载 HTTP 数据。

HTTP 要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序传输。

TCP 收到数据流之后,会将数据流砍成被称作段的小数据块,并将段封装在 IP 分组中,通过因特网进行传输。

所有这些工作都是由 TCP/IP 软件来处理的,HTTP 程序员什么都看不到。

每个 TCP 段都是由 IP 分组承载,从一个 IP 地址发送到另一个 IP 地址的。每个 IP分组中都包括:

  • 一个 IP 分组首部(通常为 20 字节):IP 首部包含了源和目的 IP 地址、长度和其他一些标记
  • 一个 TCP 段首部(通常为 20 字节):TCP 段的首部包含了 TCP端口号、TCP 控制标记,以及用于数据排序和完整性检查的一些数字值
  • 一个 TCP 数据块(0 个或多个字节)

在任意时刻计算机都可以有几条 TCP 连接处于打开状态。TCP 是通过端口号来保持所有这些连接持续不断地运行。

TCP 连接是通过四元组来唯一标识的:
< 源 IP 地址、源端口号、目的 IP 地址、目的端口号 >

两条不同的 TCP 连接不能拥有 4 个完全相同的值(但不同连接的部分组件可以拥有相同的值)

头条面试题

网页中的图片资源为什么分放在不同的域名下 ? 比如知乎中的图片资源的URL通常是 https://pic1.zhimg.com/013926975_im.jpg ,与主站的域名不同。

浏览器针对同一个域名的并发请求是有限制的,Chrome是6个。即在Chrome中访问 zhihu.com 时,同时最多只能有6个TCP连接

浏览器对并发请求的数目限制是针对域名的,即针对同一域名(包括二级域名)在同一时间支持的并发请求数量的限制。

如果请求数目超出限制,则会阻塞。因此,网站中对一些静态资源,使用不同的一级域名,可以提升浏览器并行请求的数目,加速界面资源的获取速度。

HTTP时延

与建立 TCP 连接,以及传输请求和响应报文的时间相比,事务处理时间可能是很短的。

除非服务端或客户端超载有性能问题,否则 HTTP 时延就是由 TCP 网络时延构成的。

1、域名解析。

2、建立TCP连接,TCP三次握手,四次断开。

3、客户端通过TCP管道发送HTTP请求报文。

4、服务器收到报文后,进行处理,生成响应报文。

5、通过TCP管道向客户端发送响应报文。

6、客户端浏览器收到报文,并解析渲染页面。

TCP三次握手时延


1、请求新的 TCP 连接时,客户端要向服务器发送一个小的 TCP 分组(通常是 40 ~60 个字节)。这个分组中设置了一个特殊的 SYN 标记,说明这是一个连接请求。

2、如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端回送一个TCP 分组,这个分组中的 SYN 和 ACK 标记都被置位,说明连接请求已被接受。

3、最后,客户端向服务器回送一条确认信息,通知它连接已成功建立。现代的 TCP 栈都允许客户端在这个确认分组中发送数据。

延时确认


由于因特网自身无法确保可靠的分组传输(因特网路由器超负荷的话,可以随意丢弃分组),所以 TCP 实现了自己的确认机制来确保数据的成功传输。

每个 TCP 段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段时,都会向发送者回送小的确认分组。

如果发送者没有在指定的窗口时间内收到确认信息,发送者就认为分组已被破坏或损毁,并重发数据。

由于确认报文很小,所以 TCP 允许在发往相同方向的输出数据分组中对其进行“捎带”。TCP 将返回的确认信息与输出的数据分组结合在一起,可以更有效地利用网络

为了增加确认报文找到同向传输数据分组的可能性,很多 TCP 栈都实现了一种“延迟确认”算法。

延迟确认算法会在一个特定的窗口时间(通常是 100 ~ 200 毫秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。

如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送。

但是,HTTP 具有双峰特征的请求 - 应答行为降低了捎带信息的可能。当希望有相反方向回传分组的时候,偏偏没有那么多。通常,延迟确认算法会引入相当大的时延。根据所使用操作系统的不同,可以调整或禁止延迟确认算法。

在对 TCP 栈的任何参数进行修改之前,一定要对自己在做什么有清醒的认识。TCP中引入这些算法的目的是防止设计欠佳的应用程序对因特网造成破坏。

对 TCP 配置进行的任意修改,都要绝对确保应用程序不会引发这些算法所要避免的问题。

TCP慢启动


在网络实际的传输过程中,会出现拥塞的现象,网络上充斥着非常多的数据包,但是却不能按时被传送,形成网络拥塞,其实就是和平时的堵车一个性质了。

TCP设计中也考虑到这一点,使用了一些算法来检测网络拥塞现象,如果拥塞产生,变会调整发送策略,减少数据包的发送来缓解网络的压力。

拥塞控制主要有四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生时,快速重传
  • 快速恢复

慢启动为发送方的TCP增加了一个窗口:拥塞窗口,记为 cwnd,,初始化之后慢慢增加这个 cwnd 的值来提升速度。同时也引入了 ssthresh 门限值,如果 cwnd 达到这个值会让 cwnd 的增长变得平滑,算法如下:

  1. 连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据

  2. 每当收到一个ACK,cwnd++; 呈线性上升

  3. 每当过了一个RTT,cwnd = cwnd*2; 呈指数让升

  4. 当cwnd >= ssthresh时,就会进入“拥塞避免算法”

简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。

如果某个 HTTP 事务有大量数据要发送,是不能一次将所有分组都发送出去的。必须发送一个分组,等待确认;然后可以发送两个分组,每个分组都必须被确认,这样就可以发送四个分组了,

以此类推。这种方式被称为“打开拥塞窗口”。

由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定量数据的、“已调谐”的连接慢一些.

Nagle算法

TCP 有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入 TCP 栈中——即使一次只放一个字节也可以!

每个 TCP 段中都至少装载了 40 个字节的标记和首部,所以如果 TCP 发送了大量包含少量数据的分组,网络的性能就会严重下降。

发送大量单字节分组的行为称为“发送端傻窗口综合症”。这种行为效率很低、违反社会道德,而且可能会影响其他的因特网流量。

HTTP连接的处理

connection首部

HTTP 允许在客户端和最终的源端服务器之间存在一串 HTTP 中间实体(代理,缓存等)

可以从客户端开始,逐跳地将 HTTP 报文经过这些中间设备,转发到源端服务器上去(或者进行反向传输)。

HTTP 应用程序收到一条带有 Connection 首部的报文时,接收端会解析发送端请求的所有选项,并将其应用。然后会在将此报文转发给下一跳地址之前,删除Connection 首部以及 Connection 中列出的所有首部。

串行事务处理时延

假设有一个包含了 3 个嵌入图片的 Web 页面。浏览器需要发起 4 个 HTTP 事务来显示此页面:
1 个用于顶层的 HTML 页面,3 个用于嵌入的图片

如果每个事务都需要(串行地建立)一条新的连接,那么连接时延和慢启动时延就会叠加起来:

并行连接

并行连接,意思就是打开多个TCP连接,HTTP 允许客户端打开多条TCP连接,并行地执行多个 HTTP 事务。

包含嵌入对象的组合页面如果能(通过并行连接)克服单条连接的空载时间和带宽限制,加载速度也会有所提高。

如果单条连接没有充分利用客户端的因特网带宽,可以将未用带宽分配来装载其他对象。

即使并行连接的速度可能会更快,但并不一定总是更快。

客户端的网络带宽不足时,大部分的时间可能都是用来传送数据的。在这种情况下,一个连接到速度较快服务器上的HTTP 事务就会很容易地耗尽所有可用的 Modem 带宽。

如果并行加载多个对象,每个对象都会去竞争这有限的带宽,每个对象都会以较慢的速度按比例加载,这样带来的性能提升就很小,甚至没什么提升。

并行连接的问题

  • 每个事务都会打开 / 关闭一条新的连接,会耗费时间和带宽。
  • 由于 TCP 慢启动特性的存在,每条新连接的性能都会有所降低。
  • 可打开的并行连接数量实际上是有限的。 (浏览器并发请求限制)

持久连接

持久连接就是TCP连接的重用,一个 Web 页面上的大部分内嵌图片通常都来自同一个 Web 站点,而且相当一部分指向其他对象的超链通常都指向同一个站点。

初始化了对某服务器 HTTP 请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求,这种特性称为站点局部性(site locality)

HTTP/1.1允许在 HTTP 设备在事务处理结束之后将 TCP 连接保持在打开状态,以便为未来的 HTTP 请求重用现存的连接。

事务执行结束之后保存打开状态的连接,叫持久连接。

管理持久连接可能的问题

不小心就会累积出大量的空闲连接,耗费本地以及远程客户端和服务器上的资源。

HTTP/1.0+ keep-alive连接

keep-alive是http/1.0中规定的一个规范,

客户端可以通过包含 Connection: Keep-Alive首部请求将一条连接保持在打开状态

如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部

如果响应中没有 Connection: Keep-Alive 首部,客户端就认为服务器不支持 keep-alive,会在发回响应报文之后关闭连接。

Keep-Alive选项

1
2
3
4
5
6
7
# 响应首部
# Keep-Alive 首部完全是可选的,但只有在提供 Connection: Keep-Alive 时才能使用它
# timeout 估计了服务器希望将连接保持在活跃状态的时间,这并不是一个承诺值。
# max 估计了服务器还希望为多少个事务保持此连接的活跃状态。这并不是一个承诺值。
Connection: Keep-Alive
Keep-Alive: max=5, timeout=120
# 这个例子说明服务器最多还会为另外 5 个事务保持连接的打开状态,或者将打开状态保持到连接空闲了 2 分钟之后

Keep-Alive 连接的限制和规则

  • 在 HTTP/1.0 中,keep-alive 并不是默认使用的。 客户端必须发送一个 Connection: Keep-Alive 请求首部来激活 keep-alive 连接。
  • 如果客户端没有发送
  • 哑代理,Keep-Alive首部是针对单条TCP链路的,逐跳首部只与一条特定的连接有关,不能被转发。
  • 所以现代的代理在转发报文时,要去掉 Connection 和 Keep-Alived 首部

HTTP/1.1 持久连接

与 HTTP/1.0+ 的 keep-alive 连接不同,HTTP/1.1 持久连接在默认情况下是激活的。除非特别指明,否则 HTTP/1.1 假定所有连接都是持久的。

HTTP/1.1 应用程序必须向报文中显式地添加一个 Connection:close 首部。这是显式指定报文传输完成后关闭连接。

持久连接的限制和规则

  • 发送了 Connection: close 请求首部之后,客户端就无法在那条连接上发送更多的请求了。
  • 只有当连接上所有的报文都有正确的、自定义报文长度时——也就是说,实体主体部分的长度都和相应的 Content-Length 一致,或者是用分块传输编码方式编码的——连接才能持久保持。
  • HTTP/1.1 的代理必须能够分别管理与客户端和服务器的持久连接——每个持久连接都只适用于一跳传输。

管道化连接

在持久连接的基础上,管道化连接更进一步,客户端可以将大量请求放入队列中排队。

副作用是很重要的问题。如果在发送出一些请求数据之后,收到返回结果之前,连接关闭了,客户端就无法百分之百地确定服务器端实际激活了多少事务。

有些事务,比如 GET 一个静态的 HTML 页面,可以反复执行多次,也不会有什么变化。

而其他一些事务,比如向一个在线书店 POST 一张订单,就不能重复执行,不然会有下多张订单的危险。

幂等事务

如果一个事务,不管是执行一次还是很多次,得到的结果都相同,这个事务就是幂等的。

客户端不应该以管道化方式传送非幂等请求,否则,传输连接的过早终止就会造成一些不确定的后果。要发送一条非幂等请求,就需要等待来自前一条请求的响应状态。

大多数浏览器都会在重载一个缓存的 POST 响应时提供一个对话框,询问用户是否希望再次发起事务处理。

正常关闭连接

TCP 连接是双向的。TCP 连接的每一端都有一个输入队列和一个输出队列,用于数据的读或写。

放入一端输出队列中的数据最终会出现在另一端的输入队列中。

完全关闭和半关闭

应用程序可以关闭 TCP 输入和输出信道中的任意一个,或者将两者都关闭了。套接字调用 close() 会将 TCP 连接的输入和输出信道都关闭了,这被称作“完全关闭”

还可以用套接字调用 shutdown() 单独关闭输入或输出信道。这被称为“半关闭”

关闭连接的输出信道总是很安全的。连接另一端的对等实体会在从其缓冲区中读出所有数据之后收到一条通知,说明流结束了,这样它就知道你将连接关闭了。

关闭连接的输入信道比较危险,除非你知道另一端不打算再发送其他数据了。

如果另一端向你已关闭的输入信道发送数据,操作系统就会向另一端的机器回送一条TCP “连接被对端重置” 的报文

HTTP 规范建议,当客户端或服务器突然要关闭一条连接时,应该“正常地关闭传输连接”,

代理

代理就是帮助你(client)处理与服务器(server)连接请求的中间应用,我们称之为 proxy server。

正向代理

对于 client,想要连上 server,如果这个中间过程你知道代理服务器的存在,那这就是正向代理(即这个过程是 client 主动的过程)。

比如我们在 Chrome 上配置 SwitchOmega 工具来使用代理,设置一系列代理规则,可以先将请求发到代理服务器,由代理服务器去发送请求。

比如我们在一些开发工具中,配置 proxy 来设置代理服务器。

比如我们在 ssh 或者一些网络应用的命令行工具时,也可以使用代理选项来使用代理服务器

反向代理

对于 client,直接访问当目标服务器 server(proxy),但目标服务器 server 可能会将请求转发到其它应用服务器 server(app)(即这个过程是 server 主动的过程)。

其对用户是透明的,如用户去访问 example.com,他并不知道该网站背后发生了什么事,一个 API 请求被转发到哪台服务器。

比如我们常用的 nginx,openresty 等软件。用于实现负载均衡和高可用。

代理服务器的部署

常用HTTP代理软件

https://github.com/elazarl/goproxy

出口代理

可以将代理固定在本地网络或者本地计算机的出口点,以便控制本地网络与大型因特网之间的流量。

比如在电脑的浏览器上配置代理规则。

客户端代理

  • 手工配置浏览器

    只能为所有内容指定唯一的一个代理服务器

  • 配置PAC代理

    提供一个 URI,指向一个用 JavaScript 语言编写的代理自动配置文件;客户端会取回这个 JavaScript 文件,并运行它以决定是否应该使用代理

    PAC 文件的后缀通常是 .pac,MIME 类型通常是 application/x-ns-proxy-autoconfig

    每个 PAC 文件都必须定义一个名为 FindProxyForURL(url,host) 的函数,用来计算访问 URI 时使用的适当的代理服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 根据url的scheme不同使用不同的代理
    function FindProxyForURL(url, host) {
    if (url.substring(0, 5) == "http:") {
    return "PROXY http-proxy.mydomain.com:8080";
    } else if (url.substring(0, 4) == "ftp:") {
    return "PROXY ftp-proxy.mydomain.com:8080";
    } else {
    return "DIRECT";
    }
    }

透明代理

网络基础设施可以通过若干种技术手段,在客户端不知道,或没有参与的情况下,拦截网络流量并将其导入代理。

这种拦截通常都依赖于监视 HTTP 流量的交换设备及路由设备,在客户端毫不知情的情况下,对其进行拦截,并将流量导入一个代理。

比如常用的软路由等等

代理相关的首部

VIA首部

Via 首部字段列出了与报文途经的每个中间节点(代理或网关)有关的信息。

报文每经过一个节点,都必须将这个中间节点添加到 Via 列表的末尾。

代理也可以用 Via 首部来检测网络中的路由循环。代理应该在发送一条请求之前,在 Via 首部插入一个与其自身有关的独特字符串,并在输入的请求中查找这个字符串,以检测网络中是否存在路由循环。

请求和响应报文都会经过代理进行传输,因此,请求和响应报文中都要有 Via首部

请求和响应通常都是通过同一条 TCP 连接传送的,所以响应报文会沿着与请求报文相同的路径回传

如果一条请求报文经过了代理 A、B 和 C,相应的响应报文就会通过代理 C、B、A 进行传输。因此,响应的 Via 首部基本上总是与请求的 Via 首部相反

1
2
# 报文流经了两个代理,第一个代理名为 proxy-62.irenes-isp.net ,它实现了 HTTP/1.1 协议.第二个代理名为 cache.joes-hardware.com
Via: 1.1 proxy-62.irenes-isp.net, 1.0 cache.joes-hardware.com

追踪报文

代理服务器可以在转发报文时对其进行修改。可以添加、修改或删除首部,也可以将主体部分转换成不同的格式。

代理变得越来越复杂,开发代理产品的厂商也越来越多,互操作性问题也开始逐渐显现。

通过 HTTP/1.1 的 TRACE 方法,用户可以跟踪经代理链传输的请求报文,观察报文经过了哪些代理,以及每个代理是如何对请求报文进行修改的。

当 TRACE 请求到达目的服务器时,整条请求报文都会被封装在一条 HTTP 响应的主体中回送给发送端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 请求报文
TRACE /index.html HTTP/1.1
Host: www.joes-hardware.com
Accept: text/html


# 响应头部
HTTP/1.1 200 OK
Content-Type: message/http
Content-Length: 269
Via: 1.1 cache.joes-hardware.com, 1.1 p1127.att.net, 1.1 proxy.irenes-isp.net

# 响应主体
TRACE /index.html HTTP/1.1
Host: www.joes-hardware.com
Accept: text/html
Via: 1.1 proxy.irenes-isp.net, 1.1 p1127.att.net, 1.1 cache.joes-hardware.com
X-Magic-CDN-Thingy: 134-AF-0003
Cookie: access-isp="Irene's ISP, California"
Client-ip: 209.134.49.32

代理认证

代理的互操作性

客户端、服务器和代理是由不同厂商构建的,实现的是不同版本的 HTTP 规范

它们支持的特性各不相同,也存在着不同的问题。

Allow首部

通过 HTTP OPTIONS 方法,客户端(或代理)可以发现 Web 服务器或者其上某个特定资源所支持的功能

缓存

当 Web 请求抵达缓存时,如果本地有“已缓存的”副本,就可以从本地存储设备而不是原始服务器中提取这个文档

缓存是 Web 性能优化的一个很重要的优化手段。

Web的缓存可以分为本地缓存,服务器缓存。

HTML5 中引入了应用程序缓存,这意味着 web 应用可进行缓存,并可在没有因特网连接时进行访问。

  • 离线浏览 - 用户可在应用离线时使用它们
  • 速度 - 已缓存资源加载得更快
  • 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。

目前基本上所有的浏览器都支持 HTML5 缓存技术。

大部分缓存只有在客户端发起请求,并且副本旧得足以需要检测的时候,才会对副本进行再验证。

缓存命中和未命中

当请求到达缓存时,会在缓存中检查资源是否存在,如果存在,则直接返回,称为缓存命中,如果不存在,则继续向下游服务器请求资源,称为缓存未命中

缓存的新鲜度检测

缓存对缓存的副本进行再验证时,会向原始服务器发送一个小的再验证请求。如果内容没有变化,服务器会以一个小的 304 Not Modified 进行响应。

只要缓存知道副本仍然有效,就会再次将副本标识为暂时新鲜的,并将副本提供给客户端,这称为 再验证命中

Cache-Control 首部

cache-control 首部是 HTTP1.1 引入的首部,

Cache-Control:max-age max-age 指定了文档的最大使用期限。

从服务器将文档传来之时起,可以认为此文档处于新鲜状态的秒数。最大的合法生存时间为 484200 秒

Cache-Control: max-age=0 服务器可以请求缓存不缓存文档,让每次访问都请求到服务器上。

Expires 首部

If-Modified-Since 首部

If-Modified-Since 是一个请求头部,他用于指定内容修改时间。

将这个首部添加到 GET 请求中去,If-Modified-Since:Date

  • 如果自指定日期后,文档被修改了,通常 GET 就会成功执行,携带新首部的新文档会被返回给缓存,新首部中包含了新的过期时间。
  • 如果自指定日期后,文档没被修改过,条件就为假,

If-None-Match 首部

If-None-Match 是一个请求头部,用于指定请求文档的版本号。

1
If-None-Match: W/"43cd67b71ec96ce713c66db2315e23cf"

当缓存向原始服务器请求时,就会检查这个版本号,响应报文中的 etag 就是资源的版本号。

Last-Modified 首部

If-Modified-Since 首部可以与 Last-Modified 服务器响应首部配合工作。

原始服务器会将最后的修改日期附加到所提供的文档上去。