学习笔记
计算机网络基础
计算机网络基础及面试问题
一、TCP/UDP 核心原理
1.1 TCP 和 HTTP 的区别及关系?
核心要点
- HTTP 是应用层协议,定义请求/响应格式(GET/POST、状态码、报头、报文体)
- TCP 是传输层协议,提供可靠、面向连接的传输服务
- HTTP 依赖 TCP 传输报文,HTTP 数据封装在 TCP 报文段里
详细解析
HTTP(HyperText Transfer Protocol)工作在 OSI 模型的第 7 层(应用层),它只规定了客户端和服务器之间如何格式化请求和响应,比如请求方法、URL、头部字段、状态码等。但 HTTP 本身不关心数据怎么从 A 点可靠地传到 B 点。
TCP(Transmission Control Protocol)工作在 OSI 模型的第 4 层(传输层),它通过三次握手建立连接、四次挥手关闭连接,利用序列号、确认应答、重传机制、滑动窗口等机制,保证数据按顺序、无差错、不丢失地到达对端。
两者的关系是:HTTP 是内容,TCP 是载体。HTTP 报文作为 TCP 的 payload 被封装在 TCP 报文段中,再往下经过 IP 层、数据链路层,最终通过物理介质传输。
面试口头回答
HTTP 和 TCP 是不同层次的网络协议。
HTTP 是应用层协议,它定义了请求和响应的格式,比如 GET/POST 方法、状态码、报文头和报文体。但 HTTP 本身不处理数据丢失、顺序或重传这些问题。
TCP 是传输层协议,提供可靠的、面向连接的传输服务,通过三次握手建立连接、序列号保证顺序、ACK 确认和重传机制保证数据完整到达。
所以它们的关系是:HTTP 依赖 TCP 来传输报文,HTTP 的数据会被封装在 TCP 报文段里,通过 TCP 的可靠传输机制送到服务器或客户端。
简单比喻就是:HTTP 是信件内容,TCP 是快递员和快递车,保证信件安全、完整、按顺序送达。
1.2 TCP 三次握手
核心要点
- 第一次:客户端发送 SYN (SEQ=x)
- 第二次:服务端回复 SYN-ACK (SEQ=y, ACK=x+1)
- 第三次:客户端发送 ACK (ACK=y+1),连接建立
- 为什么是 3 次而不是 2 次/4 次:阻止历史连接初始化 + 同步双方初始序列号
详细解析
握手流程:
- SYN:客户端发送同步报文段,携带初始序列号 SEQ=x,进入 SYN-SENT 状态
- SYN-ACK:服务端收到后回复同步确认报文,携带自己的初始序列号 SEQ=y,并确认收到客户端的 SYN(ACK=x+1),进入 SYN-RECV 状态
- ACK:客户端收到后发送确认报文(ACK=y+1),双方进入 ESTABLISHED 状态
为什么不是 2 次?
- 防止历史重复连接初始化:如果网络中有延迟的旧 SYN 报文到达服务端,2 次握手会导致服务端直接建立连接,造成资源浪费或数据混乱。3 次握手的最后一次 ACK 确保客户端确实想要建立这条新连接。
- 同步双方初始序列号(ISN):ISN 对后续的数据确认、顺序重组、重传机制至关重要,必须让双方都确认对方已收到自己的 ISN。
为什么不是 4 次?
- 3 次已经能确保连接的可靠性,第 4 次只会增加时延,不会提升可靠性,没有必要。
面试口头回答
TCP 三次握手的过程是这样的:
第一次,客户端向服务器发送 SYN 报文,携带自己的初始序列号 SEQ=x,表示请求建立连接。
第二次,服务器收到 SYN 后,向客户端回复 SYN-ACK 报文,携带自己的初始序列号 SEQ=y,同时确认号 ACK=x+1,表示同意建立连接。
第三次,客户端收到 SYN-ACK 后,向服务器发送 ACK 报文,确认号 ACK=y+1,表示确认连接建立,完成三次握手。
关于为什么不是两次握手:主要有两个原因。第一,三次握手才能阻止重复历史连接的初始化。如果只有两次,网络中延迟到达的旧 SYN 报文可能被误认为是新连接请求,导致服务端错误建立连接、浪费资源。第二,三次握手才能同步双方的初始序列号,这对后续的数据确认和重传机制非常关键。
关于为什么不是四次:因为三次已经保证了连接的可靠性,再多一次只会增加时延,不会提升可靠性,所以没有必要。
1.3 半连接和全连接分别处于三次握手的哪个阶段?
核心要点
- 半连接:服务端发出 SYN+ACK 后等待客户端 ACK,处于 SYN_RECV 状态,存放在半连接队列(SYN Queue)
- 全连接:三次握手完成,服务端收到 ACK 后进入 ESTABLISHED 状态,存放在全连接队列(Accept Queue),等待应用层 accept()
详细解析
在 Linux 内核中,TCP 连接建立过程涉及两个重要队列:
- 半连接队列(SYN Queue):当服务端收到客户端 SYN 并回复 SYN+ACK 后,连接进入 SYN_RECV 状态,此时连接被放入半连接队列。这个队列的大小由内核参数
net.ipv4.tcp_max_syn_backlog控制。如果队列满了,新的 SYN 请求会被丢弃,可能导致客户端重传。 - 全连接队列(Accept Queue):当服务端收到客户端最终的 ACK 后,连接进入 ESTABLISHED 状态,从半连接队列移到全连接队列,等待应用层调用
accept()取走。这个队列的大小由listen()函数的 backlog 参数和内核参数net.core.somaxconn共同决定。
SYN Flood 攻击就是利用半连接队列的有限容量,发送大量 SYN 但不完成第三次握手,占满半连接队列,使正常请求无法进入。
面试口头回答
半连接和全连接对应三次握手的不同阶段。
半连接发生在第二步:服务端收到客户端的 SYN 后,回复 SYN+ACK,此时等待客户端的确认,服务端处于 SYN_RECV 状态,这个连接被存放在半连接队列里。
全连接发生在第三步完成之后:服务端收到客户端最终的 ACK,三次握手完成,连接进入 ESTABLISHED 状态,从半连接队列转移到全连接队列,等待应用层调用 accept() 来处理。
这两个队列的设计是有意义的,半连接队列可以防止 SYN Flood 攻击,全连接队列可以缓冲已完成握手但应用层还没来得及处理的连接。
1.4 TCP 四次挥手
核心要点
- 第一次:客户端发送 FIN,进入 FIN-WAIT-1
- 第二次:服务端回复 ACK,进入 CLOSE-WAIT,客户端进入 FIN-WAIT-2
- 第三次:服务端发送 FIN,进入 LAST-ACK
- 第四次:客户端回复 ACK,进入 TIME-WAIT(等待 2MSL),服务端收到后关闭
- 为什么是 4 次:TCP 全双工,需要分别关闭两个方向的数据传输
详细解析
挥手流程:
- FIN(客户端→服务端):客户端主动关闭连接,发送 FIN 报文,表示不再发送数据,但仍可接收。进入 FIN-WAIT-1。
- ACK(服务端→客户端):服务端收到 FIN 后发送 ACK,表示收到关闭请求。进入 CLOSE-WAIT,客户端收到 ACK 后进入 FIN-WAIT-2。
- FIN(服务端→客户端):服务端完成数据发送后,也发送 FIN,表示不再发送数据。进入 LAST-ACK。
- ACK(客户端→服务端):客户端收到 FIN 后发送 ACK,进入 TIME-WAIT 状态,等待 2MSL 时间后关闭。服务端收到 ACK 后直接关闭。
为什么不是 3 次? TCP 是全双工通信,发送和接收是独立的。服务端收到客户端的 FIN 后,可能还有数据没发完,不能立刻发 FIN。所以必须先 ACK 客户端的 FIN,等数据发完后再发自己的 FIN。如果合并成 3 次,会导致服务端未发送完的数据丢失。
2MSL 等待的目的:
- MSL(Maximum Segment Lifetime)是 TCP 报文段在网络中的最大生存时间,通常 30 秒到 1 分钟。
- 等待 2MSL 有两个作用:一是确保服务端收到了最后的 ACK(如果 ACK 丢失,服务端会重传 FIN,客户端有时间重发 ACK);二是让网络中残留的旧连接报文全部过期,避免干扰新连接。
面试口头回答
TCP 四次挥手的过程是这样的:
第一次,客户端主动关闭连接,发送 FIN 报文,表示不再发送数据,但还可以接收,进入 FIN-WAIT-1 状态。
第二次,服务端收到 FIN 后回复 ACK,表示收到关闭请求,进入 CLOSE-WAIT 状态。客户端收到 ACK 后进入 FIN-WAIT-2。
第三次,服务端把剩余数据发完后,也发送 FIN 报文,表示也不再发送数据了,进入 LAST-ACK 状态。
第四次,客户端收到 FIN 后回复 ACK,进入 TIME-WAIT 状态,等待 2MSL 时间后关闭。服务端收到 ACK 后直接关闭。
为什么是四次而不是三次?因为 TCP 是全双工通信,关闭连接时需要分别关闭两个方向的数据传输。服务端收到客户端的 FIN 时,可能还有数据没发完,不能立刻发 FIN,所以必须先 ACK,等数据发完再发 FIN,确保数据完整传输。
2MSL 等待的原因:MSL 是最大报文生存时间。等待 2MSL 一是保证如果最后的 ACK 丢了,服务端重传 FIN 时客户端还在,能重发 ACK;二是让网络中残留的旧报文全部过期,避免影响新连接。
1.5 服务器上有很多 2 倍 MSL(TIME_WAIT)的数据包,可能是什么原因?
核心要点
- 短连接高频交互(HTTP 1.0、未复用的 RPC)
- 服务器作为主动关闭方
- 系统回收机制保守(未启用 tcp_tw_reuse)
- 异常关闭加剧堆积
详细解析
TIME_WAIT 状态是主动关闭连接的一方进入的状态。大量 TIME_WAIT 连接通常由以下原因导致:
- 短连接高频交互:HTTP 1.0 默认非持久连接,或者 RPC 调用没有复用连接,每个请求都新建连接然后关闭,导致短时间内大量连接进入 TIME_WAIT。
- 服务器作为主动关闭方:根据 TCP 规则,主动关闭的一方会进入 TIME_WAIT。如果服务器因业务逻辑主动断开空闲连接,就会承担大量 TIME_WAIT 连接。
- 系统回收机制保守:Linux 默认会严格等待 2MSL(通常 1-2 分钟)才释放资源。如果关闭速度快于回收速度,就会堆积。
- 异常关闭:网络波动、客户端崩溃导致连接未正常完成四次挥手,服务器重传 FIN 后超时关闭,也会进入 TIME_WAIT。
解决方案:使用长连接或连接池、启用 tcp_tw_reuse 和 tcp_tw_recycle(后者在 NAT 环境慎用)、增加本地端口范围、调整 tcp_fin_timeout。
面试口头回答
服务器上出现大量 TIME_WAIT 连接,通常有以下几个原因:
第一,短连接高频交互。比如 HTTP 1.0 默认是非持久连接,或者 RPC 调用没有复用连接,每个请求都新建然后关闭,导致大量连接快速进入 TIME_WAIT。
第二,服务器是主动关闭方。TCP 规则里主动关闭连接的一方会进入 TIME_WAIT。如果服务器业务逻辑是主动断开空闲连接,那就会积累大量这种状态。
第三,系统回收机制保守。默认要等 2MSL,大概一到两分钟才释放,如果连接关闭的速度比回收速度快,就会堆积。
第四,异常关闭。网络波动或客户端崩溃导致四次挥手没完成,服务器重传 FIN 后超时关闭,也会增加 TIME_WAIT 的数量。
解决方法可以用长连接或连接池减少连接创建,或者调整内核参数比如启用 tcp_tw_reuse、缩短 tcp_fin_timeout 等。
1.6 CLOSE_WAIT 长期处于这个状态,是什么原因?
核心要点
- CLOSE_WAIT 表示本端已收到对端的 FIN,但本端还没有调用 close()
- 根本原因是应用程序没有及时关闭 Socket
- 常见:流对象未释放、连接池回收逻辑有问题
详细解析
CLOSE_WAIT 是 TCP 四次挥手中的一个中间状态。当服务端收到客户端的 FIN 报文后,会回复 ACK 并进入 CLOSE_WAIT 状态。这个状态表示"对方已经关闭发送通道,但我这边还没关闭"。
如果大量连接长期停留在 CLOSE_WAIT,说明应用程序存在资源泄漏问题。常见原因包括:
- 代码中没有及时调用
close()或shutdown()关闭 Socket - 异常处理分支遗漏了关闭逻辑
- 连接池中的连接回收逻辑有缺陷
- 业务逻辑阻塞,导致连接一直不释放
解决方法:检查代码确保每次读取完 TCP 流后都调用 close(),使用 try-finally 或 try-with-resources 保证资源释放,优化连接池的回收逻辑。
面试口头回答
CLOSE_WAIT 状态表示本端已经收到对端的关闭请求 FIN,但本端还没有调用 close() 来关闭连接。
长期停留在这个状态,根本原因是应用程序没有及时关闭 Socket。常见的情况有:代码里读写完数据后忘记调 close(),异常处理分支遗漏了关闭逻辑,或者连接池的连接回收机制有问题,导致文件描述符一直被占用。
解决方法就是检查代码,确保每次使用完连接都正确关闭,可以用 try-finally 或者 try-with-resources 保证资源释放。在连接池和长连接场景下,要优化连接的回收逻辑,防止资源泄露。
1.7 如果服务器出现大量 TIME_WAIT 连接,如何快速恢复和预防?
核心要点
- 快速恢复:缩短 TIME_WAIT 时间、启用端口复用、增加资源
- 预防:长连接、连接池、负载均衡
详细解析
快速恢复措施:
- 缩短 TIME_WAIT 持续时间:通过
tcp_fin_timeout参数减少到 30 秒 - 启用
SO_REUSEADDR和SO_REUSEPORT让端口快速复用 - 调整内核参数
tcp_tw_reuse加速 TIME_WAIT 状态的回收 - 增加系统资源:提高文件描述符限制(
ulimit -n)、增加可用端口范围
预防措施:
- 优化应用层连接管理,尽量使用长连接或连接池
- 负载均衡和分布式部署,分散连接压力
- 优化连接的生命周期管理,减少短连接频繁的创建和关闭
面试口头回答
当服务器出现大量 TIME_WAIT 连接时,可以从快速恢复和预防两个角度来说。
快速恢复方面:
- 可以缩短 TIME_WAIT 的持续时间,比如调整 tcp_fin_timeout 参数到 30 秒;
- 启用 SO_REUSEADDR 和 SO_REUSEPORT,让端口可以快速复用;
- 调整内核参数启用 tcp_tw_reuse,加速 TIME_WAIT 状态的回收;
- 增加系统资源,比如提高文件描述符上限,扩大可用端口范围。
预防方面:
- 优化应用层的连接管理,尽量使用长连接或连接池,避免频繁建立和关闭连接;
- 通过负载均衡和分布式部署分散连接压力;
- 优化连接的生命周期管理,减少短连接高频创建关闭的问题。
1.8 TCP Keepalive 机制默认多长时间触发?
核心要点
- Linux 默认:连接空闲 2 小时后开始发送探测包
- 探测间隔:75 秒
- 最多探测次数:9 次无响应后关闭连接
- 可调整参数:tcp_keepalive_time、tcp_keepalive_intvl、tcp_keepalive_probes
详细解析
TCP Keepalive 是一种保活机制,用于检测连接是否仍然有效。它的工作方式是:当连接空闲一段时间后,发送方会定期发送空的探测包(Keepalive Probe),如果对端没有响应,就认为连接已断开。
Linux 默认值:
tcp_keepalive_time= 7200 秒(2 小时):连接空闲多久后开始发送探测tcp_keepalive_intvl= 75 秒:每次探测的间隔tcp_keepalive_probes= 9:最多发送多少次探测无响应后关闭连接
这意味着,一个连接如果异常断开,最坏情况下需要 2 小时 + 9×75 秒 ≈ 2 小时 11 分钟才能发现。
在应用层,通常更推荐使用应用层心跳(如 WebSocket 的 ping/pong、MQTT 的 keepalive),因为 TCP Keepalive 的默认间隔太长,不够灵敏。
面试口头回答
TCP Keepalive 机制默认在连接空闲 2 小时后才开始发送探测包,这是 Linux 系统的默认值。
具体参数是:探测间隔 75 秒,最多发送 9 次探测包,如果都没收到响应,就关闭连接。
所以最坏情况下,发现连接断开需要大约 2 小时 11 分钟。这个默认值对很多场景来说太长了,所以实际应用中,我们通常会在应用层实现自己的心跳机制,比如 WebSocket 的 ping/pong,这样更灵敏、更可控。当然也可以通过调整 tcp_keepalive_time、tcp_keepalive_intvl 和 tcp_keepalive_probes 这些内核参数来改变默认行为。
1.9 TCP 和 UDP 的区别
核心要点
- 连接方式:TCP 面向连接,UDP 无连接
- 可靠性:TCP 可靠,UDP 不可靠
- 流量/拥塞控制:TCP 有,UDP 无
- 顺序性:TCP 保证顺序,UDP 不保证
- 头部开销:TCP 大,UDP 小
- 通信方式:TCP 一对一,UDP 支持单播/广播/组播
详细解析
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(序列号、ACK、重传) | 不可靠(可能丢包、乱序) |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有(慢启动、拥塞避免等) | 无 |
| 顺序性 | 保证按序到达 | 不保证 |
| 头部开销 | 20-60 字节 | 8 字节 |
| 通信方式 | 一对一 | 单播、广播、组播 |
| 适用场景 | 文件传输、网页浏览 | 视频会议、在线游戏、DNS |
TCP 的可靠性是以牺牲效率和实时性为代价的。UDP 虽然不可靠,但延迟低、开销小,非常适合实时性要求高的场景。
面试口头回答
TCP 和 UDP 的主要区别可以从六个方面来说:
第一,连接方式:TCP 是面向连接的,通信前需要三次握手建立连接;UDP 是无连接的,数据直接发送,不需要建立连接。
第二,可靠性:TCP 提供可靠传输,通过序列号、确认应答和重传机制保证数据完整到达;UDP 不保证可靠性,数据可能丢失、重复或乱序。
第三,流量和拥塞控制:TCP 有流量控制和拥塞控制,会根据网络状况调整发送速率;UDP 没有这些机制,发送方可以不加限制地发送。
第四,数据顺序:TCP 保证数据按发送顺序到达,如果乱序会重组;UDP 不保证顺序。
第五,头部开销:TCP 头部比较复杂,需要更多字节维护连接状态;UDP 头部只有 8 字节,开销低、传输效率高。
第六,通信方式:TCP 是一对一的;UDP 支持一对一、一对多、多对多。
使用场景上,TCP 适合需要高可靠性的应用,比如网页浏览、文件传输;UDP 适合实时性要求高、可以容忍丢包的应用,比如视频会议、在线游戏、直播这些。
1.10 TCP 可靠传输的原理
核心要点
- 连接管理:三次握手建立、四次挥手终止
- 数据分块与序号:分割报文段、唯一序号标识、重组乱序、去重
- 校验和:检测数据差错,出错则丢弃
- 重传机制:超时重传、快速重传、带确认的选择重传(SACK)
- 流量控制:滑动窗口,接收方通过 Window 字段告知可用缓冲区
- 拥塞控制:慢启动、拥塞避免、快速重传、快速恢复
详细解析
TCP 可靠传输依赖六大机制:
连接管理:三次握手确保双方通信能力正常,四次挥手防止数据残留。
数据分块与序号:发送端将数据分割为报文段,每个段分配序号。接收端通过序号重组乱序到达的数据,并去除重复序列号实现去重。
校验和:TCP 首部包含校验和字段,如果数据在传输中发生 bit 翻转,接收端计算校验和不匹配就会丢弃该包,不返回 ACK,触发发送端重传。
重传机制:
- 超时重传(RTO):每个发送的数据包启动定时器,超时未收到 ACK 就重传。缺点是效率低,如果中间某个包丢失,后续包即使到达也不能确认,可能导致不必要的批量重传。
- 快速重传:接收方收到乱序的包时,重复发送上一个已确认序号的 ACK。发送方连续收到 3 个重复 ACK 就重传对应包,无需等待超时。
- SACK(选择确认):在快速重传基础上,接收方返回已收到的报文段范围,发送方精确知道哪些包到了、哪些没到有针对性地重传。
流量控制:接收方通过 TCP 头部的 Window 字段告诉发送方自己还能接收多少数据。发送方据此控制发送速率,窗口为 0 时暂停发送,等待窗口更新。
拥塞控制:发送方维护拥塞窗口(cwnd),与接收窗口取最小值作为实际发送量。通过慢启动、拥塞避免、快速重传、快速恢复四个算法动态调整,防止网络过载。
面试口头回答
TCP 可靠传输主要通过六个机制来实现:
第一是连接管理,三次握手建立连接确保双方通信正常,四次挥手终止连接防止数据残留。
第二是数据分块和序号,发送端把数据分割成报文段,每个段分配唯一序号,接收端通过序号重组乱序数据,并去除重复包。
第三是校验和,TCP 首部有校验和字段,如果数据传输出错就丢弃该包,不返回确认,让发送端重传。
第四是重传机制,包含三种:超时重传是超过 RTO 没收到 ACK 就重传;快速重传是收到三个重复 ACK 就立即重传,不用等超时;还有选择确认 SACK,可以精确告知发送方哪些包已收到,减少不必要的重传。
第五是流量控制,接收方通过 Window 字段告诉发送方自己还能接收多少数据,发送方根据这个窗口大小控制速率,防止把接收方淹没。
第六是拥塞控制,发送方维护拥塞窗口,通过慢启动、拥塞避免、快速恢复这些算法动态调整发送速率,防止网络过载。最终发送的数据量是滑动窗口和拥塞窗口的最小值。
1.11 TCP 流量控制和拥塞控制的区别?
核心要点
- 流量控制:端到端的速率协调,防止接收方被淹没,依据接收窗口
- 拥塞控制:全局网络负载控制,防止网络过载,依据拥塞窗口
- 流量控制用滑动窗口实现;拥塞控制用慢启动、拥塞避免、快速重传/恢复实现
详细解析
| 维度 | 流量控制(Flow Control) | 拥塞控制(Congestion Control) |
|---|---|---|
| 控制目标 | 防止发送方发得太快,接收方来不及处理 | 防止过多数据注入网络,避免网络过载 |
| 关注点 | 接收端的处理能力 | 网络的承载能力 |
| 依据信息 | 接收方返回的窗口大小(Window 字段) | 网络拥塞状况(丢包、延迟增大等) |
| 实现机制 | 滑动窗口 | 慢启动、拥塞避免、快速重传、快速恢复 |
| 作用范围 | 点对点(发送方和接收方之间) | 全局(整个网络) |
实际发送速率:由 min(接收窗口, 拥塞窗口) 决定,这样既能保证不超过接收方处理能力,也不会造成网络拥塞。
面试口头回答
流量控制和拥塞控制虽然都是控制发送速率,但目标和关注点完全不同。
流量控制是端到端的速率协调,目的是防止发送方发得太快,接收方来不及处理导致丢包。它关注的是接收端的处理能力,接收方通过 TCP 头部的 Window 字段告诉发送方自己还能接收多少数据,发送方根据这个来调整。
拥塞控制是全局的网络负载控制,目的是防止过多数据注入网络,避免网络过载瘫痪。它关注的是整个网络的承载能力,根据网络拥塞状况比如丢包、延迟增大来判断,通过慢启动、拥塞避免、快速重传和快速恢复这些算法动态调整发送速率。
简单来说,流量控制是怕接收方处理不过来,拥塞控制是怕网络本身受不了。实际发送的数据量,是接收窗口和拥塞窗口的最小值,这样两边都兼顾到了。
1.12 怎么基于 TCP 自定义应用层协议?
核心要点
- TCP 是面向字节流的,不保证消息边界
- 需要自定义消息格式:固定长度头部 + 消息体
- 头部包含消息长度、类型等信息
- 解决粘包/拆包问题:先读头部获取 payload 长度,再完整读取数据
- 可增加心跳、序列号、重传、版本号等机制
详细解析
TCP 是面向字节流的协议,它只保证字节按顺序到达,不保留消息边界。这意味着应用层发送的两次数据可能被合并成一次到达(粘包),或者一次数据被拆成多次到达(拆包)。
自定义应用层协议的通常做法:
定义消息格式:每条消息由固定长度的头部 + 可变长度的消息体组成。
头部字段:至少包含 payload 长度,还可以包含消息类型、序列号、版本号、校验和等。
序列化格式:消息体可以用 JSON、Protobuf、MessagePack 等。
解析逻辑:接收端先读取固定长度的头部,解析出 payload 长度,然后读取对应长度的数据,从而精确分割消息。
增强机制:
- 心跳机制:检测连接活性
- 序列号:保证消息顺序、支持去重
- 重传机制:提高可靠性
- 版本号:保证协议可扩展性
面试口头回答
TCP 是面向字节流的,不保证消息边界,所以自定义应用层协议需要自己定义消息格式来解决粘包和拆包问题。
通常的做法是:在每条消息前面加一个固定长度的头部,头部里包含消息长度、消息类型这些信息,消息体可以用 JSON 或 Protobuf 来序列化。
接收端解析的时候,先读取固定长度的头部,从里面拿到 payload 的长度,然后再去读取对应长度的数据,这样就能精确地分割出完整的消息,解决粘包和拆包问题。
另外为了提高可靠性,还可以增加心跳机制来检测连接是否存活,加序列号来保证消息顺序和支持去重,加重传机制来提高可靠性,设计版本号来保证协议的可扩展性。
二、HTTP / HTTPS
2.1 输入 URL 到页面展示发生了什么?
核心要点
- DNS 解析域名得到 IP
- TCP 三次握手建立连接(HTTPS 还需 TLS 握手)
- 浏览器发送 HTTP 请求
- 服务器处理请求并返回响应
- 浏览器解析渲染页面
- TCP 四次挥手释放连接
详细解析
DNS 解析:浏览器检查缓存(浏览器缓存 → OS 缓存 → hosts 文件),如果没有则向 DNS 服务器递归查询,最终获取目标 IP 地址。
建立 TCP 连接:浏览器与服务器通过三次握手建立 TCP 连接。如果是 HTTPS,还需要进行 TLS/SSL 握手协商加密参数。
发送 HTTP 请求:连接建立后,浏览器构建 HTTP 请求报文(请求行 + 请求头 + 请求体),通过 TCP 发送给服务器。
服务器处理并响应:服务器解析请求,进行业务处理、数据库查询等,生成 HTTP 响应报文(状态码 + 响应头 + 响应体),返回给客户端。
浏览器解析渲染:
- 解析 HTML 构建 DOM 树
- 解析 CSS 构建 CSSOM 树
- 合并成 Render Tree,进行布局(Layout)和绘制(Paint)
- 遇到
<script>可能阻塞解析,遇到外部资源会发起额外请求
释放连接:页面加载完成后,通过四次挥手关闭 TCP 连接(HTTP/1.1 默认 keep-alive 可能复用连接)。
面试口头回答
输入 URL 到页面展示,我按步骤来说:
第一步,DNS 解析。浏览器先把 URL 里的域名解析成 IP 地址,会依次查浏览器缓存、操作系统缓存,如果没有就发请求到 DNS 服务器查询。
第二步,建立 TCP 连接。拿到 IP 后,浏览器和服务器通过三次握手建立 TCP 连接。如果是 HTTPS,还要进行 TLS 握手协商加密。
第三步,发送 HTTP 请求。连接建立后,浏览器构建 HTTP 请求报文,包含请求方法、URL、请求头这些信息,发送给服务器。
第四步,服务器处理并响应。服务器解析请求,做业务处理、查数据库,然后生成响应数据返回给浏览器。
第五步,浏览器解析渲染页面。浏览器收到响应后,解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树,然后合成渲染树,进行布局和绘制。过程中还会加载图片、JS、CSS 这些资源。
第六步,释放连接。页面加载完成后,通过四次挥手关闭 TCP 连接。不过 HTTP/1.1 默认是长连接,可能会复用连接。
2.2 输入 URL 到页面展示(从网络层结构方面回答)
核心要点
- 按 OSI 七层模型逐层分析
- 应用层:DNS 解析、HTTP 请求构建
- 传输层:TCP 三次握手、数据分段与封装
- 网络层:IP 封装、路由选择
- 数据链路层:MAC 封装、帧传输
- 物理层:信号转换与传输
详细解析
应用层:
- DNS 解析:浏览器检查缓存,向 DNS 服务器查询域名对应的 IP 地址。DNS 属于应用层协议。
- HTTP 请求:浏览器根据 URL 构建 HTTP 请求报文,包含请求方法、请求头、请求体,然后传给传输层。
传输层:
- TCP 连接建立:通过三次握手与目标服务器建立连接。
- 数据分段与封装:TCP 将 HTTP 报文分割成多个数据段,加上 TCP 首部(源端口、目的端口、序列号、确认号等),交给网络层。
网络层:
- IP 封装:为 TCP 数据段加上 IP 首部,形成 IP 数据包,包含源 IP、目的 IP。
- 路由选择:路由器根据目的 IP 查找路由表,确定下一跳,逐步转发到目标服务器。
数据链路层:
- MAC 封装:将 IP 数据包封装成帧,添加源 MAC 地址、目的 MAC 地址。
- 介质访问控制:通过 CSMA/CD 等协议避免冲突,确保数据在局域网中正确传输。
物理层:
- 将帧转换为电信号、光信号或无线信号,通过网线、光纤等物理介质传输。
- 接收端将信号转换为数字信号,逐层向上传递,最终到达应用层。
服务器处理与响应:服务器处理请求后,生成 HTTP 响应,沿相反路径传回客户端。
客户端渲染:浏览器解析 HTML、CSS、JavaScript,构建 DOM、渲染页面。
面试口头回答
从网络分层模型的角度来回答:
应用层:首先是 DNS 解析,浏览器查缓存没有的话就向 DNS 服务器查询域名对应的 IP。然后构建 HTTP 请求报文,包含请求方法、请求头这些,传给传输层。
传输层:TCP 通过三次握手建立连接,然后把应用层的 HTTP 报文分割成数据段,加上 TCP 首部,里面包含源端口、目的端口、序列号、确认号这些,交给网络层。
网络层:IP 协议给数据段加上 IP 首部,形成 IP 数据包,包含源 IP 和目的 IP。路由器根据目的 IP 查路由表,选择下一跳,把数据包逐步转发到目标服务器。
数据链路层:把 IP 数据包封装成帧,加上源 MAC 地址和目的 MAC 地址。通过 CSMA/CD 这些协议避免冲突,确保在局域网里正确传输。
物理层:把帧转换成电信号、光信号或者无线信号,通过网线、光纤传输。接收端再转换回数字信号,逐层向上传递到应用层。
服务器处理完请求后,生成响应报文,沿着相反的路径传回客户端。浏览器接收到后解析 HTML、CSS、JavaScript,构建 DOM 树、渲染页面,最终展示给用户。
2.3 DNS 域名解析
核心要点
- 解析顺序:浏览器缓存 → OS 缓存 → hosts 文件 → 本地 DNS 服务器 → 根域名服务器 → 顶级域名服务器 → 权威 DNS 服务器
- 本地 DNS 服务器会缓存结果
- 查询方式:递归查询(客户端→本地 DNS)+ 迭代查询(本地 DNS→各级服务器)
详细解析
DNS 解析是一个分层查询的过程:
- 浏览器缓存:浏览器先检查自身是否缓存过该域名的 IP。
- 操作系统缓存:浏览器缓存未命中,查询操作系统的 DNS 缓存。
- hosts 文件:查询本地 hosts 文件(通常用于开发调试)。
- 本地 DNS 服务器(递归解析器):向配置的本地 DNS 服务器(如运营商 DNS、8.8.8.8)发送查询请求。本地 DNS 会先查自己的缓存。
- 根域名服务器:本地 DNS 向根服务器查询,根服务器返回负责该顶级域(如 .com)的顶级域名服务器地址。
- 顶级域名服务器:本地 DNS 向顶级域名服务器查询,返回该域名的权威 DNS 服务器地址。
- 权威 DNS 服务器:本地 DNS 向权威服务器查询,获取最终的 IP 地址。
- 返回结果:本地 DNS 把结果返回给客户端,并缓存起来加速下次查询。
查询方式上,客户端到本地 DNS 是递归查询(我要最终结果,你帮我去查),本地 DNS 到各级服务器是迭代查询(你给我下一步该去哪查的线索)。
面试口头回答
DNS 域名解析的过程是这样的:
首先,浏览器检查自身的 DNS 缓存,如果没有命中,就查操作系统的缓存。
然后,向本地 DNS 服务器发送查询请求,本地 DNS 也会先查自己的缓存。
如果都没有,本地 DNS 就开始迭代查询:先向根域名服务器查询,根服务器返回负责 .com 这类顶级域的服务器地址;然后本地 DNS 向顶级域名服务器查询,拿到权威 DNS 服务器的地址;最后向权威服务器查询,拿到最终的 IP 地址。
本地 DNS 把解析结果返回给客户端,同时会缓存起来,下次查询就更快了。
从查询方式上说,客户端到本地 DNS 是递归查询,就是"我要结果,你帮我去查";本地 DNS 到各级服务器是迭代查询,就是"你告诉我下一步去哪查"。
2.4 GET 和 POST 的区别
核心要点
- 数据传输方式:GET 通过 URL 传参,POST 通过请求体传参
- 数据大小:GET 受 URL 长度限制(2KB-8KB),POST 理论上无限制
- 安全性:GET 参数暴露在 URL 中,POST 在请求体中相对安全(但 HTTP 下都不绝对安全)
- 幂等性:GET 是幂等的且可缓存,POST 非幂等且不可缓存
- 使用场景:GET 用于获取资源,POST 用于提交数据、修改状态
详细解析
| 特性 | GET | POST |
|---|---|---|
| 数据传输 | URL 查询参数 | 请求体(Body) |
| 数据大小 | 受浏览器/服务器限制,通常 2-8KB | 理论上无限制,受服务器配置限制 |
| 安全性 | 参数暴露在 URL,易缓存、泄露 | 在 Body 中,相对安全(但 HTTP 明文仍不安全) |
| 幂等性 | 幂等,多次请求结果相同 | 非幂等,多次请求可能产生副作用 |
| 缓存 | 可被浏览器缓存,可加入书签 | 不缓存,不适合加入书签 |
| 使用场景 | 获取资源、查询操作 | 提交数据、注册、登录、文件上传 |
注意:从 HTTP 协议规范角度,GET 和 POST 都是语义上的区别,GET 表示"获取",POST 表示"提交"。实际开发中不应以是否安全来选择 GET/POST,敏感数据必须用 HTTPS。
面试口头回答
GET 和 POST 的区别可以从这几个维度来说:
第一,数据传输方式:GET 请求通过 URL 传递数据,参数附在 URL 后面;POST 请求把数据放在请求体里,不会显示在 URL 中。
第二,数据大小限制:GET 受 URL 长度限制,一般 2KB 到 8KB;POST 理论上没有固定限制,主要看服务器配置。
第三,安全性:GET 的参数暴露在 URL 里,容易被浏览器缓存、存到历史记录或泄露出去;POST 在请求体里相对安全一些。但要注意的是,如果是 HTTP 明文传输,POST 也不是绝对安全的,敏感数据一定要用 HTTPS。
第四,幂等性和缓存:GET 是幂等的,同样的请求多次发送结果不变,而且可以被浏览器缓存,也能加入书签;POST 是非幂等的,每次请求可能产生副作用,所以不能被缓存。
第五,使用场景:GET 通常用于获取资源、查询操作,不应该对服务器数据产生副作用;POST 用于提交数据、表单提交、用户注册登录、文件上传这些会修改服务器状态的场景。
2.5 HTTP 状态码
核心要点
- 1xx:信息响应
- 2xx:成功(200 OK)
- 3xx:重定向(301 永久、302 临时)
- 4xx:客户端错误(400、403、404)
- 5xx:服务器错误(500、502、503、504)
详细解析
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | OK | 请求成功 |
| 301 | Moved Permanently | 资源永久移动到新的 URL |
| 302 | Found | 资源临时移动,自动重定向 |
| 400 | Bad Request | 请求格式错误或参数不正确 |
| 403 | Forbidden | 服务器拒绝请求,无访问权限 |
| 404 | Not Found | 请求的资源不存在或 URL 错误 |
| 500 | Internal Server Error | 服务器内部错误 |
| 502 | Bad Gateway | 网关/代理收到上游无效响应 |
| 503 | Service Unavailable | 服务器暂时不可用,维护或过载 |
| 504 | Gateway Timeout | 网关未及时收到上游响应 |
面试口头回答
HTTP 状态码主要分为几类:
2xx 是成功响应,最常见的是 200 OK,表示请求成功。
3xx 是重定向,301 表示资源永久移动到了新 URL,浏览器会自动跳转;302 是临时重定向。
4xx 是客户端错误,400 表示请求格式错误或参数不对;403 是服务器拒绝请求,没有访问权限;404 是请求的资源不存在或者 URL 写错了。
5xx 是服务器错误,500 是服务器内部错误;502 是网关或代理收到上游服务器的无效响应;503 是服务器暂时不可用,通常是维护或者过载;504 是网关超时,没能及时收到上游的响应。
2.6 HTTP 和 HTTPS 的区别?
核心要点
- 安全性:HTTP 明文传输,HTTPS 在 HTTP 基础上加入 SSL/TLS 加密
- 端口:HTTP 默认 80,HTTPS 默认 443
- HTTPS 需要 SSL/TLS 握手过程,有一定性能开销
- HTTPS 需要数字证书,由 CA 机构颁发
详细解析
HTTP(HyperText Transfer Protocol)和 HTTPS(HTTP Secure)都是 Web 传输协议,核心区别在于安全性:
- HTTP 通过明文传输数据,通信内容易被窃听、篡改或伪造(中间人攻击)。
- HTTPS 在 HTTP 和 TCP 之间增加了 SSL/TLS 安全层,通过加密、完整性校验和身份认证保护通信安全。
其他区别:
- 默认端口不同:HTTP 用 80,HTTPS 用 443
- HTTPS 握手需要额外的时间(SSL/TLS 握手),有一定性能开销,但现代硬件下影响很小
- HTTPS 需要服务器配置数字证书,由受信任的 CA(证书颁发机构)签发
- 现代浏览器对 HTTP 网站标记"不安全",SEO 也会优先收录 HTTPS
面试口头回答
HTTP 和 HTTPS 的主要区别在安全性上。
HTTP 是超文本传输协议,数据通过明文传输,通信内容容易被窃听、篡改或者伪造。
HTTPS 是在 HTTP 的基础上加入了 SSL/TLS 加密层,通过加密保证数据传输的安全性,通过完整性校验防止数据被篡改,通过数字证书验证服务器身份。
其他区别还有:HTTP 默认端口是 80,HTTPS 是 443;HTTPS 需要额外的 SSL/TLS 握手过程;HTTPS 需要服务器配置由 CA 机构颁发的数字证书。
现在的浏览器会对 HTTP 网站标记"不安全",搜索引擎也会优先收录 HTTPS 网站,所以线上服务基本都用 HTTPS。
2.7 SSL/TLS 的工作原理?
核心要点
- 核心目标:安全协商出一个对称加密的会话密钥,同时验证服务端身份
- 握手阶段(以 RSA 为例):ClientHello → ServerHello + 证书 → 客户端验证证书 + 发送预主密钥 → 双方生成会话密钥
- 通信阶段:使用对称加密(如 AES)加密实际数据,用 MAC/AEAD 保证完整性
详细解析
TLS 握手(以 RSA 密钥交换为例):
ClientHello:客户端发送支持的 TLS 版本、加密套件列表、随机数 A(client_random,32 字节)。
ServerHello + 证书:
- 服务端选择双方都支持的 TLS 版本和加密套件
- 返回随机数 B(server_random)
- 发送 CA 签名的数字证书,包含服务端公钥、域名、有效期等
客户端验证证书并发送预主密钥:
- 检查证书是否由可信 CA 颁发
- 验证证书签名,确保证书未被篡改
- 确认证书域名与当前访问域名一致(防止钓鱼)
- 生成 48 字节的预主密钥(pre-master secret)
- 用服务端公钥加密预主密钥发送给服务端(非对称加密的核心作用:只有服务端能用私钥解密)
双方生成会话密钥:
- 服务端用私钥解密得到预主密钥
- 客户端和服务端用相同的算法,通过
pre-master secret + client_random + server_random生成会话密钥(master secret),再衍生出实际的对称加密密钥和 MAC 验证密钥
通信阶段:
- 使用协商好的会话密钥(如 AES)对称加密实际数据,效率远高于非对称加密
- 通过 MAC 或 AEAD(如 GCM 模式)确保数据完整性
- 通过序列号防止重放攻击
TLS 1.3 改进:握手简化为 1-RTT(甚至 0-RTT),移除了 RSA 密钥交换(仅支持前向安全的 ECDHE),安全性更高。
面试口头回答
TLS 握手的核心是安全地协商出一个对称加密的会话密钥,同时验证服务器的身份。
以 RSA 密钥交换为例,握手分四步:
第一步 ClientHello:客户端发送支持的 TLS 版本、加密套件列表,还有一个 32 字节的随机数 A。
第二步 ServerHello + 证书:服务器选择双方都支持的版本和加密套件,返回随机数 B,然后发送 CA 签名的数字证书,证书里包含服务器的公钥。
第三步客户端验证并发送预主密钥:客户端验证证书的有效性,包括是不是可信 CA 颁发的、签名对不对、域名是否匹配。验证通过后,客户端生成一个 48 字节的预主密钥,用服务器的公钥加密后发送过去。这里非对称加密的作用就是只有服务器能用私钥解密。
第四步双方生成会话密钥:服务器解密得到预主密钥,然后双方用预主密钥加上前面的两个随机数,通过相同的算法生成对称加密的会话密钥。
之后进入通信阶段,用协商好的对称密钥加密实际数据,效率很高,同时用 MAC 或 AEAD 保证数据完整性,用序列号防止重放攻击。
2.8 非对称加密?谁用公钥?谁用私钥?
核心要点
- 公钥公开,任何人都可以使用
- 公钥用于:加密数据(只有私钥持有者能解密)、验证数字签名
- 私钥保密,仅持有者使用
- 私钥用于:解密数据、生成数字签名
详细解析
非对称加密使用一对密钥:公钥(Public Key)和私钥(Private Key),数学上相关联但无法相互推导。
公钥的使用者:
- 需要向私钥持有者发送加密信息的一方:任何人都可以用公钥加密数据,但只有对应的私钥持有者才能解密。例如用户 A 向服务器发送密码,用服务器的公钥加密,服务器用自己的私钥解密。
- 需要验证私钥持有者签名的一方:私钥持有者用私钥对数据签名,其他人用对应的公钥验证签名是否真实。例如软件发布者用私钥签名安装包,用户下载后用发布者的公钥验证,确认软件未被篡改且确实来自该发布者。
私钥的使用:必须由持有者严格保密,仅用于解密或生成签名。
面试口头回答
非对称加密体系里,公钥是公开的,私钥是保密的。
公钥的使用者有两类: 第一类是需要向私钥持有者发送加密信息的人。任何人都可以用公钥加密数据,但只有对应的私钥持有者才能解密。比如用户给服务器发密码,用服务器的公钥加密,服务器收到后用自己的私钥解密。 第二类是需要验证私钥持有者签名的人。私钥持有者用私钥生成数字签名,其他人用公钥验证签名是否真实。比如软件发布者用私钥给安装包签名,用户下载后用发布者的公钥验证,确认软件没被篡改。
私钥则必须由持有者严格保密,只用来解密数据或者生成数字签名。
2.9 HTTP 协议的多个版本
核心要点
- HTTP/1.0:短连接,每个请求新建 TCP 连接
- HTTP/1.1:引入长连接(Persistent Connection)、管道化(Pipelining),但存在队头阻塞
- HTTP/2:基于二进制帧、多路复用(Multiplexing),解决队头阻塞
- HTTP/3:基于 QUIC 协议(UDP 上实现),连接建立更快,进一步减少队头阻塞
详细解析
| 版本 | 特点 | 问题/改进 |
|---|---|---|
| HTTP/1.0 | 短连接,每个请求新建 TCP 连接 | 性能低,TCP 连接开销大 |
| HTTP/1.1 | 长连接(Connection: keep-alive)、管道化 | 管道化仍受队头阻塞影响 |
| HTTP/2 | 二进制分帧、多路复用、头部压缩(HPACK)、服务器推送 | 解决了 HTTP 层的队头阻塞,但 TCP 层的队头阻塞仍存在 |
| HTTP/3 | 基于 QUIC(UDP + 自定义可靠传输)、0-RTT 握手、连接迁移 | 避免 TCP 队头阻塞,连接建立更快,网络切换时连接不中断 |
队头阻塞(Head-of-Line Blocking):
- HTTP/1.1 中,即使使用管道化,响应也必须按请求顺序返回,如果第一个请求处理慢,后面的请求都会被阻塞。
- HTTP/2 通过多路复用解决了 HTTP 层的队头阻塞,多个请求可以在同一个 TCP 连接上并行传输。
- 但 HTTP/2 仍然基于 TCP,TCP 层的队头阻塞(一个数据包丢失导致后续所有包等待重传)仍然存在。HTTP/3 基于 QUIC(UDP),彻底解决了这个问题。
面试口头回答
HTTP 协议经历了多个版本的演进:
HTTP/1.0 是最早的版本,采用短连接,每个请求都要新建 TCP 连接,性能比较低。
HTTP/1.1 引入了长连接,可以复用同一个 TCP 连接发送多个请求,还支持管道化。但管道化容易出现队头阻塞,就是如果第一个响应慢,后面的都得等着。
HTTP/2 基于二进制帧传输,引入了多路复用,同一个连接上可以同时并发多个请求和响应,解决了 HTTP/1.x 的队头阻塞问题。还支持头部压缩和服务器推送。
HTTP/3 基于 QUIC 协议,QUIC 是在 UDP 上实现的传输层协议,比 TCP 加 TLS 更快更可靠。它可以实现更低延迟的连接建立,同时进一步减少了队头阻塞,因为 QUIC 基于 UDP,不会因为一个包的丢失阻塞其他流。
2.10 Cookie、Session、JWT
2.10.1 Cookie 和 Session 有什么区别?
核心要点
- Cookie:信息存储在客户端,每次请求随请求头发送
- Session:信息存储在服务端,客户端只存 Session ID
- Cookie 可持久化但易被篡改;Session 更安全但占用服务端资源
- 两者常结合使用:Cookie 存 Session ID
详细解析
| 特性 | Cookie | Session |
|---|---|---|
| 存储位置 | 客户端(浏览器) | 服务端 |
| 安全性 | 较低,容易被篡改 | 较高,敏感信息不暴露 |
| 存储大小 | 单个 Cookie 4KB 左右 | 理论上无限制,受服务器内存限制 |
| 生命周期 | 可设置过期时间,关闭浏览器后仍可保留 | 通常浏览器关闭或超时后失效 |
| 适用场景 | 长期保存用户偏好、非敏感信息 | 临时保存用户会话信息、敏感数据 |
两者通常结合使用:服务器创建 Session 后,把 Session ID 通过 Set-Cookie 发给客户端,客户端后续请求通过 Cookie 携带 Session ID,服务器据此找到对应的会话数据。这样就在无状态的 HTTP 上实现了有状态会话。
面试口头回答
Cookie 和 Session 最大的区别是存储位置。
Cookie 是把用户信息存储在客户端,每次请求都会随请求头发送给服务器,用于保持状态。它可以设置过期时间,浏览器关闭后仍然有效,适合长期保存用户偏好这些信息,但容易被篡改。
Session 是把用户信息存储在服务端,客户端只保存一个 Session ID,服务器通过这个 ID 就能找到对应的会话数据。Session 更安全,适合保存敏感信息,但通常浏览器关闭或超时后就失效了。
实际应用中,两者常结合使用:Cookie 里存 Session ID,这样在无状态的 HTTP 上就能实现有状态的会话。
2.10.2 JWT 令牌和传统方式有什么区别?
核心要点
- 传统 Session:服务端存储会话信息,依赖 Cookie,跨域和分布式需要额外处理
- JWT:无状态令牌,包含用户身份和权限信息,服务端无需存储会话
- JWT 更轻量、可扩展,适合分布式和跨域场景
- 注意令牌泄露和过期管理
详细解析
传统 Session-Cookie 机制的问题:
- 服务端需要存储 Session 数据,用户量大时内存压力大
- 分布式环境下需要共享 Session(如 Redis),增加了复杂度
- 跨域场景下 Cookie 受到同源策略限制
JWT(JSON Web Token)的优势:
- 无状态:令牌自包含用户信息,服务器不需要存储会话数据
- 可扩展:天然适合分布式系统和微服务架构
- 跨域友好:通过 HTTP Header 传递,不受 Cookie 同源策略限制
- 轻量:结构紧凑,由 Header.Payload.Signature 三部分组成
JWT 的结构:
- Header:声明类型和签名算法
- Payload:携带用户身份、权限、过期时间等声明
- Signature:用密钥对前两部分签名,保证不被篡改
缺点:令牌一旦签发无法撤销(除非设置黑名单),令牌体积比 Session ID 大,如果泄露攻击者可一直使用(直到过期)。
面试口头回答
传统会话机制需要服务器存储 Session 信息,依赖 Cookie 传递 Session ID。在分布式环境下需要额外做 Session 共享,跨域场景也受到 Cookie 同源策略的限制。
JWT 是无状态的令牌,把用户身份、权限这些信息直接编码在令牌里,服务器不需要存储会话数据,验证签名后就能解析出用户信息。
相比传统方式,JWT 更轻量、更适合分布式系统和跨域场景,因为只需要通过 HTTP Header 传递就行。同时用签名保证了完整性和真实性。
但 JWT 也有需要注意的地方:令牌一旦签发就不能撤销,只能等过期;令牌体积比 Session ID 大;如果泄露了,攻击者可以一直使用直到令牌过期,所以过期时间设置和泄露处理机制要做好。
2.10.3 JWT 令牌如果泄露了,怎么解决?
核心要点
- 及时将令牌标记为失效(黑名单机制)
- 刷新令牌:重新生成新令牌,旧令牌标记失效
- 设置合理的过期时间,使用 Refresh Token 机制
详细解析
JWT 本身是无状态的,一旦签发后服务器不保存任何信息,所以无法直接"注销"一个 JWT。应对泄露的措施:
黑名单机制:服务器维护一个已失效的 JWT 列表(通常用 Redis),验证时先查黑名单。但这会破坏无状态特性。
短有效期 + Refresh Token:
- Access Token 设置很短的有效期(如 15 分钟)
- Refresh Token 有效期较长(如 7 天),存储在服务端或安全的地方
- Access Token 过期后用 Refresh Token 换取新的 Access Token
- 发现泄露后,使 Refresh Token 失效,用户需要重新登录
主动刷新令牌:检测到泄露风险时,立即生成新令牌,旧令牌加入黑名单或在服务端标记失效。
面试口头回答
JWT 是无状态的,签发后服务器不保存信息,所以没法直接注销。
应对泄露主要有几个方法:
第一是黑名单机制。服务器维护一个已失效的 JWT 列表,验证时先查黑名单。但这会牺牲一部分无状态的优势。
第二是使用短有效期加 Refresh Token 机制。Access Token 设很短的有效期,比如 15 分钟,Refresh Token 设长一些存在服务端。发现泄露后,让 Refresh Token 失效,这样攻击者拿到旧的 Access Token 也很快过期,用户需要重新登录获取新的。
第三是主动刷新令牌。检测到泄露风险时,立即重新生成令牌,把旧的标记为失效,迫使用户使用新令牌。
实际项目中,短有效期配合 Refresh Token 是最常用的方案,既保证了安全性,又不会频繁要求用户登录。
2.10.4 前端是如何存储 JWT 的?
核心要点
- 存储方式:LocalStorage、SessionStorage、Cookie
- 每次请求通过 Authorization Header 或 Cookie 携带 JWT
- 服务器验证签名后解析负载得到用户信息
- 适合分布式和跨域系统
详细解析
JWT 的存储方式对比:
| 存储方式 | 特点 | 风险 |
|---|---|---|
| LocalStorage | 持久存储,跨标签页共享 | 容易受到 XSS 攻击(恶意脚本可直接读取) |
| SessionStorage | 当前标签页有效,关闭即清除 | 同样受 XSS 威胁,但生命周期短风险稍低 |
| Cookie (httpOnly) | 可设 httpOnly 防止 JS 读取,可设 Secure、SameSite | 受到 CSRF 威胁(但 SameSite 可缓解) |
最佳实践:
- Access Token 存储在内存中(减少 XSS 风险),每次页面刷新后通过 Refresh Token 重新获取
- Refresh Token 存储在 httpOnly Cookie 中,防止 XSS 窃取
- 设置合理的过期时间
- 使用 HTTPS 传输
面试口头回答
客户端登录后,服务器验证成功会生成 JWT 令牌返回给客户端。前端通常有三种存储方式:
LocalStorage,持久存储,关闭浏览器后还在,但容易受到 XSS 攻击,恶意脚本可以直接读取。
SessionStorage,只在当前标签页有效,关闭就清除,同样受 XSS 威胁但生命周期短一些。
Cookie,可以设置 httpOnly 防止 JavaScript 读取,配合 Secure 和 SameSite 属性能增强安全性,但要注意 CSRF 风险。
之后每次请求,客户端通过 Authorization Header 或者 Cookie 携带 JWT,服务器验证签名后解析负载就能得到用户信息。
实际项目中,为了安全,Access Token 可以存在内存里,Refresh Token 放在 httpOnly Cookie 中,这样既方便使用,又能降低被窃取的风险。
三、I/O 多路复用
3.1 select、poll、epoll 的区别?
核心要点
- select:有 fd 数量限制(1024),每次调用都要拷贝和扫描全部 fd,O(n)
- poll:没有 fd 限制,用链表存储,但仍是线性扫描,O(n)
- epoll:Linux 高性能方案,内核维护就绪列表,注册一次即可,事件回调机制,效率接近 O(1)
详细解析
select:
- 将所有关注的文件描述符放入一个数组(fd_set)
- 调用
select(),内核扫描整个 fd 集合,标记出可读/可写的 - 用户态再遍历一次找到就绪的 fd
- 每次调用都需要重新传递整个 fd 集合
- 缺点:fd 数量限制 1024;每次都要从用户态拷贝 fd 集;O(n) 复杂度
poll:
- 改进版的 select,用链表结构存放 fd,不再受 1024 限制
- 但每次调用仍需拷贝整个 fd 集,仍需遍历所有 fd,时间复杂度依然是 O(n)
epoll(Linux 2.6 引入):
核心思想:将监听的 fd 注册到内核的事件表中,通过事件回调机制维护就绪列表
三个关键函数:
epoll_create创建实例、epoll_ctl注册事件、epoll_wait获取就绪 fd内核结构:红黑树存储所有监听的 fd,就绪链表存储已就绪的 fd
回调机制:内核检测到事件时直接把 fd 放入就绪队列
触发模式:
- LT(水平触发,默认):只要数据没读完都会通知
- ET(边缘触发):只在状态变化时通知一次,效率更高,但需要非阻塞 I/O
性能优势:fd 注册一次即可,不再频繁拷贝;内核主动推送事件;时间复杂度 O(1)
面试口头回答
select、poll、epoll 都是 I/O 多路复用机制,但性能和实现方式差别很大。
select 把所有关注的文件描述符放进一个数组,调用 select 的时候内核扫描整个集合,标记出可读可写的,然后用户态再遍历找就绪的。它有两个明显缺点:一是 fd 数量限制 1024,二是每次调用都要从用户态拷贝 fd 集,还要线性扫描,复杂度是 O(n)。
poll 是 select 的改进版,用链表存 fd,没有 1024 的限制了,但底层仍然是线性扫描,每次调用也要拷贝全部 fd,复杂度还是 O(n)。
epoll 是 Linux 的高性能方案。它的核心思想是把监听的 fd 注册到内核的事件表里,内核通过事件回调维护一个就绪列表。用户态通过 epoll_wait 直接拿到就绪的 fd,不需要遍历全部。epoll 有三个关键函数:create 创建实例、ctl 注册事件、wait 等待事件。内核里用红黑树存所有 fd,用就绪链表存已经就绪的 fd,有事件时通过回调直接把 fd 放进就绪队列。
epoll 的优势很明显:fd 注册一次就行,不用每次都拷贝;内核主动推送事件;时间复杂度接近 O(1)。所以高并发场景比如 Nginx、Redis 都用 epoll。
另外 epoll 有两种触发模式,水平触发 LT 是默认模式,数据没读完会一直通知;边缘触发 ET 只在状态变化时通知一次,效率更高,但需要配合非阻塞 I/O 使用。
3.2 epoll 惊群问题
核心要点
- 多个线程/进程等待同一个 epoll fd,事件发生时内核唤醒所有等待者
- 实际上只有一个线程能处理,其余线程空转浪费 CPU
- 产生原因:水平触发(LT)+ 多线程监听同一个 fd
- 解决方法:使用 ET 边缘触发、单线程监听 + 多工作线程、应用层加锁或事件队列
详细解析
惊群问题的本质:事件通知机制唤醒了过多线程,造成 CPU 空转。
产生场景:
- 多个线程/进程都调用
epoll_wait()监听同一个 epoll 实例 - 当某个 fd 上有事件发生时,内核会唤醒所有等待的线程
- 但只有一个线程能真正处理该事件(通过
accept()或read()) - 其他被唤醒的线程发现没有可处理的事件,只能再次阻塞
- 这种"一呼百应但只有一个能用"的现象就是惊群
解决方法:
- 使用 EPOLLET(边缘触发):线程只在状态变化时被唤醒,减少不必要的唤醒
- 单线程监听 epoll fd,多工作线程处理任务:一个专门的线程负责
epoll_wait(),拿到事件后分发给线程池处理 - 应用层加锁或事件队列:多线程监听时,通过锁或事件队列协调,防止多个线程竞争同一个 fd
- 现代 Linux 内核优化:内核对 epoll 惊群做了一些优化,但高并发场景仍需注意设计
面试口头回答
epoll 惊群问题指的是多个线程或进程同时等待同一个 epoll 文件描述符,当某个事件发生时,内核会唤醒所有等待的线程,但实际上只有一个线程能处理这个事件,其他线程被唤醒后发现没事可做,只能再次阻塞,导致 CPU 空转浪费。
产生的原因主要是水平触发模式加上多线程监听同一个 fd。
解决方法有几个:
- 使用 EPOLLET 边缘触发,线程只在状态变化时被唤醒,减少不必要的唤醒;
- 单线程监听 epoll,多个工作线程处理任务,避免多个线程竞争同一个 fd;
- 应用层通过锁或事件队列来控制,协调线程处理事件;
- 现代 Linux 内核对惊群问题也做了一些优化,但高并发场景还是要注意设计。
总结来说,惊群问题的本质是事件通知机制唤醒了过多线程,解决思路就是减少不必要的唤醒或者协调线程处理事件。
3.3 应用程序怎么知道客户端给它发了请求?
核心要点
- TCP 三次握手完成后连接放入全连接队列
- accept() 从全连接队列取出连接,生成新的通信 socket
- 客户端发送请求后,数据放入 socket 的接收缓冲区
- 通过 I/O 多路复用(select/poll/epoll)通知服务端 socket 可读
- 服务端调用 recv()/read() 从内核态拷贝到用户态处理
详细解析
- 建立连接:客户端与服务端完成 TCP 三次握手后,连接被放入服务端的全连接队列。
- accept 接收连接:服务端调用
accept()从全连接队列中取出连接,生成新的通信 socket,放入内核缓冲区,准备进行数据读写。 - 请求就绪检测:客户端发送请求后,内核将数据放入对应 socket 的接收缓冲区。通过 I/O 多路复用机制(select/poll/epoll),内核会通知服务端该 socket 已经可读/就绪。
- 数据读取与处理:服务端调用
recv()或read(),将数据从内核态拷贝到用户态,随后进行业务逻辑处理。
面试口头回答
应用程序知道客户端发了请求,主要分几步:
第一步是建立连接。客户端和服务端完成 TCP 三次握手后,连接会被放到服务端的全连接队列里。
第二步是 accept 接收连接。服务端调用 accept 函数,从全连接队列里取出连接,生成一个新的通信 socket,准备进行数据读写。
第三步是请求就绪检测。客户端发送请求后,内核会把数据放到对应 socket 的接收缓冲区。服务端通过 I/O 多路复用机制,比如 epoll,内核会通知服务端这个 socket 已经可读了。
第四步是读取和处理。服务端调用 recv 或 read,把数据从内核态拷贝到用户态,然后进行业务逻辑处理。
四、网络模型
4.1 七层网络协议(OSI 参考模型)
核心要点
- OSI 模型将网络通信划分为 7 个层次,每层负责特定功能
- 层与层之间通过接口协作,下层为上层提供服务
- 好处:模块化设计、兼容性标准化、便于故障排查、促进技术迭代
详细解析
| 层次 | 名称 | 功能 | 典型协议/设备 | 数据单元 |
|---|---|---|---|---|
| 第 7 层 | 应用层 | 为用户提供网络服务接口 | HTTP、FTP、SMTP、DNS、SSH | 报文 |
| 第 6 层 | 表示层 | 数据格式转换、加密、压缩 | SSL/TLS、ASCII、UTF-8、gzip | 报文 |
| 第 5 层 | 会话层 | 建立、管理、终止会话 | 数据库连接、RPC 会话 | 报文 |
| 第 4 层 | 传输层 | 端到端通信、可靠/快速传输 | TCP、UDP | 段 |
| 第 3 层 | 网络层 | 跨网络路由和转发 | IP、ICMP、路由器 | 包 |
| 第 2 层 | 数据链路层 | 相邻设备间帧传输、MAC 寻址 | 以太网、交换机 | 帧 |
| 第 1 层 | 物理层 | 比特流传输、物理介质 | 网线、光纤、网卡 | 比特 |
各层详解:
- 物理层:负责传输原始比特流(0 和 1),定义电压、接口类型、传输介质(网线、光纤、无线信号)、传输速率等。典型设备:网线、光纤、网卡物理接口。
- 数据链路层:将比特流封装成"帧",负责相邻设备间的直接通信,通过 MAC 地址实现局域网内寻址。包含两个子层:LLC(逻辑链路控制)和 MAC(介质访问控制,如 CSMA/CD)。典型设备:交换机。
- 网络层:实现不同网络之间的数据包路由和转发,通过 IP 地址进行跨网络寻址,选择最佳传输路径,处理网络拥塞。核心协议:IP、ICMP、OSPF、RIP。典型设备:路由器。
- 传输层:为源主机和目标主机的应用程序提供端到端的数据传输服务,负责数据分段、重组、流量控制和差错恢复。TCP 提供可靠传输,UDP 提供快速传输。
- 会话层:建立、管理和终止两个应用程序之间的"会话",控制会话中的数据交换顺序,提供会话恢复(如断网后重连)。典型场景:数据库连接的建立与关闭、视频会议的会话控制。
- 表示层:处理数据的格式转换,确保发送方和接收方的应用程序能理解数据格式,包括编码(ASCII、UTF-8)、加密(SSL/TLS)、压缩(gzip)等。
- 应用层:直接为用户应用程序提供网络服务,定义应用程序之间的通信规则。常见协议:HTTP、FTP、SMTP、DNS、SSH。
七层模型的好处:
- 模块化设计:每层独立负责特定功能,开发者可专注于某一层优化,无需关注其他层。
- 兼容性与标准化:统一标准让不同厂商设备兼容通信。
- 便于故障排查:网络出问题时可逐层排查,快速定位故障点。
- 促进技术迭代:某一层技术更新不影响其他层,如 IPv4 升级到 IPv6 只影响网络层。
面试口头回答
OSI 七层模型把网络通信从上到下分为七层:
第七层应用层,直接给用户应用提供网络服务,比如 HTTP、FTP、DNS、SSH 这些协议都工作在应用层。
第六层表示层,负责数据格式转换、加密解密、压缩解压,比如 UTF-8 编码、TLS 加密、gzip 压缩。
第五层会话层,建立和管理应用程序之间的会话,比如数据库连接的建立和关闭、视频会议的会话控制。
第四层传输层,提供端到端的通信,TCP 保证可靠传输,UDP 提供快速传输,通过端口号区分不同的应用程序。
第三层网络层,负责跨网络的数据包路由和转发,通过 IP 地址寻址,选择最佳传输路径。核心协议是 IP、ICMP,典型设备是路由器。
第二层数据链路层,把比特流封装成帧,负责相邻设备间的传输,通过 MAC 地址在局域网内寻址。典型设备是交换机。
第一层物理层,负责传输原始比特流,定义电压、接口、传输介质这些物理特性。比如网线、光纤、网卡。
这个分层模型的好处是模块化设计,每层职责清晰;标准化后不同厂商的设备可以兼容;出了问题可以逐层排查;某一层技术更新不会影响其他层,便于技术演进。
评论区