Source: 前端常见八股文

计算机基础

  1. 进程与线程区别是什么
  2. 讲讲 TCP 三次握手、四次挥手,为什么要三次握手、四次挥手
  3. TCP 和 UDP 区别是什么
  4. 实现一个LRU算法

进程与线程区别是什么

进程是资源分配的基本单位线程是 CPU 调度的基本单位

特性进程 (Process)线程 (Thread)
根本区别资源分配的基本单位CPU 调度的基本单位
资源拥有拥有独立的地址空间、内存、文件句柄等系统资源。不拥有资源,与同进程下的其他线程共享所有资源(如内存、文件)。只拥有少量私有资源(如执行栈、寄存器、程序计数器)。
内存空间每个进程都有独立的内存空间。同一进程内的所有线程共享同一块内存空间。
隔离性进程间相互隔离,一个进程的崩溃不会直接影响其他进程。安全性高线程间不隔离,一个线程的崩溃可能会导致整个进程崩溃。安全性低
切换开销切换成本。需要切换整个内存地址空间,保存和恢复大量的上下文信息。切换成本。只需保存和恢复少量寄存器和栈信息即可。
通信方式进程间通信 (IPC) 复杂,需要通过管道、消息队列、共享内存等特定机制。线程间通信简单,可以直接读写共享的全局变量和内存数据(但需要注意线程同步问题,如加锁)。
创建与销毁创建和销毁的开销创建和销毁的开销

讲讲 TCP 三次握手、四次挥手,为什么要三次握手、四次挥手

三次握手步骤分解:

  1. 第一次握手 (SYN)

    • 客户端 -> 服务器
    • 客户端想和服务器建立连接,于是发送一个特殊的报文段。
    • 内容SYN = 1 (表示这是一个连接请求), seq = x (x 是一个随机生成的初始序列号)。
    • 状态:客户端进入 SYN_SENT 状态,等待服务器确认。
  2. 第二次握手 (SYN+ACK)

    • 服务器 -> 客户端
    • 服务器收到了客户端的请求,如果同意连接,就会做出响应。
    • 内容SYN = 1 (我也发起一个连接请求), ACK = 1 (确认你的请求有效), seq = y (y 是服务器随机生成的初始序列号), ack = x + 1 (我期望你下一个发来的数据序号是 x+1,以此确认我收到了你的第一个包)。
    • 状态:服务器进入 SYN_RCVD 状态。
  3. 第三次握手 (ACK)

    • 客户端 -> 服务器
    • 客户端收到了服务器的同意和确认,它也需要给服务器一个确认。
    • 内容ACK = 1 (确认收到你的确认), seq = x + 1 (我从这个序号开始发数据), ack = y + 1 (我期望你下一个发来的数据序号是 y+1)。
    • 状态:客户端和服务器都进入 ESTABLISHED 状态,连接成功建立,可以开始传输数据了。

为什么要三次握手?而不是两次或四次?

核心目的:为了确认双方的发送和接收能力都正常,并同步初始序列号。

  • 为什么不能是两次?
    • 无法防止已失效的连接请求报文。这是一个主要原因。
    • 场景:客户端发送了一个连接请求 A,但因为网络延迟,这个请求在某个地方滞留了。客户端等不到确认,就超时重发了一个新的请求 B。请求 B 正常建立了连接,数据传输完毕后释放了连接。这时,那个滞留已久的请求 A 突然到达了服务器。
    • 如果是两次握手:服务器收到请求 A,会误以为是客户端又发起了一个新连接,于是发送确认,连接就建立了。但此时客户端并不会理会这个确认,因为它已经结束了。服务器就会一直等待客户端发送数据,白白浪费资源。
    • 如果是三次握手:服务器收到请求 A,回复一个确认。但客户端的上下文已经改变,它知道自己没有发起这个连接,所以会忽略服务器的确认,不发送第三次握手。服务器收不到确认,就知道这是一个无效连接,不会建立连接。
  • 为什么不需要四次?
    • 三次已经足够验证双方的收发能力了。
    • 第一次握手:服务器知道了客户端的发送能力正常。
    • 第二次握手:客户端知道了服务器的发送接收能力都正常。
    • 第三次握手:服务器知道了客户端的接收能力正常。
    • 至此,双方都确认了对方的收发能力没问题,可以开始通信。再增加一次握手就显得多余,只会增加连接建立的延迟。

