转载于https://github.com/poetries/FE-Interview-Questions,by poetries
一、网络
#1 UDP
1.1 面向报文
UDP
是一个面向报文(报文可以理解为一段段的数据)的协议。意思就是UDP
只是报文的搬运工,不会对报文进行任何拆分和拼接操作
具体来说
- 在发送端,应用层将数据传递给传输层的
UDP
协议,UDP
只会给数据增加一个UDP
头标识下是UDP
协议,然后就传递给网络层了 - 在接收端,网络层将数据传递给传输层,
UDP
只去除IP
报文头就传递给应用层,不会任何拼接操作
1.2 不可靠性
UDP
是无连接的,也就是说通信不需要建立和断开连接。UDP
也是不可靠的。协议收到什么数据就传递什么数据,并且也不会备份数据,对方能不能收到是不关心的UDP
没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是TCP
1.3 高效
- 因为
UDP
没有TCP
那么复杂,需要保证数据不丢失且有序到达。所以UDP
的头部开销小,只有八字节,相比TCP
的至少二十字节要少得多,在传输数据报文时是很高效的
头部包含了以下几个数据
- 两个十六位的端口号,分别为源端口(可选字段)和目标端口 整个数据报文的长度
- 整个数据报文的检验和(
IPv4
可选 字段),该字段用于发现头部信息和数据中的错误
1.4 传输方式
UDP
不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能
#2 TCP
2.1 头部
TCP
头部比UDP
头部复杂的多
对于
TCP
头部来说,以下几个字段是很重要的
Sequence number
,这个序号保证了TCP
传输的报文都是有序的,对端可以通过序号顺序的拼接报文Acknowledgement Number
,这个序号表示数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到Window Size
,窗口大小,表示还能接收多少字节的数据,用于流量控制
标识符
URG=1
:该字段为一表示本数据报的数据部分包含紧急信息,是一个高优先级数据报文,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。ACK=1
:该字段为一表示确认号字段有效。此外,TCP
还规定在连接建立后传送的所有报文段都必须把ACK
置为一PSH=1
:该字段为一表示接收端应该立即将数据 push 给应用层,而不是等到缓冲区满后再提交。RST=1
:该字段为一表示当前TCP
连接出现严重问题,可能需要重新建立TCP
连接,也可以用于拒绝非法的报文段和拒绝连接请求。SYN=1
:当SYN=1
,ACK=0
时,表示当前报文段是一个连接请求报文。当SYN=1
,ACK=1
时,表示当前报文段是一个同意建立连接的应答报文。FIN=1
:该字段为一表示此报文段是一个释放连接的请求报文
2.2 状态机
HTTP
是无连接的,所以作为下层的TCP
协议也是无连接的,虽然看似TCP
将两端连接了起来,但是其实只是两端共同维护了一个状态
TCP
的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手。- 在这之前需要了解一个重要的性能指标 RTT。该指标表示发送端发送数据到接收到对端数据所需的往返时间
建立连接三次握手
- 在
TCP
协议中,主动发起请求的一端为客户端,被动连接的一端称为服务端。不管是客户端还是服务端,TCP
连接建立完后都能发送和接收数据,所以TCP
也是一个全双工的协议。 - 起初,两端都为
CLOSED
状态。在通信开始前,双方都会创建TCB
。 服务器创建完TCB
后遍进入LISTEN
状态,此时开始等待客户端发送数据
第一次握手
客户端向服务端发送连接请求报文段。该报文段中包含自身的数据通讯初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通信初始序号。
第二次握手
服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入
SYN-RECEIVED
状态。
第三次握手
当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入
ESTABLISHED
状态,服务端收到这个应答后也进入ESTABLISHED
状态,此时连接建立成功。
- PS:第三次握手可以包含数据,通过
TCP
快速打开(TFO
)技术。其实只要涉及到握手的协议,都可以使用类似TFO
的方式,客户端和服务端存储相同cookie
,下次握手时发出cookie
达到减少RTT
的目的
你是否有疑惑明明两次握手就可以建立起连接,为什么还需要第三次应答?
- 因为这是为了防止失效的连接请求报文段被服务端接收,从而产生错误
可以想象如下场景。客户端发送了一个连接请求 A,但是因为网络原因造成了超时,这时 TCP 会启动超时重传的机制再次发送一个连接请求 B。此时请求顺利到达服务端,服务端应答完就建立了请求。如果连接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又需要建立 TCP 连接,从而应答了该请求并进入
ESTABLISHED
状态。此时客户端其实是 CLOSED 状态,那么就会导致服务端一直等待,造成资源的浪费
PS:在建立连接中,任意一端掉线,TCP 都会重发 SYN 包,一般会重试五次,在建立连接中可能会遇到 SYN FLOOD 攻击。遇到这种情况你可以选择调低重试次数或者干脆在不能处理的情况下拒绝请求
断开链接四次握手
TCP
是全双工的,在断开连接时两端都需要发送FIN
和ACK
。
第一次握手
若客户端 A 认为数据发送完成,则它需要向服务端 B 发送连接释放请求。
第二次握手
B 收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,表示 A 到 B 的连接已经释放,不接收 A 发的数据了。但是因为 TCP 连接时双向的,所以 B 仍旧可以发送数据给 A。
第三次握手
B 如果此时还有没发完的数据会继续发送,完毕后会向 A 发送连接释放请求,然后 B 便进入 LAST-ACK 状态。
PS:通过延迟确认的技术(通常有时间限制,否则对方会误认为需要重传),可以将第二次和第三次握手合并,延迟 ACK 包的发送。
第四次握手
- A 收到释放请求后,向 B 发送确认应答,此时 A 进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有 B 的重发请求的话,就进入 CLOSED 状态。当 B 收到确认应答后,也便进入 CLOSED 状态。
为什么 A 要进入 TIME-WAIT 状态,等待 2MSL 时间后才进入 CLOSED 状态?
- 为了保证 B 能收到 A 的确认应答。若 A 发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 B 不能正常关闭
#3 HTTP
HTTP
协议是个无状态协议,不会保存状态
3.1 Post 和 Get 的区别
Get
请求能缓存,Post
不能Post
相对Get
安全一点点,因为Get
请求都包含在URL
里,且会被浏览器保存历史纪录,Post
不会,但是在抓包的情况下都是一样的。Post
可以通过request body
来传输比Get
更多的数据,Get
没有这个技术URL
有长度限制,会影响Get
请求,但是这个长度限制是浏览器规定的,不是RFC
规定的Post
支持更多的编码类型且不对数据类型限制
3.2 常见状态码
2XX 成功
200 OK
,表示从客户端发来的请求在服务器端被正确处理204 No content
,表示请求成功,但响应报文不含实体的主体部分205 Reset Content
,表示请求成功,但响应报文不含实体的主体部分,但是与204
响应不同在于要求请求方重置内容206 Partial Content
,进行范围请求
3XX 重定向
301 moved permanently
,永久性重定向,表示资源已被分配了新的 URL302 found
,临时性重定向,表示资源临时被分配了新的 URL303 see other
,表示资源存在着另一个 URL,应使用 GET 方法丁香获取资源304 not modified
,表示服务器允许访问资源,但因发生请求未满足条件的情况307 temporary redirect
,临时重定向,和302含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
4XX 客户端错误
400 bad request
,请求报文存在语法错误401 unauthorized
,表示发送的请求需要有通过HTTP
认证的认证信息403 forbidden
,表示对请求资源的访问被服务器拒绝404 not found
,表示在服务器上没有找到请求的资源
5XX 服务器错误
500 internal sever error
,表示服务器端在执行请求时发生了错误501 Not Implemented
,表示服务器不支持当前请求所需要的某个功能503 service unavailable
,表明服务器暂时处于超负载或正在停机维护,无法处理请求
3.3 HTTP 首部
通用字段 | 作用 |
---|---|
Cache-Control |
控制缓存的行为 |
Connection |
浏览器想要优先使用的连接类型,比如 keep-alive |
Date |
创建报文时间 |
Pragma |
报文指令 |
Via |
代理服务器相关信息 |
Transfer-Encoding |
传输编码方式 |
Upgrade |
要求客户端升级协议 |
Warning |
在内容中可能存在错误 |
请求字段 | 作用 |
---|---|
Accept |
能正确接收的媒体类型 |
Accept-Charset |
能正确接收的字符集 |
Accept-Encoding |
能正确接收的编码格式列表 |
Accept-Language |
能正确接收的语言列表 |
Expect |
期待服务端的指定行为 |
From |
请求方邮箱地址 |
Host |
服务器的域名 |
If-Match |
两端资源标记比较 |
If-Modified-Since |
本地资源未修改返回 304(比较时间) |
If-None-Match |
本地资源未修改返回 304(比较标记) |
User-Agent |
客户端信息 |
Max-Forwards |
限制可被代理及网关转发的次数 |
Proxy-Authorization |
向代理服务器发送验证信息 |
Range |
请求某个内容的一部分 |
Referer |
表示浏览器所访问的前一个页面 |
TE |
传输编码方式 |
响应字段 | 作用 |
---|---|
Accept-Ranges |
是否支持某些种类的范围 |
Age |
资源在代理缓存中存在的时间 |
ETag |
资源标识 |
Location |
客户端重定向到某个 URL |
Proxy-Authenticate |
向代理服务器发送验证信息 |
Server |
服务器名字 |
WWW-Authenticate |
获取资源需要的验证信息 |
实体字段 | 作用 |
---|---|
Allow |
资源的正确请求方式 |
Content-Encoding |
内容的编码格式 |
Content-Language |
内容使用的语言 |
Content-Length |
request body 长度 |
Content-Location |
返回数据的备用地址 |
Content-MD5 |
Base64 加密格式的内容MD5 检验值 |
Content-Range |
内容的位置范围 |
Content-Type |
内容的媒体类型 |
Expires |
内容的过期时间 |
Last_modified |
内容的最后修改时间 |
#4 DNS
DNS 的作用就是通过域名查询到具体的 IP。
- 因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么
在
TCP
握手之前就已经进行了DNS
查询,这个查询是操作系统自己做的。当你在浏览器中想访问www.google.com
时,会进行一下操作
- 操作系统会首先在本地缓存中查询
- 没有的话会去系统配置的 DNS 服务器中查询
- 如果这时候还没得话,会直接去 DNS 根服务器查询,这一步查询会找出负责 com 这个一级域名的服务器
- 然后去该服务器查询 google 这个二级域名
- 接下来三级域名的查询其实是我们配置的,你可以给 www 这个域名配置一个 IP,然后还可以给别的三级域名配置一个 IP
以上介绍的是 DNS 迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的 DNS 服务器做请求,得到结果后将数据返回给客户端。
#二、数据结构
#2.1 栈
概念
- 栈是一个线性结构,在计算机中是一个相当常见的数据结构。
- 栈的特点是只能在某一端添加或删除数据,遵循先进后出的原则
实现
每种数据结构都可以用很多种方式来实现,其实可以把栈看成是数组的一个子集,所以这里使用数组来实现
class Stack {
constructor() { this.stack = [] } push(item) { this.stack.push(item) } pop() { this.stack.pop() } peek() { return this.stack[this.getCount() - 1] } getCount() { return this.stack.length } isEmpty() { return this.getCount() === 0 } }
应用
匹配括号,可以通过栈的特性来完成
var isValid = function (s) { let map = { '(': -1, ')': 1, '[': -2, ']': 2, '{': -3, '}': 3 } let stack = [] for (let i = 0; i < s.length; i++) { if (map[s[i]] < 0) { stack.push(s[i]) } else { let last = stack.pop() if (map[last] + map[s[i]] != 0) return false } } if (stack.length > 0) return false return true };
#2.2 队列
概念
队列一个线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出的原则
实现
这里会讲解两种实现队列的方式,分别是单链队列和循环队列
- 单链队列
class Queue {
constructor() { this.queue = [] } enQueue(item) { this.queue.push(item) } deQueue() { return this.queue.shift() } getHeader() { return this.queue[0] } getLength() { return this.queue.length } isEmpty() { return this.getLength() === 0 } }
因为单链队列在出队操作的时候需要
O(n)
的时间复杂度,所以引入了循环队列。循环队列的出队操作平均是O(1)
的时间复杂度
- 循环队列
class SqQueue {
constructor(length) { this.queue = new Array(length + 1) // 队头 this.first = 0 // 队尾 this.last = 0 // 当前队列大小 this.size = 0 } enQueue(item) { // 判断队尾 + 1 是否为队头 // 如果是就代表需要扩容数组 // % this.queue.length 是为了防止数组越界 if (this.first === (this.last + 1) % this.queue.length) { this.resize(this.getLength() * 2 + 1) } this.queue[this.last] = item this.size++ this.last = (this.last + 1) % this.queue.length } deQueue() { if (this.isEmpty()) { throw Error('Queue is empty') } let r = this.queue[this.first] this.queue[this.first] = null this.first = (this.first + 1) % this.queue.length this.size-- // 判断当前队列大小是否过小 // 为了保证不浪费空间,在队列空间等于总长度四分之一时 // 且不为 2 时缩小总长度为当前的一半 if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) { this.resize(this.getLength() / 2) } return r } getHeader() { if (this.isEmpty()) { throw Error('Queue is empty') } return this.queue[this.first] } getLength() { return this.queue.length - 1 } isEmpty() { return this.first === this.last } resize(length) { let q