HTTP 的知识新手也许感觉没什么重要的,但是随着学习的加深,它就会变得越来越重要,限制人的技术的高度和深度,例如一个 HTTP 请求失败的分析,和后端讨论方案等等,都会让你畏手畏脚,底气不足.

五层网络模型

五层网络模型

我们一般在讲解网络信息传输的时候,都会套用一个五层模型来诠释.在每台电脑,每台服务器上都可以用它来维护我们的网络传输.

低三层

比较底层,靠近硬件,我们不用过于关心了解

物理层

主要作用是定义物理设备如何传输数据;
简单来说就是我们电脑的硬件,网卡,光缆

数据链路层

在通信的实体间建立数据链路连接;
就是帮我们在硬件之间创建连接的比较底层的软件服务,最基础的 0101 的二进制传播

网络层

为数据在结点之间传输创建逻辑链路;
就是比如我们的电脑如何寻找百度的服务器,和它建立连接

传输层

主要有两个协议 tcp 和 udp.一般我们用 tcp 协议的比较多,因为它更加安全稳定

传输层向用户提供可靠的端到端(End-to-End)服务,就是我们的电脑和百度的服务器建立了连接后,我们之间如何去传输数据,我们传输的数据可能很小可能很大,我们要分包分片,传输过去后要进行数据的组装等等这些都是在传输层定义的.

传输层向高层屏蔽了下层数据通信的细节,我们输入一个 url,创建一个 ajax 等等,都是其中网络如何分包组装,如何保证数据的完整性等等这些都是 http 不用关心的事

应用层

http 协议就在这一层

为应用软件提供了很多服务

构建于 TCP 协议之上

屏蔽网络传输相关细节

HTTP 协议历史

HTTP/0.9

比较简单,只有一个命令 GET

没有 header 等描述数据的信息

服务器发送完毕, 就关闭 TCP 连接

HTTP/1.0

增加了很多命令 如 POST ,HEAD , PUT

增加了 status code 和 header

多字符集支持、多部分发送、权限、缓存等

HTTP/1.1

持久连接,http 请求后 tcp 连接可以不立即关闭

pipeline,就是 http 请求的在服务端在以前前一个请求处理时间比较久,后一个比较快,都是服务端处理是按顺序的,问题就像 JavaScript 的事件的并行和串行,所以在 http1.1 做了优化

增加 host 和其他一些命令(一台物理服务器可以开多个 web 服务)

HTTP2

所有数据以二进制传输,在 http1.1 里.大部分的数据是字符串格式的

同一个连接里面发送多个请求不再需要按照顺序来(并行的效率)

头信息压缩以及推送等提高效率的功能,不必要的头信息会省略,还有服务器可以主动发起一些数据传输的,比如可以在请求 HTML 文档的时候主动把相关的图片,js 和 css 推送到客户端

TCP 三次握手

F40mp8.md.png http 协议不存在连接的概念,只有请求和响应的概念,它是进行在一个 TCP connection 中, tcp 连接可以发送多个 http 请求. F4wOT1.png F4017n.md.png 第一次握手
建立连接。客户端发送连接请求报文段,将 SYN 位置为 1,Sequence Number 为 x;然后,客户端进入 SYN_SEND 状态,等待服务器的确认;

第二次握手
服务器收到 SYN 报文段。服务器收到客户端的 SYN 报文段,需要对这个 SYN 报文段进行确认,设置 Acknowledgment Number 为 x+1(Sequence Number+1);同时,自己自己还要发送 SYN 请求信息,将 SYN 位置为 1,Sequence Number 为 y;服务器端将上述所有信息放到一个报文段(即 SYN+ACK 报文段)中,一并发送给客户端,此时服务器进入 SYN_RECV 状态;

第三次握手
客户端收到服务器的 SYN+ACK 报文段。然后将 Acknowledgment Number 设置为 y+1,向服务器发送 ACK 报文段,这个报文段发送完毕以后,客户端和服务器端都进入 ESTABLISHED 状态,完成 TCP 三次握手。

