前言

这是一道非常非常经典的面试题,但涉及到的内容非常的广,接下来从小到大逐步分析他所包含的内容

详细版

  1. 在浏览器地址栏输入 url(这里可能会问 url 的长度最长是多少,状态码,编码方式,)
  2. 浏览器查看缓存,如果请求资源在缓存中且新鲜,则跳到转码步骤(这里考缓存的形式,新鲜度的计算,缓存的头字段,缓存策略)
    2.1 如果资源未缓存,发起新请求
    2.2 如果已缓存,检测是否足够新鲜,足够新鲜提供给客户端,否则与服务器进行验证。
  3. 解析 url 获取协议 端口 主机 path(这里可能会考 https 协议 端口看协议 主机可能和主机托管相关)
  4. 浏览器组装一个 HTTP 请求报文(当然这个不一定是 get 请求,所以可能考各种请求方式,如果是 2 的话还要考流的运输组装)
  5. 浏览器获取主机 ip 地址 过程如下
    5.1 浏览器本地缓存
    5.2 本机缓存
    5.3 hosts 文件(考 host 的话可能考 linux 里面的 host 路径和 windows 下的 host 路径 其实改 host 能解决 github 被墙的原因也是来自于此)
    5.4 路由器缓存(这个不了解)
    5.5 ISP DNS 缓存()
    5.6 DNS 递归查询

url 最长长度

关于这个问题其实不同的浏览器规范不一样的,比如 ie 是 2083,chrome8182 等。但最好不要超过 2048 个字符,虽然 ie 被干掉了,但是还是保持 get 请求的 url 短一点吧
所以如果后端给批量操作的接口的时候,你就有理由告诉他改成 post 了

字节

这块遇到过考察 汉字是多少个字节
在 GBK 中,我们的汉字是两个字节,UTF-16 中是两个字节,UTF-8 则是 3 个字节

这里不讲原理 因为我也不会 当他自己是这么定义的好了

问题是你知道怎么输出一个字母或者汉字的字节吗

1
2
3
4
let enc = new TextEncoder();
console.log(enc.encode("哈").length); //3
console.log(enc.encode("1").length); //1
console.log(enc.encode("j").length); //1

使用这个 api 可以返回 utf-8 下面的字节数。其实他本来是可以传入参数的,但目前 mdn 上说被废弃掉了

有了 encoder 肯定会有 decoder

下面是我搬运 mdn 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
let utf8decoder = new TextDecoder(); // default 'utf-8' or 'utf8'

let u8arr = new Uint8Array([240, 160, 174, 183]);
let i8arr = new Int8Array([-16, -96, -82, -73]);
let u16arr = new Uint16Array([41200, 47022]);
let i16arr = new Int16Array([-24336, -18514]);
let i32arr = new Int32Array([-1213292304]);

console.log(utf8decoder.decode(u8arr)); //𠮷
console.log(utf8decoder.decode(i8arr)); //𠮷
console.log(utf8decoder.decode(u16arr)); //𠮷
console.log(utf8decoder.decode(i16arr)); //𠮷
console.log(utf8decoder.decode(i32arr)); //𠮷

这个看一看就好了 平时可能就不太会用到

状态码

说到这个状态码 url 超出服务器限制的时候返回的是 414(URI Too Long)

缓存

缓存这块涉及的知识点就很多了,主要分为强缓存和协商缓存。

强缓存是什么呢?

指的是该缓存在存在的期间不需要发送请求,会返回状态码 200 且告诉你是 memoryCache 还是 diskCache

memoryCache指的是资源存在内存中,当页面被关闭就会释放
diskCache指的是资源存在磁盘中

至于这两者什么时候是 memoryCache 什么时候是 diskCache 取决于浏览器

参考网上的一个表格,chrome浏览器对于cache的统计

一般样式就存在disk,但是js这类的就存在memory,因为从磁盘读脚本对IO的开销比较大

浏览器中查看的方式:

先把停用缓存/disable cache关掉 然后ctrl+r刷新你的页面 这里拿掘金为例子 可以看见缓存中memoryCache和diskCache都有
memoryCache

diskCache

没走缓存 要下载的资源,注意了噢 如果是200 则是资源的大小 如果是304则是报文的大小

强缓存怎么触发呢?

这块涉及到头字段:ExpiresCache-Control:max-age

首先前者 来自于 http1.0 其中的值是一个 GMT 格式字符串 代表缓存过期时间

eg

