抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

概述

HTTP的概念

  1. HTTP又名超文本传输协议,是在应用层上的协议
  2. 基于TCP协议
  3. 通过请求和响应进行通信
  4. 无状态的协议:HTTP本身不保存任何状态

URI和URL

URI 是 Uniform Resource Identifier(统一资源标识符)的缩写;URL 是 Uniform Resource Locator(统一资源定位符)的缩写。

其中,URI 用字符串标识某一互联网资源,而 URL 表示资源的地点。URL是URI的子集。

绝对URI的格式如下:

格式

  1. 请求:请求行(请求方法 路径 HTTP版本)、请求头、请求体
  2. 响应:响应行(版本 状态码 状态说明)、响应头、响应体

请求方法

概览

HTTP提供了很多的请求方法:

  • GET:请求一个指定资源的表示形式.使用GET的请求应该只被用于获取数据.
  • POST:用于将实体提交到指定的资源,通常导致在服务器上的状态变化或副作用
  • PUT:用请求有效载荷替换目标资源的所有当前表示
  • DELETE:删除指定的资源
  • HEAD:请求一个与GET请求的响应相同的响应,但没有响应体
  • CONNECT:建立一个到由目标资源标识的服务器的隧道。
  • OPTIONS:用于描述目标资源的通信选项。
  • TRACE:沿着到目标资源的路径执行一个消息环回测试。
  • PATCH:用于对资源应用部分修改。

请求方法的性质

  1. 幂等性:同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的

    具有幂等性的方法:GET HEAD OPTIONS PUT DELETE

  2. 安全性:不会修改服务器的数据的方法

    具有安全性的方法:GET HEAD OPTIONS

GET和POST请求

其中,GET和POST在浏览器上最为常见,但在一些表现上有一些区别:

  1. 缓存:GET请求可以被浏览器主动缓存,而POST请求一般不会。

  2. 记录:GET请求可以被保存在浏览记录里,也可以被收藏,而POST请求不会。

  3. 幂等:GET请求是幂等的,而POST请求不是幂等的。也就是说,回退POST请求可能会导致资源重复创建。

  4. 参数:GET请求一般只支持URL Query,而POST请求则支持JSON、FormData等多种类型。此外,GET请求数据会暴露在URL上,不安全。

状态码

HTTP通过状态码来告知请求的结果。

类型状态码解释
1xx:请求已接受,继续操作100继续:请求已接受,继续处理
101切换协议
2xx:请求已成功200成功:请求已成功
201已创建:服务器成功创建了新的资源
202已接受:服务器已接受请求,但并未处理
204无内容:请求已成功,但服务器未返回任何数据
206部分请求:已请求部分数据
3xx:重定向301永久移动:请求的网页已永久移动到新位置,会引导浏览器修改缓存、历史记录和书签
302临时移动:服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置
304未修改:服务器未修改自上次请求后的资源
4xx400错误请求:请求包含语法错误或无法完成
401未授权:客户端未通过身份验证
403禁止:服务器拒绝请求
404未找到:服务器找不到请求的网页
405方法禁用:服务器不允许请求使用GET方法
408请求超时:服务器等候请求时发生超时
5xx500服务器错误
502错误网关:服务器收不到上游服务器的响应
503服务不可用:服务器当前不能处理请求
505HTTP版本不受支持:服务器不支持请求中所使用的HTTP版本

基于Socket简单模拟HTTP/1.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const net = require('net')
const server = net.createServer();

server.on('connection',(socket) => {
socket.on('data', (buffer) => {
console.log("接收请求")
socket.write('HTTP/1.1 200 OK')
socket.write('\n')
socket.write('Content-Length: 1')
socket.write('\n\n')
socket.write('0')
//socket.end()
})

socket.on('close', () => {
console.log("关闭连接")
})
})

server.on('error', console.error)
server.listen(8888,"0.0.0.0", () => console.log('服务器监听在8888端口'))

HTTP连接

短连接