为什么要三次握手

为了防止已失效的连接请求报文段突然又传送到了服务端,因而让服务端创建无用的连接。

具体例子:“已失效的连接请求报文段”的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。

TCP 四次挥手

F4BEDJ.md.png 第一次分手
主机 1(可以使客户端,也可以是服务器端),设置 Sequence Number,向主机 2 发送一个 FIN 报文段;此时,主机 1 进入 FIN_WAIT_1 状态;这表示主机 1 没有数据要发送给主机 2 了;

第二次分手
主机 2 收到了主机 1 发送的 FIN 报文段,向主机 1 回一个 ACK 报文段,Acknowledgment Number 为 Sequence Number 加 1;主机 1 进入 FIN_WAIT_2 状态;主机 2 告诉主机 1,我“同意”你的关闭请求;

第三次分手
主机 2 向主机 1 发送 FIN 报文段,请求关闭连接,同时主机 2 进入 LAST_ACK 状态;

第四次分手
主机 1 收到主机 2 发送的 FIN 报文段,向主机 2 发送 ACK 报文段,然后主机 1 进入 TIME_WAIT 状态;主机 2 收到主机 1 的 ACK 报文段以后,就关闭连接;此时,主机 1 等待 2MSL 后依然没有收到回复,则证明 Server 端已正常关闭,那好,主机 1 也可以关闭连接了。

为什么要四次分手

TCP 协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP 是全双工模式,这就意味着,当主机 1 发出 FIN 报文段时,只是表示主机 1 已经没有数据要发送了,主机 1 告诉主机 2,它的数据已经全部发送完毕了;但是,这个时候主机 1 还是可以接受来自主机 2 的数据;当主机 2 返回 ACK 报文段时,表示它已经知道主机 1 没有数据发送了,但是主机 2 还是可以发送数据到主机 1 的;当主机 2 也发送了 FIN 报文段时,这个时候就表示主机 2 也没有数据要发送了,就会告诉主机 1,我也没有数据要发送了,之后彼此就会愉快的中断这次 TCP 连接。

URI、URL、URN

URI

URI 包含 URL 和 URN

Uniform Resource Identifier / 统一资源标识符

用来唯一标识互联网上的信息资源

URL

Uniform Resource Locator / 统一资源定位器

http://user:pass@host.com:80/path?query=string#hash

协议: http://

身份(现在基本不用): user:pass@

服务器地址: host.com (本质用的是 ip)

端口: :80 (默认是 80, 所以地址栏一般都是看不到)

路由: /path

搜索参数: ?query=string

文档的不同 hash: #hash

URN

永久统一资源定位符

在资源移动之后还能被找到

目前还没有非常成熟的使用方案

HTTP 报文

F4BtUI.md.png

http 请求报文

http 请求报文是指客户端到服务器端的消息,客户端通过发送 http 请求向服务器请求对资源的访问。包括三个部分:请求行、请求头部、请求数据。请求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这几种。

  1. 请求行:包含请求方法、uri 和协议的版本,用空格分隔,例如:GET/sample.jsp HTTP/1.1
  2. 请求头部:包含有关客户端环境及请求正文的信息,如请求正文长度、浏览器所用编码格式等,例如;
