【计算机网络系列 1/3】网络基础与TCP协议:从生活场景理解三次握手
导语 :作为一名Java程序员,你是否曾经遇到过接口调用超时、连接被拒绝、数据传输异常等问题?这些问题的背后往往都与计算机网络密切相关。本文将用通俗易懂的方式,带你系统地学习网络基础和TCP协议,并解释为什么要这样设计 ,帮助你建立完整的知识体系。
一、引言:为什么你要学网络?
1.1 从痛点场景出发
想象一下这样的场景:
java
// 场景1:接口偶尔超时,用户投诉不断
@FeignClient(name = "user-service")
public interface UserService {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id); // 偶尔超时,为什么?是DNS慢?TCP握手慢?还是服务端处理慢?
}
// 场景2:高并发下连接池耗尽,系统崩溃
@RestController
public class OrderController {
@Autowired
private DataSource dataSource;
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest request) {
// 高峰期报错:Cannot get connection from pool
// 怎么办?盲目增加连接池大小?还是优化代码?
}
}
不懂网络知识的后果:
- ❌ 只能盲目增加超时时间,治标不治本
- ❌ 只会重启服务,问题反复出现
- ❌ 不知道如何优化,性能瓶颈无法突破
懂网络知识的好处:
- ✅ 能通过抓包分析是DNS慢、TCP握手慢、还是服务端处理慢
- ✅ 能合理配置连接池参数,避免资源浪费
- ✅ 能设计合理的重试和降级策略,提升系统稳定性
1.2 餐厅点菜类比:理解客户端-服务器模型
让我们用一个生活中的例子来理解网络通信:
你(客户端)去餐厅吃饭:
1. 你看菜单点菜 → 发送HTTP请求
2. 服务员记录你的需求 → 网络协议处理
3. 服务员把订单送到厨房 → 数据传输
4. 厨房做好菜 → 服务器处理
5. 服务员把菜端给你 → 返回响应
如果服务员(网络协议)不懂规矩,会出现什么问题?
- 点菜没说清楚(请求格式错误)→ 400 Bad Request
- 厨房太忙响应慢(服务器过载)→ 503 Service Unavailable
- 菜送错了(数据传输出错)→ 需要重传机制
这就是为什么你要学网络:理解"服务员"的工作流程,才能快速定位问题!
1.3 学习目标
学完本文后,你将能够:
- 🎯 理解网络分层的本质原因(解耦、复用、可替换)
- 🎯 掌握TCP三次握手/四次挥手的为什么(而非死记硬背)
- 🎯 能在实际工作中分析连接相关问题
- 🎯 能用生活案例解释给非技术人员听
二、网络分层:快递物流系统的启示
2.1 为什么要分层?
类比:网购一本书的完整流程
想象你在淘宝买了一本书,整个配送过程是这样的:
【应用层】你在淘宝下单(选择商品、填写地址、支付)
↓
【传输层】快递公司承诺送达(顺丰保证次日达)
↓
【网络层】规划最优路线(北京→石家庄→郑州→武汉→长沙)
↓
【链路层】每个运输段的具体方式(公路卡车/铁路货车/航空飞机)
↓
【物理层】实际的交通工具(沥青路/铁轨/航线)
关键洞察:
- 淘宝(应用层)不关心快递用什么车(物理层)
- 顺丰(传输层)不关心具体走哪条路(网络层)
- 每层各司其职,互不干扰
分层的核心价值
| 价值 | 说明 | 举例 |
|---|---|---|
| 解耦 | 各层独立变化,互不影响 | 淘宝改版不影响快递运输 |
| 复用 | 下层可以被多个上层复用 | 同一条公路可以运书、运衣服、运电子产品 |
| 可替换 | 某一层实现改变不影响其他层 | 公路坏了可以改铁路,淘宝无感知 |
如果没有分层会怎样?
❌ 淘宝需要知道:
- 快递公司的车辆型号
- 每条路的拥堵情况
- 司机的驾驶习惯
😱 复杂度爆炸!根本无法维护!
2.2 OSI七层 vs TCP/IP四层:理论vs实践
OSI七层模型(教科书版)
7️⃣ 应用层 - 用户直接接触(HTTP、FTP、SMTP)
6️⃣ 表示层 - 数据格式转换(加密、压缩、编码)
5️⃣ 会话层 - 建立和维护会话(Session管理)
4️⃣ 传输层 - 端到端传输(TCP、UDP)
3️⃣ 网络层 - 路由选择(IP、ICMP)
2️⃣ 数据链路层 - 相邻节点传输(以太网、WiFi)
1️⃣ 物理层 - 比特流传输(光纤、电缆、无线电波)
TCP/IP四层模型(实际使用)
4️⃣ 应用层 - 合并了OSI的应用层+表示层+会话层
3️⃣ 传输层 - TCP、UDP
2️⃣ 网际层 - IP、ICMP、ARP
1️⃣ 网络接口层 - 数据链路层+物理层
对比表格
| 维度 | OSI七层 | TCP/IP四层 |
|---|---|---|
| 层数 | 7层 | 4层 |
| 特点 | 理论完美,分层细致 | 实用主义,效率高 |
| 使用场景 | 教科书、考试、理论研究 | 实际开发、互联网标准 |
| 表示层功能 | 单独一层 | 由应用层处理(如HTTPS加密) |
| 会话层功能 | 单独一层 | 由应用层处理(如Session、Token) |
记忆口诀:
OSI七层:应表会传网数物
谐音记忆:英标会穿袜子捂(英国标准会穿袜子捂脚😂)
TCP/IP四层:应传网接
谐音记忆:鹰穿网街(老鹰穿过网状的街道)
为什么实际使用TCP/IP四层?
原因1:OSI过于理想化
- 表示层和会话层的功能在很多场景中不需要独立存在
- 例如:HTTPS在应用层处理加密(表示层功能),Session在应用层管理(会话层功能)
原因2:效率优先
- 分层太细导致每层都要处理头部信息,效率低
- TCP/IP合并层次,减少开销
原因3:历史原因
- TCP/IP协议栈先于OSI模型出现
- 互联网基于TCP/IP构建,已成事实标准
2.3 各层协议速查表
| 层级 | 核心职责 | 常见协议 | 为什么需要这个协议? | Java示例 |
|---|---|---|---|---|
| 应用层 | 定义数据格式和语义 | HTTP、HTTPS、FTP、DNS、SMTP | 让不同应用能互相理解 | @RestController |
| 传输层 | 端到端的可靠/不可靠传输 | TCP、UDP | TCP保证不丢包,UDP追求速度 | ServerSocket / DatagramSocket |
| 网际层 | 跨网络的路由转发 | IP、ICMP、ARP | 让数据包能找到目标地址 | InetAddress |
| 网络接口层 | 物理介质上的传输 | Ethernet、WiFi、PPP | 将数字信号转换为物理信号 | 无需关心(操作系统处理) |
实际应用中的体现:
java
// 应用层:Spring MVC控制器
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}
// 传输层:TCP Socket
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
// 网际层:IP地址解析
InetAddress address = InetAddress.getByName("www.example.com");
System.out.println("IP地址: " + address.getHostAddress());
// 网络接口层:由操作系统和网卡驱动处理,Java程序无需关心
三、TCP协议:可靠传输的艺术
3.1 TCP vs UDP:打电话 vs 发微信语音
生活化类比
TCP(打电话):
📞 你:"喂,听得见吗?"
📞 对方:"听得见,你说"
📞 你:"今天天气不错"
📞 对方:"嗯,确实不错"
✅ 双方确认都能正常通信,消息不会丢失
UDP(发微信语音):
📱 你:发送语音消息
❌ 对方可能没收到(网络不好)
❌ 你可能也不知道对方是否收到
⚡ 但是速度快,适合不重要的消息
对比表格
| 特性 | TCP(打电话) | UDP(发微信语音) |
|---|---|---|
| 连接 | 需要建立连接(三次握手) | 无连接,直接发送 |
| 可靠性 | ✅ 保证送达(不丢包) | ❌ 可能丢失 |
| 顺序 | ✅ 保证顺序(不乱序) | ❌ 可能乱序 |
| 速度 | 较慢(有确认机制) | 较快(无确认机制) |
| 流量控制 | ✅ 有(滑动窗口) | ❌ 无 |
| 适用场景 | Web浏览、邮件、文件传输 | 视频直播、在线游戏、DNS查询 |
Java代码示例
java
// TCP Socket(可靠传输)
import java.io.*;
import java.net.*;
// 服务端
public class TcpServer {
public static void main(String[] args) throws IOException {
// 创建ServerSocket,监听8080端口
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("服务器启动,等待连接...");
// 阻塞等待客户端连接(三次握手在此完成)
Socket socket = serverSocket.accept();
System.out.println("客户端已连接: " + socket.getInetAddress());
// 读取客户端发送的数据
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String message = in.readLine();
System.out.println("收到消息: " + message);
// 回复客户端
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("收到你的消息: " + message);
// 关闭连接(四次挥手)
socket.close();
serverSocket.close();
}
}
// 客户端
public class TcpClient {
public static void main(String[] args) throws IOException {
// 创建Socket,连接到服务器(发起三次握手)
Socket socket = new Socket("localhost", 8080);
// 发送消息
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
out.println("Hello, Server!");
// 接收回复
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
String response = in.readLine();
System.out.println("服务器回复: " + response);
socket.close();
}
}
// UDP Socket(快速传输)
import java.net.*;
public class UdpServer {
public static void main(String[] args) throws IOException {
// 创建DatagramSocket,监听9090端口
DatagramSocket socket = new DatagramSocket(9090);
byte[] buffer = new byte[1024];
// 接收数据包(可能收不到,没有保证)
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength());
System.out.println("收到消息: " + message);
socket.close();
}
}
public class UdpClient {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket();
// 准备数据
String message = "Hello, UDP!";
byte[] buffer = message.getBytes();
// 发送到指定地址和端口
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(
buffer, buffer.length, address, 9090
);
socket.send(packet);
socket.close();
}
}
3.2 三次握手详解:为什么不是两次或四次?
时间线图解
客户端 (CLOSED → SYN_SENT → ESTABLISHED) 服务器 (CLOSED → LISTEN → SYN_RCVD → ESTABLISHED)
| |
| --- SYN=1, seq=100 --------------------------------> | 【第一次】我想建立连接
| |
| <-- SYN=1, ACK=1, seq=200, ack=101 -----------------| 【第二次】好的,我也想建立连接
| |
| --- ACK=1, ack=201 --------------------------------> | 【第三次】好的,我们开始通信吧
| |
报文段字段说明:
SYN=1:同步标志,表示这是连接请求ACK=1:确认标志,表示确认号有效seq:序列号,随机生成的初始值ack:确认号,期望收到的下一个字节序号
状态机变化
客户端状态变化:
CLOSED(关闭)
→ SYN_SENT(已发送SYN,等待确认)
→ ESTABLISHED(连接建立)
服务器状态变化:
CLOSED(关闭)
→ LISTEN(监听状态)
→ SYN_RCVD(收到SYN,已发送SYN+ACK)
→ ESTABLISHED(连接建立)
核心问题:为什么不能两次握手?
场景模拟:假设只有两次握手
时间线:
T1: 客户端A发送SYN请求(seq=100)到服务器
T2: 服务器回复SYN+ACK(seq=200, ack=101)
T3: 客户端A认为连接建立,开始发送数据
T4: ⚠️ 但客户端A的ACK在路上延迟了...(网络拥堵)
T5: 客户端A超时,重传SYN(seq=100)
T6: 服务器又回复SYN+ACK(seq=300, ack=101)
T7: 客户端A收到,发送ACK,新连接建立
T8: 🔴 问题来了:之前延迟的ACK突然到达服务器!
T9: 服务器以为又有一个新连接,分配资源
T10: 💥 服务器维护了两个连接,但客户端只有一个!
结论 :两次握手无法防止已失效的连接请求报文段突然又传送到了服务端,导致服务器浪费资源创建无效连接。
三次握手的作用:
- ✅ 确认双方的发送能力正常
- ✅ 确认双方的接收能力正常
- ✅ 同步双方的初始序列号
- ✅ 防止已失效的连接请求突然到达
为什么不需要四次握手?
因为第二次握手时,服务器可以同时发送两个信息:
SYN:我也想建立连接ACK:我收到你的SYN了
这两个信息可以合并在一个报文段中,所以三次就够了。
如果分成四次:
第二次:服务器只发送ACK(我收到你的SYN)
第三次:服务器再发送SYN(我也想建立连接)
第四次:客户端发送ACK
❌ 没必要!第二次就可以一起发送SYN+ACK
记忆口诀
三次握手建连接,
防止旧连来捣乱。
SYN、SYN+ACK、ACK完,
双方确认才安全。
Spring Boot优化配置
yaml
# application.yml - Tomcat连接优化
server:
tomcat:
accept-count: 100 # 等待队列长度(超过max-connections后的请求排队)
max-connections: 8192 # 最大连接数(同时处理的连接数)
threads:
max: 200 # 最大工作线程数
min-spare: 10 # 最小空闲线程数
connection-timeout: 20000 # 连接超时时间(毫秒)
参数说明:
accept-count:当连接数达到max-connections后,新请求进入等待队列,队列长度为100max-connections:Tomcat能同时处理的最大连接数(包括正在处理和等待的)connection-timeout:三次握手的超时时间,超过20秒未完成则断开
3.3 四次挥手详解:为什么比握手多一次?
关键原因:TCP是全双工通信
全双工:数据可以在两个方向上同时传输(就像双向车道)
类比:两个人打电话
客户端说:"我没话说了"(FIN)
服务器说:"好的,我知道了"(ACK)
⏳ 但服务器可能还有话要说...
服务器说:"我也没话说了"(FIN)
客户端说:"好的,再见"(ACK)
因为是双向独立的,所以需要分别关闭两个方向的连接,因此需要四次。
时间线图解
客户端 (ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED)
| |
| --- FIN=1, seq=u ---------------------------------------------> | 【第一次】我没数据要发了
| |
| <-- ACK=1, ack=u+1 --------------------------------------------| 【第二次】好的,我知道了
| |
| | ⏳ 服务器可能还有数据要发送...
| |
| <-- FIN=1, ACK=1, seq=v, ack=u+1 ------------------------------| 【第三次】我也没数据了
| |
| --- ACK=1, ack=v+1 --------------------------------------------> | 【第四次】好的,再见
| |
服务器 (ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED)
为什么需要TIME_WAIT状态?
客户端发送最后一个ACK后,进入TIME_WAIT状态,持续2MSL(Maximum Segment Lifetime,最大报文段生存时间,通常60秒)。
原因1:确保服务器收到最后一个ACK
如果最后一个ACK丢失:
- 服务器会重传FIN
- 客户端在TIME_WAIT状态下可以重新发送ACK
- 如果客户端直接进入CLOSED,就无法响应服务器的重传
原因2:让旧连接的报文在网络中消失
网络中可能还残留着旧连接的报文段:
- 等待2MSL时间,确保所有旧报文都已过期
- 避免新连接收到旧连接的报文,造成混乱
实战命令:查看TIME_WAIT连接
bash
# Linux/Mac
netstat -an | grep TIME_WAIT | wc -l
# Windows
netstat -an | findstr TIME_WAIT
# 查看详细信息
netstat -an | grep TIME_WAIT
# 输出示例:
# tcp 0 0 192.168.1.100:8080 192.168.1.200:54321 TIME_WAIT
# tcp 0 0 192.168.1.100:8080 192.168.1.200:54322 TIME_WAIT
# ...
如果TIME_WAIT连接过多怎么办?
bash
# 方案1:开启tcp_tw_reuse(Linux)
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# 方案2:调整内核参数
sudo sysctl -w net.ipv4.tcp_fin_timeout=30 # 默认60秒,改为30秒
# 方案3:使用连接池复用连接(推荐)
# 在应用中配置HikariCP、Druid等连接池
优化方案
Spring Boot配置:
yaml
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数
minimum-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 获取连接超时30秒
idle-timeout: 600000 # 空闲超时10分钟
max-lifetime: 1800000 # 最大生命周期30分钟
keepalive-time: 30000 # 保持活跃时间30秒
代码示例:监控连接状态
java
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPoolMXBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class ConnectionMonitorController {
@Autowired
private HikariDataSource dataSource;
@GetMapping("/monitor/connections")
public Map<String, Object> getConnectionStatus() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
Map<String, Object> status = new HashMap<>();
status.put("activeConnections", poolMXBean.getActiveConnections());
status.put("idleConnections", poolMXBean.getIdleConnections());
status.put("totalConnections", poolMXBean.getTotalConnections());
status.put("threadsAwaitingConnection", poolMXBean.getThreadsAwaitingConnection());
status.put("maxPoolSize", poolMXBean.getMaximumPoolSize());
// 告警:如果使用率超过80%
double usageRate = (double) poolMXBean.getActiveConnections()
/ poolMXBean.getMaximumPoolSize();
if (usageRate > 0.8) {
status.put("warning", "连接池使用率超过80%,建议优化!");
}
return status;
}
}
3.4 TCP可靠性保证机制
五大机制
| 机制 | 作用 | 类比 |
|---|---|---|
| 序列号+确认应答 | 每个字节都有编号,接收方确认收到 | 快递单号,签收确认 |
| 超时重传 | 规定时间内未收到ACK,重新发送 | 快递丢失,重新发货 |
| 流量控制 | 滑动窗口,控制发送速度 | 根据对方处理能力调整发送节奏 |
| 拥塞控制 | 避免网络拥堵(慢启动、拥塞避免等) | 交通拥堵时减速慢行 |
| 校验和 | 检测数据是否损坏 | 快递包装完整性检查 |
滑动窗口示意图
发送方窗口:
[已发送已确认][已发送未确认][可发送][待发送]
←---- 窗口大小 ----→
接收方窗口:
[已接收已读取][已接收未读取][可接收][缓冲区满]
←---- 窗口大小 ----→
工作原理:
1. 发送方维护一个发送窗口,窗口内的数据可以发送
2. 收到ACK后,窗口向前滑动
3. 接收方通过通告窗口大小(rwnd)告诉发送方自己的接收能力
4. 发送方根据rwnd调整发送速度,避免接收方缓冲区溢出
代码示例:查看Socket连接信息
java
import java.net.*;
import java.io.*;
public class SocketInfoDemo {
public static void main(String[] args) throws IOException {
// 创建Socket连接
Socket socket = new Socket("www.baidu.com", 80);
// 查看本地信息
System.out.println("本地地址: " + socket.getLocalAddress());
System.out.println("本地端口: " + socket.getLocalPort());
// 查看远程信息
System.out.println("远程地址: " + socket.getInetAddress());
System.out.println("远程端口: " + socket.getPort());
// 查看连接状态
System.out.println("是否连接: " + socket.isConnected());
System.out.println("是否关闭: " + socket.isClosed());
System.out.println("是否绑定: " + socket.isBound());
// 设置Socket选项
socket.setSoTimeout(5000); // 读取超时5秒
socket.setKeepAlive(true); // 开启心跳保活
socket.setTcpNoDelay(true); // 禁用Nagle算法(立即发送)
socket.close();
}
}
Socket选项说明:
setSoTimeout:读取数据超时时间,避免无限等待setKeepAlive:开启TCP心跳,检测连接是否仍然有效setTcpNoDelay:禁用Nagle算法,小数据包立即发送(适用于实时性要求高的场景)
四、实战案例分析
4.1 案例1:接口调用超时排查
问题现象
java
// 用户反馈:查询用户信息接口偶尔超时(超过5秒)
@FeignClient(name = "user-service")
public interface UserService {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
}
排查步骤
步骤1:检查DNS解析时间
bash
# 查看DNS解析是否正常
nslookup api.example.com
# 测量DNS解析耗时
time nslookup api.example.com
# 输出示例:
# Server: 8.8.8.8
# Address: 8.8.8.8#53
#
# Non-authoritative answer:
# Name: api.example.com
# Address: 93.184.216.34
#
# real 0m0.050s ← DNS解析耗时50ms,正常
# user 0m0.010s
# sys 0m0.005s
步骤2:检查TCP连接建立时间
bash
# 使用telnet测试连接
telnet api.example.com 443
# 或使用curl查看详细时间分解
curl -w "@curl-format.txt" -o /dev/null -s https://api.example.com/users/123
curl-format.txt模板:
txt
time_namelookup: %{time_namelookup}\n
time_connect: %{time_connect}\n
time_appconnect: %{time_appconnect}\n
time_pretransfer: %{time_pretransfer}\n
time_redirect: %{time_redirect}\n
time_starttransfer: %{time_starttransfer}\n
----------\n
time_total: %{time_total}\n
输出示例:
time_namelookup: 0.050 # DNS解析耗时50ms
time_connect: 0.120 # TCP连接建立耗时70ms(120-50)
time_appconnect: 0.250 # SSL握手耗时130ms(250-120)
time_pretransfer: 0.250
time_redirect: 0.000
time_starttransfer: 0.800 # 服务器处理耗时550ms(800-250)
----------
time_total: 0.850 # 总耗时850ms
分析:
- 如果
time_namelookup大 → DNS解析慢,考虑使用DNS缓存或CDN - 如果
time_connect大 → TCP握手慢,检查网络质量 - 如果
time_appconnect大 → SSL握手慢,考虑Session复用 - 如果
time_starttransfer - time_appconnect大 → 服务器处理慢,优化代码
步骤3:抓包分析
bash
# 使用tcpdump抓包
sudo tcpdump -i any host api.example.com -w capture.pcap
# 用Wireshark打开capture.pcap,过滤TCP流
# 查看三次握手耗时、数据传输耗时
Wireshark分析要点:
- 找到TCP三次握手(SYN、SYN+ACK、ACK)
- 计算握手耗时(ACK时间 - SYN时间)
- 查看是否有重传(Retransmission)
- 分析RTT(Round Trip Time,往返时间)
解决方案
Feign客户端优化:
java
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
/**
* 配置超时时间
*/
@Bean
public Request.Options options() {
return new Request.Options(
5000, // 连接超时5秒(TCP握手+SSL握手)
10000 // 读取超时10秒(服务器处理时间)
);
}
/**
* 配置重试机制
*/
@Bean
public Retryer retryer() {
return new Retryer.Default(
1000, // 初始重试间隔1秒
3000, // 最大重试间隔3秒
3 // 最多重试3次
);
}
}
// 使用配置
@FeignClient(name = "user-service", configuration = FeignConfig.class)
public interface UserService {
@GetMapping("/users/{id}")
User getUser(@PathVariable Long id);
}
熔断降级(Resilience4j):
java
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public User getUser(Long id) {
// 调用远程服务
return remoteUserService.getUser(id);
}
/**
* 降级方法:当远程服务不可用时返回默认值
*/
public User getUserFallback(Long id, Throwable throwable) {
log.warn("用户服务调用失败,返回默认用户", throwable);
User defaultUser = new User();
defaultUser.setId(id);
defaultUser.setName("默认用户");
return defaultUser;
}
}
application.yml配置:
yaml
resilience4j:
circuitbreaker:
instances:
userService:
sliding-window-size: 10 # 滑动窗口大小
failure-rate-threshold: 50 # 失败率阈值50%
wait-duration-in-open-state: 30s # 熔断后等待30秒
permitted-number-of-calls-in-half-open-state: 5 # 半开状态允许5次请求
4.2 案例2:高并发下连接池耗尽
问题现象
错误日志:
Caused by: java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out after 30000ms.
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
原因分析
| 原因 | 说明 | 占比 |
|---|---|---|
| 连接池大小设置不合理 | 最大连接数太小,无法应对高峰 | 40% |
| 连接泄漏 | 代码中未正确关闭连接 | 30% |
| 慢查询 | SQL执行时间长,占用连接 | 20% |
| 事务过长 | 事务中包含远程调用,持有连接过久 | 10% |
监控代码:HikariCP连接池状态检查
java
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPoolMXBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class ConnectionPoolMonitor {
@Autowired
private HikariDataSource dataSource;
/**
* 每分钟监控一次连接池状态
*/
@Scheduled(fixedRate = 60000)
public void monitorPool() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
int active = poolMXBean.getActiveConnections();
int idle = poolMXBean.getIdleConnections();
int total = poolMXBean.getTotalConnections();
int max = poolMXBean.getMaximumPoolSize();
int waiting = poolMXBean.getThreadsAwaitingConnection();
double usageRate = (double) active / max * 100;
log.info("连接池状态 - 活跃:{}, 空闲:{}, 总数:{}, 最大:{}, 等待:{}, 使用率:{}%",
active, idle, total, max, waiting, String.format("%.2f", usageRate));
// 告警
if (usageRate > 80) {
log.warn("⚠️ 连接池使用率超过80%: {}/{}", active, max);
}
if (waiting > 0) {
log.error("🔴 有{}个线程在等待连接!", waiting);
}
}
/**
* 对外暴露监控接口
*/
public Map<String, Object> getPoolMetrics() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
Map<String, Object> metrics = new HashMap<>();
metrics.put("activeConnections", poolMXBean.getActiveConnections());
metrics.put("idleConnections", poolMXBean.getIdleConnections());
metrics.put("totalConnections", poolMXBean.getTotalConnections());
metrics.put("maxPoolSize", poolMXBean.getMaximumPoolSize());
metrics.put("threadsAwaitingConnection", poolMXBean.getThreadsAwaitingConnection());
return metrics;
}
}
最佳配置
yaml
# application.yml
spring:
datasource:
hikari:
# 核心参数
maximum-pool-size: 20 # 最大连接数(根据公式计算)
minimum-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 获取连接超时30秒
idle-timeout: 600000 # 空闲超时10分钟
max-lifetime: 1800000 # 最大生命周期30分钟
keepalive-time: 30000 # 保持活跃时间30秒
# 性能优化
data-source-properties:
cachePrepStmts: true # 缓存预编译语句
prepStmtCacheSize: 250 # 缓存大小
prepStmtCacheSqlLimit: 2048 # SQL长度限制
# 监控
metrics-tracker-factory: com.zaxxer.hikari.metrics.prometheus.PrometheusMetricsTrackerFactory
连接数计算公式:
连接数 = CPU核心数 × 2 + 磁盘数
示例:
- 8核CPU,1块磁盘:8×2+1 = 17 ≈ 20
- 16核CPU,2块磁盘:16×2+2 = 34 ≈ 40
- 32核CPU,4块磁盘:32×2+4 = 68 ≈ 70
注意:
1. 这只是起始值,需要根据实际情况调整
2. 观察监控指标,动态调整
3. 考虑业务特点:IO密集型可以适当增加
排查连接泄漏:
java
// 启用泄漏检测
spring:
datasource:
hikari:
leak-detection-threshold: 60000 # 连接持有超过60秒警告
// 日志中会输出:
// HikariPool-1 - Connection leak detection triggered for connection com.mysql.jdbc.JDBC4Connection@xxx
优化慢查询:
sql
-- 1. 查看慢查询日志
SHOW VARIABLES LIKE 'slow_query_log';
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2; -- 超过2秒的查询记录
-- 2. 使用EXPLAIN分析SQL
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'PENDING';
-- 3. 添加索引
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- 4. 避免SELECT *,只查询需要的字段
SELECT id, order_no, amount FROM orders WHERE user_id = 123;
五、总结与思维导图
5.1 核心要点回顾
- ✅ 网络分层的本质:解耦、复用、可替换,就像快递物流系统一样,各层各司其职
- ✅ TCP三次握手的原因:防止失效的连接请求突然到达,确认双方的发送和接收能力
- ✅ TCP四次挥手的原因:TCP是全双工通信,需要分别关闭两个方向的连接
- ✅ TCP可靠性保证:序列号+ACK、超时重传、流量控制、拥塞控制、校验和五大机制
- ✅ 实战技巧:用curl分解耗时、用Wireshark抓包分析、合理配置连接池
5.2 思维导图
网络基础与TCP协议
├── 网络分层
│ ├── 为什么要分层?
│ │ ├── 解耦
│ │ ├── 复用
│ │ └── 可替换
│ ├── OSI七层 vs TCP/IP四层
│ │ ├── 应表会传网数物(口诀)
│ │ └── 实际使用TCP/IP四层
│ └── 各层协议速查表
│ ├── 应用层:HTTP、DNS
│ ├── 传输层:TCP、UDP
│ ├── 网际层:IP、ICMP
│ └── 网络接口层:Ethernet
│
├── TCP协议
│ ├── TCP vs UDP
│ │ ├── 可靠 vs 不可靠
│ │ ├── 有序 vs 无序
│ │ └── 慢 vs 快
│ ├── 三次握手
│ │ ├── 为什么不能两次?(防止失效连接)
│ │ ├── 为什么不需要四次?(SYN+ACK合并)
│ │ └── Spring Boot优化配置
│ ├── 四次挥手
│ │ ├── 全双工通信
│ │ ├── TIME_WAIT状态
│ │ └── 连接池复用
│ └── 可靠性保证
│ ├── 序列号+ACK
│ ├── 超时重传
│ ├── 流量控制(滑动窗口)
│ ├── 拥塞控制
│ └── 校验和
│
└── 实战应用
├── 接口超时排查
│ ├── nslookup(DNS)
│ ├── curl(时间分解)
│ ├── tcpdump(抓包)
│ └── Feign优化(超时、重试、熔断)
└── 连接池优化
├── HikariCP配置
├── 连接数计算公式
├── 泄漏检测
└── 慢查询优化
5.3 下一步学习指引
恭喜你完成了第一篇的学习!接下来建议学习:
📖 第二篇:《HTTP协议深度解析:从HTTP/1.0到HTTP/3.0的演进之路》
- HTTP请求/响应结构
- GET vs POST的本质区别
- HTTP状态码完全指南
- HTTP/2.0多路复用原理
- RESTful API设计最佳实践
5.4 课后练习
练习1:Wireshark抓包观察TCP三次握手
bash
# 1. 安装Wireshark
# 2. 开始抓包,过滤器输入:tcp.port == 80
# 3. 浏览器访问 http://www.example.com
# 4. 找到SYN、SYN+ACK、ACK三个报文
# 5. 记录每个报文的时间戳,计算握手耗时
练习2:检查你的项目连接池配置
java
// 在你的项目中添加监控接口
@RestController
public class PoolMonitorController {
@Autowired
private HikariDataSource dataSource;
@GetMapping("/pool/status")
public Map<String, Object> getPoolStatus() {
// 实现监控逻辑
}
}
// 访问 http://localhost:8080/pool/status
// 观察连接池使用情况,判断配置是否合理
练习3:用curl测试接口耗时
bash
# 创建curl-format.txt文件(见上文)
# 测试你的项目接口
curl -w "@curl-format.txt" -o /dev/null -s http://localhost:8080/api/users/123
# 分析各个阶段的耗时,找出性能瓶颈
结语
网络知识看似复杂,但只要用生活化的类比去理解,就会发现它们都源于我们的日常经验。TCP三次握手就像打电话前的确认,四次挥手就像告别时的礼貌,网络分层就像快递物流的分工协作。
希望这篇文章能帮助你建立起对网络基础和TCP协议的直观认知。记住:理解比记忆更重要,实践比理论更 valuable。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多的小伙伴一起学习!
下一篇预告:《HTTP协议深度解析:从HTTP/1.0到HTTP/3.0的演进之路》,我们将深入探讨Web开发的基石------HTTP协议,敬请期待!🚀