在最早期的模型中,每个连接都是独立完成的,即一个HTTP请求对应一个TCP连接。

请求过程:

  1. 建立TCP连接
  2. 发送请求
  3. 响应数据
  4. 关闭TCP连接

持久连接

短连接模型会带来一些问题:

  • TCP连接的建立与释放都需要握手,这会带来一定开销
  • TCP刚开始采用慢启动策略,这导致每个请求刚开始的数据发送速率很慢。
  • 一般电脑上最大TCP连接的数量其实是有限的。

HTTP 1.1 中提出了持久连接的概念,使得多个HTTP请求共有有限的TCP连接(一般一个域名开启6-8个连接)。复用TCP连接可以避免握手带来的时间浪费,也可以减少慢启动导致传输速率低的问题。

设置长连接:

1
Connection: keep-alive

HTTP 1.1如果不显式声明关闭长连接,则默认都是开启长连接的。大部分服务器为了兼容性,还是会带上 Connection: keep-alive

关闭长连接:

1
Connection: close

长连接也会带来问题:

  • 空闲时仍然会消耗服务器资源
  • TCP连接占满后,剩余的连接都会被阻塞。解决这个问题的办法是采用不同的域名来传输数据

使用Socket编程体验长连接

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const net = require('net')
const server = net.createServer();

server.on('connection',(socket) => {
socket.on('data', (buffer) => {
console.log("接收请求")
socket.write('HTTP/1.1 200 OK')
socket.write('\n')
socket.write('Content-Length: 1')
socket.write('\n\n')
socket.write('0')
})

socket.on('close', () => {
console.log("关闭连接")
})
})

server.on('error', console.error)
server.listen(8888,"0.0.0.0", () => console.log('服务器监听在8888端口'))

响应结束后,不会出现"关闭连接"的日志。如果加上 Connection: close,则请求后出现关闭连接的日志。

如果在响应完后,服务器主动关闭连接,那么连接也是可以关闭的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server.on('connection',(socket) => {
socket.on('data', (buffer) => {
console.log("接收请求")
socket.write('HTTP/1.1 200 OK')
socket.write('\n')
socket.write('Content-Length: 1')
socket.write('\n\n')
socket.write('0')
socket.end()
})

socket.on('close', () => {
console.log("关闭连接")
})
})

在服务器和浏览器中,持久连接一般有个超时时间(默认似乎是1分钟),修改超时连接:

  • nginx服务器设置 keepalive_timeout
  • 通过 Keep-Alive 头部

不过,这些都只是约定,连接是可以随时被任何一方关闭的。

Content-Length

通过之前的Socket编程可以发现,HTTP基于TCP(3.0之前),是在流里发送接受数据。要想正确接收报文,就需要想办法从流里请求行、请求头、请求体(响应是类似的)。其中:

  • 请求行/状态行只有一行,只需要读到 \n 就行
  • 请求头/响应头有很多行,但头部和内容中间隔了空行。这意味着只需要读到两个\n\n就行。
  • 请求/响应体的内容格式不确定,很难像上面一样使用界定符。一种界定方法是接受内容直到服务器关闭连接,另一种就是用户指定响应体的长度。

对于持久连接,”接受内容直到服务器关闭连接“这种办法显然是行不通的,因为理论上短期内连接不会被关闭。

Content-Length响应头表示了HTTP内容的长度(如果开启了内容压缩,则是压缩后的长度)。

Content-Length需要跟响应体的内容长度完全一致。否则:

  • 若Content-Length>内容长度,则服务器会一直等待内容,直到超时
  • 若Content-Length<内容长度,则服务器会截断内容。如果开启了持久连接,截断点后面的内容会进入下一个响应体(可能会导致解析错误)。

如果不确定Content-Length,可以设置分块传输(后面提到)

流水线

在同一条长连接上发出连续的请求,而不用等待应答返回。

由于流水线机制需要解决的问题很多,且浏览器默认都不开启流水线方式,此处不详细展开。