四次挥手步骤分解:

  1. 第一次挥手 (FIN)
    • 客户端 -> 服务器
    • 客户端的数据已经发送完毕,它想断开连接,于是发送一个 FIN 报文。
    • 内容FIN = 1seq = u (u 是客户端最后发送数据的序列号+1)。
    • 状态:客户端进入 FIN_WAIT_1 状态。
  2. 第二次挥手 (ACK)
    • 服务器 -> 客户端
    • 服务器收到了客户端的断开请求。它先回复一个确认,表示“我知道你想断开了”。
    • 内容ACK = 1ack = u + 1
    • 状态:服务器进入 CLOSE_WAIT 状态。客户端收到后进入 FIN_WAIT_2 状态。
    • 注意:此时服务器可能还有数据没发完,所以它不能马上关闭连接。这个阶段,连接处于半关闭状态,即客户端不能再发送数据,但服务器仍然可以发送。
  3. 第三次挥手 (FIN)
    • 服务器 -> 客户端
    • 服务器的数据也发送完毕了,现在它可以关闭连接了。它向客户端发送一个 FIN 报文。
    • 内容FIN = 1ACK = 1seq = v (v 是服务器最后发送数据的序列号+1), ack = u + 1
    • 状态:服务器进入 LAST_ACK 状态,等待客户端的最后确认。
  4. 第四次挥手 (ACK)
    • 客户端 -> 服务器
    • 客户端收到了服务器的关闭请求,发送最后一个确认报文。
    • 内容ACK = 1seq = u + 1ack = v + 1
    • 状态:客户端进入 TIME_WAIT 状态,等待 2*MSL(Maximum Segment Lifetime,报文最大生存时间)后,才真正关闭连接。服务器收到这个 ACK 后,立即进入 CLOSED 状态。

为什么要四次挥手?

核心原因:因为 TCP 是全双工的,需要给被动关闭方一个机会,确保它所有的数据都发送完毕了。

  • 当客户端(主动方)说“我要关闭了”(第一次挥手),它只是表明自己没有数据要发送了。
  • 服务器(被动方)收到后,先回复一个“知道了”(第二次挥手)。但此时服务器可能还有未发送完的数据,不能立刻关闭。所以不能把 ACK 和 FIN 合并成一个报文。
  • 服务器必须等到自己的数据也全部发送完毕后,才能发送“我也准备好了,可以关闭了”的 FIN 报文(第三次挥手)。
  • 这样一来一回,就形成了四次挥手。

为什么客户端最后要等待 2*MSL?

这是为了确保连接的可靠关闭。主要有两个原因:

  1. 保证服务器能收到最后的 ACK:客户端发送的最后一个 ACK 报文可能会丢失。如果丢失了,服务器会因为收不到确认而超时重传第三次挥手的 FIN 报文。如果客户端不等待,直接关闭,就无法响应这个重传的 FIN,导致服务器无法正常关闭。等待 2*MSL 可以确保有足够的时间接收到服务器的重传并重新发送确认。
  2. 防止“已失效的连接请求报文”出现:与三次握手的原因类似。等待 2*MSL 可以确保本次连接中所有在网络中滞留的报文段都已消失,不会在下一次新的连接中(使用相同的端口号)出现,干扰新连接。

TCP 和 UDP 区别是什么

特性TCP (传输控制协议)UDP (用户数据报协议)
连接性面向连接 (Connection-oriented)无连接 (Connectionless)
说明在传输数据前,必须先建立连接(三次握手)。传输结束后,需要释放连接(四次挥手)。发送数据前不需要建立连接,直接把数据包(数据报)扔给对方。
可靠性可靠不可靠 (尽力而为)
说明通过确认(ACK)、重传、序列号等机制,保证数据不丢失、不重复、按顺序到达。不保证数据能否到达,也不保证顺序。数据包可能会丢失或乱序。
有序性有序无序
说明数据是作为字节流按顺序发送和接收的。接收端会根据序列号对数据包进行排序。每个数据报都是独立的,它们到达的顺序与发送的顺序可能不一致。
速度/效率
说明因为需要建立连接、发送确认、处理排序等,开销(Overhead)较大,传输速度相对较慢。没有复杂的控制机制,开销小,传输速度快。
头部开销(TCP 头部大小为 20-60 字节)(UDP 头部固定为 8 字节)
说明头部包含了序列号、确认号、窗口大小等大量控制信息。头部只包含源/目的端口、长度和校验和等最基本的信息。
数据形式字节流 (Byte Stream)数据报 (Datagram)
说明应用层数据被视为一连串无边界的字节流。TCP 可能会对数据进行拆分或合并。应用层交下来的数据报,UDP 只会加上头部,不会进行任何拆分或合并。保留了消息的边界。
控制机制(流量控制、拥塞控制)
说明流量控制:防止发送方过快,导致接收方来不及处理。
拥塞控制:根据网络状况动态调整发送速率,避免网络拥堵。
以恒定的速率发送数据,不管网络是否拥堵,也不管接收方是否能处理。