1
Expires: Wed, 22 Oct 2018 08:41:00 GMT

设置的方式也很简单:
(不同框架实现不一样 随便写个例子)

1
xxx.set("Expires", new Date(Date.now() + 30000).toUTCString());//当前时间+30s

expires的缺点

受限于本地时间 修改本地时间可能导致缓存失败

Cache-Control:max-age

1.1提出的规范

优先级高于Expires,且设置的是秒数,前者是毫秒数

1
xxx.set('Cache-Control', 'max-age=30')

cachecontrol除了是设置maxage之外还有别的value
可以连起来写

  • private:客户端可以缓存
  • public:客户端和代理服务器都可以缓存
  • s-maxage:和max-age一样,但这个是设定代理服务器的缓存时间
  • no-cache:需要使用协商缓存来验证缓存数据
  • no-store:所有内容都不会缓存

注意经常考的是 no-storeno-cache的区别,这个要牢记

除此之外 如果是采用了CDN的形式 但是有些内容不想放在CDN上,比如一些敏感信息 此时采用private更加合适 只有客户端才能缓存

协商缓存

这块的话 分两个字段:Last-Modified,If-Modified-Since
Last-modified是服务端返回给客户端携带的头字段
If-Modified-Since是客户端请求的头字段

先从字面上去理解字段的意思:前者是最后一次操作(资源),后者是如果有操作(资源)的话,则从操作(资源)之后发送新的(资源)

前者的值是资源最后的更新时间,后者的值是前者的值。

过程是这样的:
首先有一个txt文件,前端请求后端返回,返回的过程携带了Last-modified响应头,值是这个txt文件最后一次修改的时间

然后前端接收到了,再次请求该txt文件,请求的时候带上(这一步是浏览器帮你带的不需要自己带)If-Modified-Since字段,值是Last-modified的值,到后端 后端会去验证 该请求字段的值是否和txt文件最后一次修改的时间一致,如果一致,说明txt文件没改,没改的话则返回304。且不会返回Last-modified
和资源内容

上一段来自掘金的代码

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
29
30
31
32
// 获取文件信息
const getFileStat = (path) => {
return new Promise((resolve) => {
fs.stat(path, (_, stat) => {
resolve(stat)
})
})
}

app.use(async (ctx) => {
const url = ctx.request.url
if (url === '/') {
// 访问根路径返回index.html
ctx.set('Content-Type', 'text/html')
ctx.body = await parseStatic('./index.html')
} else {
const filePath = path.resolve(__dirname, `.${url}`)
const ifModifiedSince = ctx.request.header['if-modified-since']
const fileStat = await getFileStat(filePath)
console.log(new Date(fileStat.mtime).getTime())
ctx.set('Cache-Control', 'no-cache')
ctx.set('Content-Type', parseMime(url))
// 比对时间,mtime为文件最后修改时间
// 如果时间不变则返回304,且不携带`Last-Modified`和资源文件
if (ifModifiedSince === fileStat.mtime.toGMTString()) {
ctx.status = 304
} else {
ctx.set('Last-Modified', fileStat.mtime.toGMTString())
ctx.body = await parseStatic(filePath)
}
}
})

Last-modified的缺点

其实lastmodify本身也有缺点,就是他主要区分的话还是在于文件的修改时间的一个对比

假设我们有一个txt文件里面有一个字符串jojo,此时修改的时间是a

那么我把txt里面的jojo去掉 保存之后 又重新添加回去 再保存 此时修改的时间变成了b

按正常人的思路肯定是期望这个资源还能被缓存,因为内容没有发生变化,这样一个周期性的变化,Last-modified是检测不了的,因为修改的时间变了

再说另外一个缺点
文件修改时间这个值本身返回的是秒,要是我的内容本身小于单位秒,比如几毫秒内修改了,那么依旧也是检测不到的

ETag

所以出现了另外一个终极方案叫Etag,他是借由文件的内容生成的hash值,这样就能保证资源周期性和唯一性,然后Etag也是后端发来的,前端再次请求会携带一个If-None-Match

也就是只要记得他是通过文件内容生成hash值就行了,他的优先级比lastmodify高,他的校验方式和lastmodify是一样的

待补充的:

这一块还有的内容是新鲜度的计算,启发式缓存,代理缓存,客户端的代理宽容限制等

https协议

s指的是TLS/SSL证书 以前是叫做SSL,现在还是叫TLS好一点点,和http的区别如下:(老生常谈)
首先是钱 https要申请CA证书,画的比较多,但安全,http纯白嫖
安全性上面 https加密传输,http明文
端口 https是443端口 http是80