多路复用

HTTP 2.0 增加了一个二进制分帧层,将原报文分成若干个帧,不同请求的各个帧可以在一条TCP连接上传输。

缓存

浏览器缓存策略

内容协商

当我们访问例如微软 等这样的国际网站时,你会发现,它会自动跳转到它的中文网站了。这样,用户就不需要手动修改这个网站的语言。

那么,微软是怎么知道用户期望访问的语言呢?这就要提到HTTP的内容协商机制了。

服务端驱动型内容协商

除了考虑到网站的语言,还要考虑到用户期望使用什么样的字符编码(GBK、UTF-8等),还要考虑到用户期望返回什么样的内容(返回HTML、CSS还是JS)。

内容协商涉及到了一系列的请求头和响应头:

协商内容请求头响应头
响应体的MINE类型(JSON、HTML、JS等)AcceptContent-Type
响应体的字符集Accept-CharsetContent-Type
响应体的内容压缩方式Accept-EncodingContent-Encoding
响应体的内容对应的显示语言Accept-LanguageContent-Language
浏览器和操作系统信息User-Agent-

示例:

1
2
3
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6

考虑到用户可能期望多种情况,在发送请求时,可以指定多条内容,并通过 q=xxx 来设置优先级。
详细可参考: https://developer.mozilla.org/zh-CN/docs/Glossary/Quality_values

Vary响应头

在复杂的情况下,一个HTTP请求往往需要经过多个代理服务器。为了提高性能,一些代理服务器会对内容进行缓存(如CDN节点)。但是,内容协商机制使得同一个url可能会返回不同的内容。而缓存代理服务器往往根据url来生成缓存,这会使得用户访问到不正确的内容。

Vary响应头用来告诉客户端,服务器使用了什么请求头来进行内容协商:

1
2
Vary: * // 所有请求都是唯一的,不缓存
Vary: <header-name>, <header-name>, ... //根据指定的请求头判断是否需要缓存

这样,缓存服务器就能根据这个请求头的信息,将URL和上面请求头的值作为缓存的key。

典型的案例:

假如一个网站为pc端和手机端提供了不同的版本,设置下面的响应头就可以让缓存服务器分别缓存PC端和手机端资源:

1
Vary: User-Agent

从而使不同设备的用户能够正确访问到对应端的资源。

数据传输

内容编码

在网络传输中,传输的内容越小,往往性能越好。所以,为了减小传输的数据量,压缩响应体是很有必要的。

内容编码是端到端的压缩方式,即服务器生成压缩后的数据,中间服务器不做任何修改,然后返回给客户端。

内容编码主要由下面的一对HTTP头组成。

很明显,这也是需要协商的内容,因为不同的浏览器和服务器支持的压缩方式是不同的。

内容请求头响应头
响应体的内容压缩方式Accept-EncodingContent-Encoding

常见的压缩格式:

  • gzip(GNU zip):采用 Lempel-Ziv coding(LZ77)压缩算法,以及 32 位 CRC 校验的编码方式
  • compress(UNIX 系统的标准压缩):这种方式已经被大部分浏览器弃用
  • deflate(zlib):采用zlib结构和deflate压缩算法
  • br:表示采用 Brotli 算法的编码方式
  • identity(不进行编码)

通常,内容压缩只针对文本文件。图片、视频等类型的文件往往是已经高度压缩的,重复压缩效果微乎其微还浪费时间。

传输编码

Transfer-Encoding 用来指定传输时,使用什么编码方式。这个响应头是两个服务器之间的传输编码,不同服务器之间可以使用不同的传输编码。

Content-Encoding 是客户端到服务端的,Transfer-Encoding 是两个服务器之间的。

  • gzip:GNU zip
  • compress:UNIX 系统的标准压缩
  • deflate:zlib
  • identity:不进行编码
  • chunked:分块传输

分块传输

当响应体的内容不确定时,可以使用分块传输。