选择 TCP 的场景 (可靠性优先)

当数据的完整性和顺序至关重要,不容许任何差错时,必须使用 TCP。

  • Web 浏览 (HTTP/HTTPS):网页的 HTML、CSS、JS 文件必须完整无误地按顺序加载,否则页面会错乱或无法显示。
  • 文件传输 (FTP, SFTP):传输文件时,一个字节的错误都可能导致整个文件损坏。
  • 电子邮件 (SMTP, POP3, IMAP):你肯定不希望你的邮件内容丢失单词或段落。
  • 远程终端 (SSH):你输入的命令和服务器返回的结果,都必须准确无误。

选择 UDP 的场景 (实时性/速度优先)

当对实时性要求极高,可以容忍少量数据丢失时,UDP 是更好的选择。

  • 在线视频/直播 (Streaming):如果为了等一个丢失的数据包(比如画面中的一帧)而让整个视频卡顿,用户体验会非常糟糕。宁愿丢失一帧(画面出现瞬间花屏),也要保证流畅播放。
  • 网络电话 (VoIP):和视频同理,实时通话的流畅性远比偶尔丢失一个声音采样点更重要。
  • 在线游戏:玩家的位置、动作等信息需要被快速发送。如果一个数据包延迟到达,它包含的已经是过时的信息,不如直接丢弃,使用最新的数据包。
  • DNS (域名系统):查询一个域名的 IP 地址,这个过程要求非常快。通常只是一次简单的请求和响应,如果查询失败(数据包丢失),客户端简单地重试一次即可,使用 UDP 开销最小。

实现一个LRU算法

利用Map的有序性,简单实现:

  • get 操作:当获取一个元素时,为了把它标记为“最新使用”,我们先从 Map 中删除它,然后再重新插入。这样它就会被移动到 Map 迭代顺序的末尾。
  • put 操作:当缓存满了需要淘汰时,我们淘汰的就是 Map 迭代顺序中的第一个元素。
class LRU {
	constructor(capacity) {
		this.capacity = capacity;
		this.cache = new Map();
	}
	
	get(key) {
		if (!this.cache.has(key)) return -1;
		
		const value = this.cache.get(key);
		this.cache.delete(key);
		this.cache.set(key, value);
	
		return value;
	}
	
	
	put(key, value) {
		if (this.cache.has(key)) {
			this.cache.delete(key);
		}
		this.cache.set(key, value);
		
		if (this.cache.size > this.capacity) {
			const oldestKey = this.cache.keys().next().value;
			this.cache.delete(oldestKey);
		}
	}
}

最佳实践是使用「哈希表 + 双向链表」:

  • 哈希表 (JavaScript 中的 Map):用于实现 O(1) 的快速查找。Map 的键是数据的 key,值是指向链表节点的引用。
  • 双向链表 (Doubly Linked List):用于维护数据的访问顺序。链表头部是“最新”的数据,链表尾部是“最旧”的数据。
class Node {
	constructor(key, value) {
		this.key = key;
		this.value = value;
		this.pre = null;
		this.next = null;
	}
}

class LRUCache {
	constructor(capacity) {
		this.capacity = capacity;
		this.cache = new Map();
		this.head = new Node(0, 0);
		this.tail = new Node(0, 0);
		this.head.next = this.tail;
		this.tail.prev = this.head;
	}

	_moveToHead(node) {
		// 从原位置断开
		node.prev.next = node.next;
		node.next.prev = node.prev;

		// 插入到头部之后;
		node.next = this.head.next;
		this.head.next.prev = node;
		this.head.next = node;
		node.prev = this.head;
	}
	
	_removeTail() {
		const tailNode = this.tail.pre;
		tailNode.prev.next = this.tail;
		this.tail.prev = tailNode.prev;
		return tailNode;
	}
	
	get(key) {
		if (!this.cache.has(key)) return -1;
		
		const node = this.cache.get(key);
		this._moveToHead(node);
		
		return node.value;
	}
	
	put(key, value) {
		if (this.cache.has(key)) {
			const node = this.cache.get(key);
			node.value = value;
			this._moveToHead(node);
		} else {
			const newNode = new Node(key, value);
			this.cache.set(key, newNode);
			
			newNode.next = this.head.next;
			this.head.next.prev = newNode;
			this.head.next = newNode;
			newNode.prev = this.head;
			
			if (this.cache.size > this.capacity) {
				const tailNode = this._removeTail();
				this.cache.delete(tailNode.key);
			}
		}
	}
}

前端面试常见题-计算机基础

https://www.yuque.com/baiyueguang-rfnbu/tr4d0i/rz15kr
作者

MeorinLime 梦灵

发布日期

2025 - 08 - 11