Accept:image/gif.image/jpeg.*/*

Accept-Language:zh-cn

Connection:Keep-Alive

Host:localhost

User-Agent:Mozila/4.0(compatible:MSIE5.01:Windows NT5.0)

Accept-Encoding:gzip,deflate
1
2
3
4
5
6
7
8
9
10
11
  1. 请求数据:即客户端发送给服务器的内容,可为空(GET 请求就没有请求数据),例如:username=jinqiao&password=1234, 请求头部和请求数据之间必须有空行,用以区分。

HTTP 响应报文

http 应答报文是指服务器回应 http 请求,发送给客户端的消息。也包括三个部分:状态行、响应头部、响应数据。

  1. 状态行:协议版本、状态码、简要描述,例如:HTTP/1.1 200 OK
  2. 响应头部:必须指明 Content-Type,其他可选,例如:Content-Type: text/plain
  3. 响应数据:即服务器回应客户端的内容。

常见状态码

  • 1xx:指示信息--表示请求已接收,继续处理。
  • 2xx:成功--表示请求已被成功接收、理解、接受。
  • 3xx:重定向--要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误--请求有语法错误或请求无法实现。
  • 5xx:服务器端错误--服务器未能实现合法的请求。

CORS 跨域请求的限制与解决

不管有没有跨域, 客户端发起请求, 服务端都会接受你的请求并且返回内容.

跨域是浏览器来做的! 浏览器接受到服务器返回的内容, 没看到请求头中没有设置 Access-Control-Allow-Origin 就报错, 不展示给你结果!

一句话: 服务端没有跨域, 只有浏览器有同源策略

解决跨域

CORS

增加 Access-Control-Allow-Origin 响应头

response.writeHead(200, {
  'Access-Control-Allow-Origin': '*'
});
1
2
3

JSONP 处理跨域

利用 img script link 三个标签可以发起跨域请求.

CORS 预请求

其他不被允许的都要经过预请求 跨域时, 允许的方法只有 GET HEAD POST.

跨域时, 允许的 Content-Type 有 text/plain multipart/form-data application/x-www-form-urlencoded

其他限制:

请求头限制

XMLHttpRequestUpload 对象均没有注册任何事件监听器

请求中没有使用 ReadableStream 对象

增加 'Access-Control-Allow-Headers': 'X-Test-Cors'

浏览器对跨域请求的预请求

会先发送一个 OPTIONS 请求, 服务端根据不同的 methods 进行不同的操作.

利用 OPSTIONS 请求获得服务端认可, 实际发送 POST 请求 F4r96U.md.png

设置 Methods

F4rim4.md.png

设置 Max-Age

'Access-Control-Max-Age': '1000', 表示 1000 s 不需要再发预请求.

const http = require('http');

http
  .createServer(function(request, response) {
    console.log('request come', request.url);
    response.writeHead(200, {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'X-Test-Cors',
      'Access-Control-Allow-Methods': 'POST, PUT, DELETE',
      'Access-Control-Max-Age': '1000'
    });
    response.end('123');
  })
  .listen(8887);

console.log('server run 8887');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Cache-Control

F4rQne.md.png

可缓存性

public, 表示所有 http 经过的任何地方(代理)都可以缓存

private, 只有发起请求的浏览器才可以缓存

no-cache, 告诉浏览器、缓存服务器,不管本地副本是否过期,使用资源副本前,一定要到源服务器进行副本有效性校验

no-cache 从字面意义上很容易误解为不缓存,但是 no-cache 代表不缓存过期的资源,缓存会向服务器进行有效处理确认之后处理资源

到期

max-age=< seconds >, 缓存到多少秒过期

s-maxage=< seconds >, 会代替 max-age, 只有在代理服务器才会生效, 浏览器还是会读取 max-age

max-stale=< seconds >, 发起请求方主动带的头信息, 代表即便缓存已经过期, 在 max-stale 设置的时间内还是会读取缓存, 浏览器用不到.

重新验证

must-revalidate, 设置了 max-age 并且已经过期了, 那么会重新发起请求到原服务端获取数据看是否真的过期, 而不是直接使用缓存

proxy-revalidate,同 must-revalidate 只不过用在代理服务器上.

其他

no-store, 永远都要去服务器拿新的内容,no-store 才是真正的不进行缓存.

no-transform, 用于缓存服务器中,告诉代理服务器不要随意改动内容

前端最常见的是更新文件时按文件内容设置 hash 类似 72.63f2ae81.js, 这样如果文件内容改变, hash 就变化了, 浏览器就会拿到新的文件.

Last-Modified 和 Etag

验证头

  • Last-Modified
  • Etag

Last-Modified

根据上次修改时间

配合 If-Modified-Since 或者 If-Unmodified-Since 使用

也就是浏览器下次发起请求会带上 If-Modified-Since 头信息, 值就是 服务器返回 Last-Modified 的值.

对比上次修改时间以验证资源是否需要更新

Etag

根据数据签名 (常用的是 hash 计算)

配合 If-match 或者 If-Non-Match 使用

也就是浏览器下次发起请求会带上 If-match 或者 If-Non-Match 头信息, 值就是服务器返回的 Etag 的值.

对比资源的签名判断是否使用缓存

设置

设置从缓存读取的话就不重新发送内容 F4smUs.md.png

if (request.url === '/script.js') {
  const etag = request.headers['if-none-match'];
  if (etag === '777') {
    response.writeHead(304, {
      'Content-Type': 'text/javascript',
      'Cache-Control': 'max-age=2000000, no-cache',
      'Last-Modified': '123',
      Etag: '777'
    });
    response.end('');
  } else {
    response.writeHead(200, {
      'Content-Type': 'text/javascript',
      'Cache-Control': 'max-age=2000000, no-cache',
      'Last-Modified': '123',
      Etag: '777'
    });
    response.end('console.log("script loaded")');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

服务器通过 Set-Cookie 这个 header 设置到浏览器里面, 保存在浏览器

下次同域的请求中就会带上这个 cookie

键值对, 可以设置多个

max-age 和 expires 设置过期时间

secure 只在 https 的时候发送

设置 http-only 后, 无法通过 document.cookie 访问

设置过期时间

'Set-Cookie': ['id=123;max-age=2']

设置 domain 可以让 test.a.com 访问到 a.com 的 cookie.

const http = require('http');
const fs = require('fs');
http
  .createServer(function(request, response) {
    console.log('request come', request.url);

    const host = request.headers.host;

    if (request.url === '/') {
      const html = fs.readFileSync('test.html', 'utf-8');

      if (host === 'test.com') {
        response.writeHead(200, {
          'Content-Type': 'text/html',
          'Set-Cookie': ['id=123;max-age=2;domain=test.com']
        });
      }
      response.end(html);
    }
  })
  .listen(8888);

console.log('server run 8888');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

session

session 有很多种实现方式, 经常用的是 cookie 而已.

比如服务器可以根据 cookie 里面的一个唯一 key 然后去查用户信息, 只要能定位用户, 就是 session.

不一定要 cookie, 也可以 js 写在 header 中.

HTTP 长连接

在一个 tcp 连接发 多个 http 请求.

长连接会复用 tcp 连接, chrome 并发是 6 个.

Connection: keep-alive 表示长连接开启.

'Connection': 'close' 关闭常长连接:

数据协商

客户端给服务端发送请求的时候, 客户端会在请求头中声明我希望拿到的数据格式及数据相关的限制是怎样的,

服务端会根据客户端请求, 做出判断, 再具体返回什么.

分类

请求 和 返回

请求

Accept

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8

指定想要的数据类型(根据 MIME 类型告诉服务端想要什么)

Accept-Encoding

Accept-Encoding: gzip, deflate, br

数据的编码方式, 主要是告诉服务端怎样的一个数据压缩(数据压缩算法有很多 gzip, deflate, br 等)

Accept-Language

Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

语言, 比如中文, q 表示权重

User-Agent

User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36

Mozilla/5.0 网景浏览器的头

Windows NT 10.0; WOW64 操作系统版本

AppleWebKit/537.36 表示 苹果公司开发的 WebKit 内核

KHTML 类似 火狐的 Gecko 的渲染引擎

Chrome/69.0.3497.92 浏览器版本号

Safari/537.36 因为是苹果的 webkit 内核, 所以会加上这个

浏览器相关的信息, 移动端还是 PC.

返回

Content-Type

对应请求中的 Accept, Accept 可以有很多种类型, Content-Type 可以从里面选择一种返回

可以设置 X-Content-Type-Options: nosniff, 禁止浏览器根据内容去预测类型, 为了安全性.

Content-Encoding

对应 Accept-Language

Content-Language

对应 Accept-Language

服务端不返回 User-Agent

数据在整个传输过程中大小: 包含 http headers 和 body 和首行信息

数据拿到后并且根据 Content-Encoding 解压后的 body 实际内容大小, 不是 传输里面的 body 大小

MIME 类型

application/x-www-form-urlencoded

<form
  action="/form"
  method="post"
  enctype="application/x-www-form-urlencoded
"
>
  <input type="text" name="name " />
  <input type="password" name="password " /> <input type="submit " />
</form>
1
2
3
4
5
6
7
8
9

请求头:

Content-Type: application/x-www-form-urlencoded

name=1&password=1
1
2
3

multipart/form-data

<form action="/form" method="post" enctype="multipart/form-data ">
  <input type="text" name="name" />
  <input type="password" name="password" />
  <input type="submit" />
</form>
1
2
3
4
5

请求头:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIyBOcYI2n3gyM9IQ

// ----WebKitFormBoundaryIyBOcYI2n3gyM9IQ 用来分隔表单每一项
1
2
3
------WebKitFormBoundaryIyBOcYI2n3gyM9IQ
Content-Disposition: form-data; name="name"

3
------WebKitFormBoundaryIyBOcYI2n3gyM9IQ
Content-Disposition: form-data; name="password"

4
------WebKitFormBoundaryIyBOcYI2n3gyM9IQ
Content-Disposition: form-data; name="file"; filename="url-tcp.png"
Content-Type: image/png

------WebKitFormBoundaryIyBOcYI2n3gyM9IQ--
1
2
3
4
5
6
7
8
9
10
11
12
13

Redirect

服务器告诉浏览器你要的资源在什么地方, 浏览器重新请求新的地址

302 临时跳转

const http = require('http');

http
  .createServer(function(request, response) {
    console.log('request come', request.url);

    if (request.url === '/') {
      response.writeHead(302, {
        Location: '/new'
      });
      response.end('');
    }
    if (request.url === '/new') {
      response.writeHead(200, {
        'Content-Type': 'text/html'
      });
      response.end('< div >this is conetnt< div >');
    }
  })
  .listen(8888);

console.log('server run 8888');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

301 永远跳转

response.writeHead(301, {
  Location: '/new'
});
1
2
3

只有第一次是 301, 以后每次否是请求最新的了. 直接在浏览器变成新的路径, 不需要再去服务器指定新的 location.

from disk 已经存到浏览器了.注意: 301 不会主动失效, 只有当用户清除缓存才行.

Content-Security-Policy

内容安全策略

作用

  • 限制资源获取
  • 报告资源获取越权

限制方式

default-src 全局限制

指定资源类型 F4hGq0.md.png

限制只能外链

'Content-Security-Policy': 'default-src http: https:'
1
1
2

限制只能加载本站点脚本

'Content-Security-Policy': 'default-src \'self\''
1
1
2

限制 form 为本站

'Content-Security-Policy': 'default-src \'self\'; form-action \'self\''
1
1
2

只限制 script

'Content-Security-Policy': 'script-src \'self\''
1
1
2

上报路径

如果触发 CSP 则请求 上报接口

'Content-Security-Policy': 'script-src \'self\'; report-uri /repoert'
1

设置是上报, 但是给他加载资源:

'Content-Security-Policy-Report-Only': 'script-src \'self\'; report-uri /repoert'
1

html 中设置 meta 和服务端设置 header 都行

report-uri 需要在 header 才能设置.

<head>
  <meta charset="UTF-8 " />
  <title>Document</title>
  <meta
    http-equiv="Content-Security-Policy"
    content="script-src 'self'; form-action 'self'"
  />
</head>
1
2
3
4
5
6
7
8

TOC