Transfer-Encoding: chunked 可开启分块传输。此时不需要提供 Content-Length

开启分块传输后,响应体由不同的块构成。

  • 每一块的第一行表示当前块的长度(用 16 进制数字表示),后面表示块的内容。
  • 长度行和内容结束后,以CRLF作为分隔符。
  • 当块的长度为0时,表示这是最后一个块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const net = require('net')
const server = net.createServer();

server.on('connection',(socket) => {
socket.on('data', (buffer) => {
console.log("接收请求")
socket.write('HTTP/1.1 200 OK\n')
socket.write('Transfer-Encoding: chunked\n')
socket.write('Connection: close\n')
socket.write('\n')
// 第一个 chunk
socket.write('5\r\n')
socket.write('hello\r\n')
// 第二个 chunk
socket.write('5\r\n')
socket.write('world\r\n')
// 第三个 chunk
socket.write('0\r\n')
socket.write('\r\n')
})

socket.on('close', () => {
console.log("关闭连接")
})
})

server.on('error', console.error)
server.listen(8888,"0.0.0.0", () => console.log('服务器监听在8888端口'))

请求后,得到响应 helloworld

范围请求

在下载内容时,如果要下载的文件非常大,一旦连接中断了,就得从0开始重新下载。借助范围请求技术,我们可以实现断点续传,多线程下载的功能。

服务器是否支持

要判断服务器是否支持范围请求,可以先向服务器发送一个HEAD请求,如果出现下面的内容,则支持范围请求:

1
Accept-Ranges: bytes

如果没有这个响应头或者值为 none,则服务器可能不支持这个功能。

发送范围请求

根据上面请求的 Content-Length 可以获得数据的大小。

发送请求时,头部携带下面的请求头,就可以发送范围请求:

1
Range: bytes=0-1023

其中 0-1023 表示范围长度,支持 0-50, 100-150 这样的多重范围。

范围请求的响应

如果服务器不支持范围请求,则返回200状态码;如果范围越界,则返回416状态码;否则返回206状态码。

对于单一范围,服务器返回下面的内容:

1
2
3
4
Content-Range: bytes 0-1023/146515
Content-Length: 1024

内容

Content-Range 返回范围信息和整个文件的大小;Content-Length 范围选中范围的长度。

对于多重范围,则返回下面的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 282

--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 0-50/1270

<!doctype html>
<html>
<head>
<title>Example Do
--3d6b6a416f9b5
Content-Type: text/html
Content-Range: bytes 100-150/1270

eta http-equiv="Content-type" content="text/html; c
--3d6b6a416f9b5--

其中响应头的内容类型为 multipart/byteranges,响应体各个部分会以响应头中指定的分界线为界。每部分开头会提供内容类型和范围信息。

数据过期处理

在下载过程中,数据可能会更新,这样就会导致下载的内容不正确。

在前面缓存部分,我们可以通过内容的ETag或Last-Modified 来判断使用缓存还是重新下载数据。

在这里,我们可以使用 If-Range 来对上面的ETag或Last-Modified进行验证,如果一致则使用 Range 属性,否则重新开始下载:

1
If-Range: ETag或Last-Modified的值

认证与安全

Cookie与状态管理

介绍

HTTP本身是无状态的,但浏览器实现了一套Cookie机制,可以使得HTTP能够携带一些状态。

内容请求头响应头
CookieCookieSet-Cookie

Cookie的构成

基本要素

  • 名称:唯一标识cookie 的名称。浏览器中不区分大小写。
  • :存储在cookie 里的字符串值。这个值必须经过URL编码。

作用范围

在作用范围内的Cookie将被发送,否则不会被发送。

安全

  • 安全限制(http-only):只允许通过HTTP协议访问cookie,不允许通过JavaScript 访问cookie。
  • 跨域限制(samesite):当通过A站点访问B站点,是否向B发送Cookie。支持三个值 strictlaxnone
    • strict:禁止发送Cookie。但是如果用户在A站点点击了Github 的链接,Github就收不到Cookie了。
    • lax: 较为宽松,允许通过链接、预加载和GET表单访问时发送Cookie,其他情况不允许发送。
    • none:不限制。
  • 同源定义(same-party):支持用户定义哪些站点是同源的。需要同时配置 secure属性。