这块涉及到的内容有很多 一个是TLS1.2和1.3的握手方式。OSI七层模型。数字签名,CA证书,安全问题(中间人),对称加密非对称加密,

OSI七层模型

先简单的说一下OSI七层模型,这块经常忘记,实际上它是一个可以展开描述的重点内容

七层分别是应用层,表示层,会话层,传输层,网络层,数据链路层,物理层

图片来自网络:

首先应用层是为了提供给用户接口或者说网络服务,所以在这一层里面自然的就是一些网络服务协议
可能比较陌生的协议提一嘴:telnet,它是TCP/IP协议中的一员,用于处理远程登陆服务这块的;SNMP协议全称Simple Network Management Protocol简单网络管理协议,它们提供了一种从网络上的设备中收集网络管理信息的方法,简单来说它就是收集信息的

表示层这块是为了给上述的应用层提供一个数据格式服务转换的功能让对方的应用层能够理解,也就是它类似于生活中的翻译官,需要给对方的应用层做一个翻译,或者说它是一个电报员需要发加密的电报防止中间人窃取这样。主要就是加密解密,编码解码。但其实压缩和解压缩也是在这块的需要注意。

会话层这块是为了给上述表示层做一个通信处理的,首先它会建立一个会话连接,然后去验证对方是否是自己这边的人,然后把我方表示层的资料包装起来给到下面的传输层去传输,确定了传输完成之后会话就会终止。
会话层主要的作用就是集中管理这些会话,过程中有一些常见的场景比如验证登陆,断点续传。

传输层就是把上述会话层封装好的表示层资料给传输到对方,TCP和UDP就是常见的传输协议,在这一层里面还要负责处理一些数据包的错误。

网络层呢,其实就是一个IP层,它通过ip寻址来建立两个节点的连接,为上述传输层的数据选择一个合适的路由和交换节点,正确无误的按照地址传送给对方的传输层。传输层和网络层就相当于快递员和快递中心的关系

数据链路层和物理层就不太展开介绍了涉及到的东西很多

数字签名 CA证书

数字签名是公钥加一些个人信息用哈希算法生成摘要之后,再用私钥加密生成的东西。
数字证书应用了数字签名的技术,将个人信息和公钥用哈希算法生成摘要之后,再用CA的私钥加密,这整个就是数字证书。

TLS1.2和1.3的握手方式

TLS1.2握手

step1:Client Hello

首先浏览器发送client_random,TLS版本,加密套件列表。

client_random是什么?用来形成最终secret的一个参数

加密套件列表是什么?eg

1
TLS_ECDHE_WITH_AES_128_GCM_SHA256

意思是在TLS握手的过程中,使用ECDHE算法pre_random,128位的AES算法进行对称加密,在对称加密的过程中使用主流的GCM分组模式,因为对称加密中很重要的一个问题就是如何分组,最后一个是采用哈希摘要算法,采用SHA256算法。

其中值得解释的是这个哈希摘要算法,试想一下这个场景,服务端现在给客户端发信息来了,而客户端不知道此时的消息是服务端的还是中间人的,现在引入这个哈希摘要算法,将服务端的证书信息通过这个算法生成一个摘要,用来标识这个服务器的身份,然后用CA私钥加密后把加密后的标识和CA公钥发给客户端。客户端拿到这个CA公钥来解密,生成另外一份摘要。两个摘要进行对比,如果相同则能确认服务器的身份。这也就是所谓数字签名的原理。其中除了哈希摘要算法,最重要的是私钥加密,公钥解密。

step2 Server Hello

server_random发送这个随机数,确认TLS版本,需要使用的加密套件和自己的证书,那么这个server_params是做什么的呢?

step3 Client验证证书,生成secret

客户端验证服务器传过来的证书和签名是否通过,如果验证通过,则将client_params传递给服务器。

接着客户端通过ECDHE算法计算出了pre_random,其中传入两个参数:server_params和client_params。现在你应该清楚这两个参数的作用了吧,由于ECDHE算法是基于椭圆曲线离散对数,这两个参数也称作椭圆曲线的公钥。

客户端现在拥有了client_random,server_random和pre_random,接下来这三个数通过一个伪随机数函数计算出最终的secret

step4 Server生成secret

客户端传了client_params过来了,服务端开始用ECDHE算法生成pre_random,接着用和客户端同样的伪随机函数生成最后的secret