SameSite 详细参考 http://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html
SameParty 详细参考 https://blog.csdn.net/frontend_nian/article/details/124221944

生存时间

  • 过期时间(expires):这个值是GMT 格式(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定删除cookie 的具体时间。这样即使关闭浏览器cookie也会保留在用户机器上。把过期时间设置为过去的时间会立即删除cookie。

  • 生存时间(max-age):这是一个相对时间。如果设置了这个值,则在这个时间段内cookie 不会被删除。

Cookie的存储和访问

在Set-Cookie请求头中,格式如下:

1
Set-Cookie: 名称=值; 属性[=值]; ....

document中只能以 名称=值; 的字符串访问。

Session 技术

由于Cookie 会放在HTTP头部,所以Cookie不适合存储大量数据(还有4KB的限制)。
而且,在未开启https的情况下是明文传播,所以仅使用Cookie技术可能会存在用户信息泄露的问题。

Session 是另外一项技术,可以将用户的一些认证信息保存在服务器中。

Session需要和Cookie配合使用。在开始时,服务端为当前的会话生成一个SessionID,并存放在Cookie中。服务器根据SessionID就可以判断当前会话的请求(比如是否登录)。

为了保证安全,SessionID一般是一个随机的值。

HTTPS

TODO

跨域资源共享

浏览器同源策略

访问来源与防盗链

TODO

服务器与集群

虚拟主机技术

有时候为了节省服务器资源,希望在同一个一个服务器(同一个公网IP)上开设不同的网站,并指向不同的域名。

但是,在请求时,服务器获取不到请求的域名,因为在DNS解析的过程中,域名已经被解析成IP了。所以,不同的域名最后解析后得到的请求报文是一样的,服务器是无法区分的。

因此,HTTP提供了一个名为 Host 的请求头,来告诉服务器正在请求的域名,以及端口号。

CDN

TODO

发展中的HTTP

HTTP/0.9

HTTP/0.9 是于 1991 年提出的,主要用于学术交流,需求很简单。

整个过程:

  • 建立 TCP 连接
  • 发送请求行
  • 读取对应的 HTML 文件,返回 ASCII 字符流
  • 断开 TCP 连接

特点:

  • 只有一个请求行,没有请求头和请求体
  • 服务器没有返回头信息
  • 仅支持 ASCII 编码

HTTP/1.0

由于万维网的高速发展,HTTP/0.9 很明显就不能适应发展了:

  • 浏览器展示的不仅是 HTML,还有 CSS、JS、图片等
  • 文件的编码多种多样

为了解决这样问题,HTTP/1.0 引入了请求头和响应头信息。这样,用户就可以在请求时告诉浏览器期望得到的数据类型:

1
2
3
4
Accept: text/html
Accept-Encoding: gzip, deflate, br
Accept-Charset: utf-8
Accept-Language: zh-CN,zh

同时,服务器返回时,也会告诉浏览器响应数据的类型:

1
2
Content-Encoding: br
Content-Type: text/html; charset=UTF-8

这样,浏览器可以按照 br 方法解压文件,使用 UTF-8 编码解析文件。

此外,HTTP 1.0 还新增了一些特性:

  • 状态码机制:告诉浏览器响应的一些状态
  • 缓存机制:减轻服务器压力
  • 用户代理:将浏览器本身的信息发送给服务器,方便服务器统计客户端的信息

HTTP/1.1

随着需求的不断变化,技术的不断更新,很快,HTTP/1.0 也不能满足需求了,所以 HTTP/1.1 又做了大量的更新。

  1. 改进持久连接:可以在同一个 TCP 连接上可以传输多个 HTTP 请求。只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。这样就减少了建立连接上的耗时。

注:浏览器为每个域名最多同时维护 6 个 TCP 持久连接

  1. 虚拟主机支持:HTTP/1.1 的请求头中增加了 Host 字段,可以表示当前请求的域名地址。这样服务器可以根据不同的域名来设置不同的虚拟主机,使得同一个物理主机可以运行多个网站。
  2. Chunk transfer 机制:HTTP/1.0 需要在响应头中设置完整的数据大小,这对于动态生成的内容是不利的。在 HTTP/1.1 中,可以通过 Chunk transfer机制,将内容分成若干个任意大小的数据块,发送时附上数据块的长度,最后使用零长度的数据块作为传输完成的标志。
  3. 客户端 Cookie 和安全机制:方便鉴权和用户认证等

目前,HTTP/1.1 是使用最为广泛的 HTTP 版本。

HTTP/2.0

尽管 HTTP/1.1 已经很实用了,但是仍然存在 带宽利用低 的问题。原因如下:

带宽 是指每秒最大能发送或者接收的字节数。我们把每秒能发送的最大字节数称为上行带宽,每秒能够接收的最大字节数称为下行带宽。

  1. TCP 的慢启动:刚开始传输的时候会使用较慢的速度去发送数据,然后逐渐加快速度。这是 TCP 减少拥塞的一种策略。但是,很多资源本来就不大,使用慢启动便会比正常情况耗费不少的时间。
  2. 多个 TCP 连接竞争带宽,但没有优先级:当 TCP 连接很多,带宽快不足的时候,便会减慢传输数据的速度。有些 TCP 连接下载的是一些关键资源,有些 TCP 连接下载的是一些次要资源,但无法让各个连接之间协商哪个先下载。这样混在一起,就会影响关键资源的下载。
  3. 队头拥塞:尽管 HTTP/1.1 中可以支持多个资源共用一个 TCP 连接,但这个 TCP 连接只能以串行的方式进行请求,也就是说,第一个请求发送完了,才能发送第二个请求。但是有时候第一个请求被莫名其妙的阻塞了(如:发生了超时重传),这就影响到了后面的请求。

针对上面这样的问题,HTTP/2.0 提出了多路复用的机制。

  1. 一个域名使用一个 TCP 长连接。这样整个资源的下载只有一次慢启动的过程。
  2. 每个请求传输时分成多个帧,每个帧增加了对应的 ID,这样可以随时把请求发送给浏览器。
  3. 服务器收到信息后,可以按照优先级将响应返回给用户,同样的,响应也分帧,且带上 ID。最后将各个 ID 的请求合并起来就可以得到完整的请求了。这样,服务器可以优先处理关键资源的请求。

HTTP/2.0 增加了一个 二进制分帧层 来实现多路复用。发送请求时,请求数据经过二进制分帧层处理之后,会被转换为一个个带有请求 ID 编号的帧,通过协议栈将这些帧发送给服务器。响应数据时,二进制分帧层会将这些响应数据转换为一个个带有请求 ID 编号的帧,经过协议栈发送给浏览器,最后浏览器将其拼接成完成的响应数据。

不过,HTTP/2.0 的语义和 HTTP/1.1 基本是一样的,这样并不会带来太大的切换成本。

HTTP/2.0 还具有一些其他的特性:

  1. 请求优先级:发送请求时标上该请求的优先级,服务器可以处理优先级高的请求。
  2. 服务器推送:服务器可以将数据直接推送到浏览器。比如请求 index.html,就可以把相关的 cssjs 也推送过去。
  3. 头部压缩:HTTP/2 对请求头和响应头也进行了压缩

HTTP/3.0

  1. QUIC协议,基于UDP实现。
  2. 实现了多管道的拥塞控制,避免队头堵塞。
  3. 支持快速握手。

参考

  1. 图解HTTP
  2. JavaScriipt高级程序设计(第四版)
  3. HTTP权威指南
  4. MDN

评论