注意事项

TLS握手实际上是一个双向认证的过程,从step1可以看出客户端有能力验证服务器的身份(数字签名),那么服务器能不能验证客户端的身份呢?

当然是可以的。具体来说,在step3中,客户端发送client_params,实际上是给服务器一个验证消息,让服务器走相同的流程(哈希摘要,私钥加密,公钥解密),确认客户端的身份

在客户端生成secret后,会给服务器发送一个收尾的信息,告诉服务器之后都要用对称加密,对称加密的算法就是用第一次约定的,服务器生成完secret后也会向客户端发送一个收尾信息,告诉客户端以后用对称加密来通信。

这个收尾信息包括两个部分,一部分是Change Cipher Spec,意味着后面加密传输了,另外一个是Finished消息,这个消息是对之前发送的所有数据做的摘要,对摘要进行加密,让对方验证一下。

当双方都验证通过之后,握手才正式结束,后面的HTTP正式开始传输加密报文。

什么是中间人攻击?

中间人攻击(Man-in-the-MiddleAttack,简称“MITM攻击”)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方 直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻 击中,攻击者可以拦截通讯双方的通话并插入新的内容。中间人攻击是一个(缺乏)相互认证的攻击。大多数的加密协议都专门加入了一些特殊的认证方法以阻止中间人攻击。例如,SSL协议可以验证参与通讯的一方或双方使用的证书是否是由权威的受信 任的数字证书认证机构颁发,并且能执行双向身份认证。
中间人攻击过程 1客户端发送请求到服务端,请求被中间人截获。
2)服务器向客户端发送公钥。
3)中间人截获公钥,保留在自己手上。然后自己生成一个【伪 造的】公钥,发给客户端。
4)客户端收到伪造的公钥后,生成加密hash值发给服务器。
5)中间人获得加密hash值,用自己的私钥解密获得真秘钥。同时生成假的加密hash值,发给服务器。
6 ) 服务器用私钥解密获得假密钥。然后加密数据传输给客户端。

hosts

window下:C:\windows\system32\drivers\etc。

host文件的作用:
作用1. 加快域名解析作用
我们会经常访问网站,那么我们则可以通过hosts文件来配置域名以及IP之间的关系,提高域名解析速度。这主要是因为两者之间的映射关系,简单来说就是我们输入域名计算机就能很快解析出IP。而不是在网络上请求服务器。

作用2.方便局域网用户
在很多的局域网中,我们会有很多的服务器提供给用户进行使用。而在局域网中是缺少DNS服务器的,那么输入进去的IP地址就很难记住。hosts文件则是能够有效减少这样的麻烦,方便用户使用。

作用3.屏蔽网站
在很多的网站中,我们会看见很多没有经过用户同意就安装上去的插件。在这些插件中存在木马病毒的可能性很大,使用hosts文件可以将错误IP映射到本地IP之中,将网站屏蔽。

DNS

DNS

DNS的作用就是通过域名查询到IP

因为IP存在数字和英文的组合IPv6,很不利于人类记忆,所以出现了域名。你可以把域名看成某个IP的别名,DNS就是去查询这个别名真正的名称是什么

在TCP握手之前就已经进行了DNS查询,这个查询是操作系统自己完成的,当在浏览器中想访问www.google.com会进行以下操作。

  • 本地客户端向服务器发起请求查询 IP 地址
  • 查看浏览器有没有该域名的 IP 缓存
  • 查看操作系统有没有该域名的 IP 缓存
  • 查看 Host 文件有没有该域名的解析配置
  • 如果这时候还没得话,会通过直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
  • 然后去该服务器查询 google.com 这个二级域名
  • 接下来查询 www.google.com 这个三级域名的地址
  • 返回给 DNS 客户端并缓存起来

以上介绍的是DNS迭代查询,还有一种是递归查询,区别是前者是由客户端去请求,后者是由系统配置的DNS去请求,得到结果之后将数据返回给客户端。

DNS解析优化

主要分为两个方案

  • DNS预解析
  • 减少DNS请求

DNS预解析

DNS解析也需要时间的,可以通过预解析的方式来预先获取域名所对应的IP

link方式:手动解析

1
<link rel="dns-prefetch" href="//blog.poetries.top">

meta方式:https自动解析
1
<meta http-equiv="x-dns-prefetch-control" content="on">

设置响应头:自动解析
1
ctx.set('X-DNS-Prefetch-Control', 'on')