Java 网络与 TCP/UDP 全过程
网络编程不是只会创建 Socket。数据库连接、Redis、HTTP、RPC、MQ 和 Netty 最终都要经过操作系统网络协议栈。本章从 JDK 7/8 可运行代码出发,讲清一次请求怎样建立连接、怎样可靠传输、为什么会粘包,以及线上连接异常从哪里开始查。
学习目标
学完应能独立回答并验证:
- IP、端口、DNS、路由和 Socket 各自解决什么问题;
- 一个 TCP 连接为什么由四元组唯一标识,监听 Socket 与已连接 Socket 有何不同;
- 三次握手为什么不是两次,四次挥手为什么经常不是严格四个报文;
- TCP 如何依靠序号、ACK、重传、流量控制和拥塞控制提供可靠字节流;
- 为什么 TCP 没有“消息边界”,粘包、半包应该在哪一层解决;
- 连接超时、读超时、TCP Keepalive、业务心跳分别保护什么;
- 如何定位
Connection refused、Connect timed out、Read timed out、CLOSE_WAIT、TIME_WAIT和端口耗尽。
一、先建立完整心智模型
1.1 从业务方法到网络
flowchart TD
A["Java业务代码"] --> B["Socket或HTTP客户端"]
B --> C["JDK网络API"]
C --> D["系统调用"]
D --> E["内核TCP或UDP协议栈"]
E --> F["网卡驱动与网卡"]
F --> G["交换机和路由器"]
G --> H["服务端网卡与协议栈"]
H --> I["服务端Socket"]
I --> J["服务端业务代码"]Socket 不是 TCP 本身,也不是一条物理线路。它是应用程序操作内核网络能力的编程端点。Java 调用 connect/read/write,最终由内核维护连接状态、发送缓冲区、接收缓冲区、序号和重传计时器。
1.2 分层的价值
| 层次 | 常见协议或对象 | 主要职责 |
|---|---|---|
| 应用层 | HTTP、DNS、Redis、MySQL 协议 | 定义业务消息含义和格式 |
| 传输层 | TCP、UDP | 进程到进程传输;端口位于这一层 |
| 网络层 | IP | 跨网络寻址与路由 |
| 数据链路层 | Ethernet、MAC、ARP | 同一链路上的帧传输和下一跳寻址 |
| Java API | Socket、ServerSocket、DatagramSocket | 向应用暴露操作系统网络能力 |
分层意味着 HTTP 超时不一定是 HTTP 解析慢,也可能是 DNS、TCP 建连、TLS、服务端处理或响应读取中的任何一段。
二、IP、子网、网关、ARP、DNS 与路由
2.1 IP 和端口不是一回事
- IP 用来找到主机或网络接口;一台机器可有多个 IP。
- 端口是 0~65535 的 16 位编号,用来把到达主机的数据交给某个 Socket。
- 服务端通常绑定固定端口;客户端连接时由操作系统分配临时端口。
127.0.0.1只在本机回环;0.0.0.0作为监听地址表示绑定所有本地 IPv4 接口,不能把它当作远端目的地址。
2.2 同网段和跨网段
发送方用 IP 与子网掩码判断目标是否在本地网段:
flowchart TD
A["准备发送IP包"] --> B{"目标和本机同网段吗"}
B -- "是" --> C["ARP查询目标MAC"]
C --> D["直接发送给目标"]
B -- "否" --> E["查询路由表"]
E --> F["ARP查询下一跳网关MAC"]
F --> G["先把帧交给网关"]ARP 解析的是同一链路上“下一跳 IP 对应的 MAC”,并不是把互联网任意 IP 直接解析成 MAC。跨网段时,以太网帧的目的 MAC 通常是网关,IP 包的目的 IP 仍是最终服务器。
2.3 DNS 做了什么
DNS 把域名解析成 IP,但得到 IP 后仍需建立 TCP/UDP 通信。常见流程是浏览器/JVM缓存、操作系统缓存、本地 DNS、递归解析器、权威 DNS。DNS 缓存可减少延迟,却会带来地址切换后的陈旧缓存问题。
Java 可用以下代码观察解析结果:
import java.net.InetAddress;
public class DnsDemo {
public static void main(String[] args) throws Exception {
InetAddress[] addresses = InetAddress.getAllByName("example.com");
for (InetAddress address : addresses) {
System.out.println(address.getHostAddress());
}
}
}JVM DNS 缓存时长受安全属性和 JDK/发行版配置影响,不能假定永不过期或固定 30 秒。服务地址频繁变化时,应核对 networkaddress.cache.ttl、客户端连接复用和服务发现机制。
三、TCP 连接究竟是什么
3.1 四元组
一个 TCP 连接通常由以下四元组唯一识别:
源IP + 源端口 + 目的IP + 目的端口因此,同一台服务端可以在 10.0.0.8:8080 上同时接收成千上万个客户端连接,因为客户端的 IP 或临时端口不同。
3.2 监听 Socket 与连接 Socket
ServerSocket 绑定本地地址并监听。accept() 返回的是一个新的、已连接的 Socket:
flowchart TD
A["ServerSocket绑定8080"] --> B["监听Socket等待连接"]
B --> C["客户端完成握手"]
C --> D["accept返回连接Socket"]
D --> E["连接Socket负责本次会话"]
B --> F["监听Socket继续接收其他连接"]监听 Socket 不负责承载每个会话的数据。多个已连接 Socket 可以共享服务端本地 IP 和端口,但远端 IP/端口不同。
四、三次握手为什么必须是三次
4.1 握手过程
假设客户端初始序号为 x,服务端为 y:
flowchart TD
A["客户端发送SYN seq=x"] --> B["服务端确认客户端发送能力"]
B --> C["服务端发送SYN加ACK ack=x+1 seq=y"]
C --> D["客户端确认服务端收发能力"]
D --> E["客户端发送ACK ack=y+1"]
E --> F["服务端确认客户端能收到服务端报文"]
F --> G["双方进入ESTABLISHED"]SYN 会消耗一个序号,所以确认号是 x + 1。握手的核心不只是“打招呼”,而是:
- 确认双方发送和接收路径可用;
- 交换初始序号,后续才能确认字节;
- 协商 MSS、窗口扩大、SACK 等 TCP 选项;
- 避免历史失效连接请求直接让服务端建立错误连接。
4.2 为什么两次不够
若服务端发出第二个报文后立即认为连接成立,它不知道客户端是否收到自己的 SYN。历史网络中滞留的旧 SYN 也可能突然到达,使服务端为已经不存在的客户端分配连接资源。第三次 ACK 证明客户端收到了服务端的序号和响应。
“三次握手绝对消除所有半开连接”也不准确:第三次 ACK 可能丢失,服务端会重传 SYN+ACK,客户端收到后再回 ACK。TCP 靠状态机和重传收敛。
4.3 建连失败怎么区分
| 现象 | 常见含义 | 优先检查 |
|---|---|---|
Connection refused | 目标主动返回 RST,常见于端口未监听 | 地址、端口、服务进程、监听地址 |
Connect timed out | SYN 多次无响应或路径被丢弃 | 防火墙、安全组、路由、网络、对端过载 |
No route to host | 本机无法找到可用路由或收到不可达 | 路由表、网关、网段配置 |
| 域名解析失败 | 尚未进入 TCP 建连 | DNS 配置、域名、缓存 |
五、TCP 为什么可靠
TCP 提供的是“可靠、有序、无重复的字节流”,不是保证业务一定成功。服务端收到支付请求后连接断开,客户端不知道业务是否落库,仍需业务幂等和结果查询。
5.1 序号和 ACK
TCP 按字节编号。若发送 seq=1001、长度 500 字节,接收方返回 ack=1501,表示 1501 之前的字节已连续收到,下一字节期待 1501。ACK 通常是累计确认,不需要每段一一确认。
5.2 丢包与重传
flowchart TD
A["发送数据段"] --> B{"按时收到确认吗"}
B -- "是" --> C["滑动窗口继续前进"]
B -- "超时" --> D["RTO超时重传"]
B -- "多个重复ACK" --> E["快速重传疑似丢失段"]
D --> F["调整拥塞控制状态"]
E --> F重传超时 RTO 会根据往返时间动态估算,不能简单理解为固定秒数。接收方重复确认某个序号,通常表示后面的段到了但中间有缺口,发送方可在超时前快速重传。
5.3 校验、有序与去重
- 校验和用于发现传输中的比特错误,错误段会被丢弃并依赖重传恢复;
- 序号使接收方能排序乱序段并识别重复段;
- 应用
read()看到的是已经由 TCP 整理后的有序字节流。
5.4 流量控制不是拥塞控制
| 机制 | 防止谁被压垮 | 依据 |
|---|---|---|
| 流量控制 | 接收端 | 接收方通告的接收窗口 rwnd |
| 拥塞控制 | 网络路径 | 拥塞窗口 cwnd、丢包/时延等信号 |
实际可发送未确认数据量受 min(rwnd, cwnd) 限制。只提高应用线程数不能绕过网络和接收方容量,反而可能制造队列与重传。
5.5 TCP 可靠不等于消息不重复
客户端发送请求,服务端处理成功,但响应丢失;客户端超时重试后,服务端会收到第二个业务请求。TCP 保证的是每条连接内的字节流,不跨重连替业务去重。订单扣款必须使用业务请求号、唯一索引或状态机实现幂等。
六、四次挥手与连接状态
TCP 是全双工,两个方向要分别关闭:
flowchart TD
A["主动方发送FIN"] --> B["被动方确认ACK"]
B --> C["主动方进入FIN_WAIT"]
B --> D["被动方进入CLOSE_WAIT"]
D --> E["被动方应用关闭后发送FIN"]
E --> F["主动方确认ACK并进入TIME_WAIT"]
F --> G["等待2MSL后释放四元组"]ACK 和 FIN 若条件允许可以合并,因此抓包不一定恰好四个包。
6.1 CLOSE_WAIT 为什么多
CLOSE_WAIT 表示对端已发 FIN,本机内核也已通知应用,但本机应用还没有关闭 Socket。大量长期 CLOSE_WAIT 通常是代码未关闭流/Socket、异常路径漏关、业务线程卡死,而不是靠调整 TCP 参数解决。
6.2 TIME_WAIT 为什么存在
主动关闭方通常进入 TIME_WAIT,等待 2MSL,主要用于:
- 最后一个 ACK 丢失时还能重新发送;
- 让旧连接的延迟报文在网络中消失,避免污染相同四元组的新连接。
大量短连接会产生 TIME_WAIT。正确治理通常是连接池和 Keep-Alive、减少无意义重连、检查谁主动关闭、评估临时端口范围。不要先照抄内核参数强行复用,否则可能掩盖架构问题或引入报文混淆风险。
6.3 半关闭
Socket.shutdownOutput() 只发送本方向 FIN,表示“不再发送,但仍可接收”。close() 通常关闭整个 Socket。某些协议使用 EOF 表示请求结束时会用到半关闭,但业务协议更常使用长度字段。
七、TCP 为什么会粘包和半包
“粘包”不是 TCP 把两个包错误粘在一起。TCP 本来就是字节流,没有应用消息边界:一次 write 不对应一次 read。
flowchart TD
A["应用写消息A"] --> C["TCP字节流"]
B["应用写消息B"] --> C
C --> D["一次read得到A的一部分"]
C --> E["一次read得到A加B"]
C --> F["多次read才得到完整消息"]原因包括发送缓冲、MSS 分段、IP 分片、接收缓冲、线程调度和 read 缓冲区大小。解决方案必须由应用层协议定义边界:
| 方案 | 适合场景 | 风险 |
|---|---|---|
| 固定长度 | 固定格式设备协议 | 浪费空间,变更困难 |
| 分隔符 | 文本协议,如按行 | 内容转义与最大行长度 |
| 长度字段 + 消息体 | RPC、网关、采集协议 | 必须校验负数、过大长度和不完整帧 |
| 自描述格式 | HTTP 等成熟协议 | 解析复杂度更高 |
7.1 JDK 7/8 长度字段 Demo
协议规定:先写 4 字节大端整数,再写 UTF-8 消息体。
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
public class LengthFieldServer {
private static final int MAX_FRAME = 1024 * 1024;
private static final Charset UTF8 = Charset.forName("UTF-8");
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9000);
try {
Socket socket = server.accept();
socket.setSoTimeout(5000);
try {
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
while (true) {
int length;
try {
length = in.readInt();
} catch (EOFException eof) {
break;
}
if (length < 0 || length > MAX_FRAME) {
throw new IllegalArgumentException("illegal frame length: " + length);
}
byte[] body = new byte[length];
in.readFully(body); // 循环读取直到填满;不能假设一次read读全
String request = new String(body, UTF8);
byte[] response = ("ACK:" + request).getBytes(UTF8);
out.writeInt(response.length);
out.write(response);
out.flush();
}
} finally {
socket.close();
}
} finally {
server.close();
}
}
}客户端核心代码:
Socket socket = new Socket();
socket.connect(new java.net.InetSocketAddress("127.0.0.1", 9000), 3000);
socket.setSoTimeout(5000);
try {
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
DataInputStream in = new DataInputStream(socket.getInputStream());
byte[] body = "采集设备-001".getBytes("UTF-8");
out.writeInt(body.length);
out.write(body);
out.flush();
int length = in.readInt();
if (length < 0 || length > 1024 * 1024) {
throw new IllegalStateException("illegal response length");
}
byte[] response = new byte[length];
in.readFully(response);
System.out.println(new String(response, "UTF-8"));
} finally {
socket.close();
}安全边界非常重要:若直接按攻击者给出的长度创建数组,可能造成 OOM;若使用一次 read(body),可能只读到部分数据。
八、阻塞、超时、Keepalive 与心跳
8.1 BIO 中哪里会阻塞
accept()等待新连接;connect()等待建连结果;read()在无数据且未到 EOF 时等待;write()在发送缓冲长期塞满时也可能阻塞;- DNS 解析也可能等待。
BIO 的“一连接一线程”容易理解,但大量空闲长连接会占用大量线程栈和调度资源。高连接数时可学习 IO/NIO 全过程 与 Selector 多路复用。
8.2 连接超时与读取超时
Socket socket = new Socket();
socket.connect(new java.net.InetSocketAddress("10.0.0.8", 8080), 3000);
socket.setSoTimeout(5000);connect(..., 3000)限制 TCP 建连等待;setSoTimeout(5000)限制阻塞读取等待,超时抛SocketTimeoutException;- 读超时不等于整个业务调用总超时。若持续每 4 秒收到一点数据,单次读可能不超时,但总耗时仍很长;上层还要控制总 deadline。
8.3 TCP Keepalive 与业务心跳
| 机制 | 目的 | 能否证明业务健康 |
|---|---|---|
| TCP Keepalive | 探测长时间空闲连接的对端/路径是否失效 | 不能,只能说明连接层有响应 |
| 业务心跳 | 检查协议、鉴权、业务线程是否仍能处理 | 可以携带业务状态,但要自行设计 |
| 请求超时 | 限制单次业务等待 | 不负责长期连接保活 |
socket.setKeepAlive(true) 只是启用机制,探测空闲时间和次数通常由操作系统控制,默认可能很长。负载均衡器/NAT 常有更短的 idle timeout,因此商用长连接往往需要间隔合理的业务心跳。
8.4 Nagle 与 TCP_NODELAY
Nagle 算法倾向于合并小块发送以减少小包。setTcpNoDelay(true) 禁用 Nagle,适合低延迟小消息,但可能增加包数量。它不能解决应用协议粘包,因为 TCP 仍是字节流。应先做正确拆帧,再依据抓包和延迟指标决定是否调整。
九、ServerSocket 完整 JDK 7/8 Demo
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class EchoServer {
private static final Charset UTF8 = Charset.forName("UTF-8");
public static void main(String[] args) throws Exception {
final ExecutorService workers = Executors.newFixedThreadPool(20);
final ServerSocket server = new ServerSocket(9000, 200);
try {
while (!Thread.currentThread().isInterrupted()) {
final Socket socket = server.accept();
workers.execute(new Runnable() {
public void run() {
handle(socket);
}
});
}
} finally {
server.close();
workers.shutdownNow();
}
}
private static void handle(Socket socket) {
try {
socket.setSoTimeout(10000);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), UTF8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), UTF8));
String line;
while ((line = reader.readLine()) != null) {
if (line.length() > 4096) {
throw new IllegalArgumentException("line too long");
}
writer.write("ACK:" + line);
writer.newLine();
writer.flush();
}
} catch (Exception e) {
System.err.println("connection failed: " + e.getMessage());
} finally {
try { socket.close(); } catch (Exception ignored) { }
}
}
}这是教学 Demo,不是无限扩展的生产服务:固定线程池可限制并发线程,却仍需有界任务队列、拒绝策略、连接数上限、优雅停机、指标和日志。readLine() 使用换行作为边界,所以必须限制单行长度,否则客户端一直不发换行可占住线程并消耗内存。
十、UDP 原理与 Demo
UDP 是面向数据报的无连接协议。一次发送对应一个数据报边界,但它不保证到达、不保证只到一次、不保证顺序,也没有 TCP 的拥塞控制和流量控制。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.Charset;
public class UdpServer {
public static void main(String[] args) throws Exception {
Charset utf8 = Charset.forName("UTF-8");
DatagramSocket socket = new DatagramSocket(9001);
try {
byte[] buffer = new byte[2048];
DatagramPacket request = new DatagramPacket(buffer, buffer.length);
socket.receive(request);
String text = new String(request.getData(), request.getOffset(),
request.getLength(), utf8);
byte[] reply = ("ACK:" + text).getBytes(utf8);
DatagramPacket response = new DatagramPacket(reply, reply.length,
request.getAddress(), request.getPort());
socket.send(response);
} finally {
socket.close();
}
}
}若接收缓冲区小于数据报,超出部分可能被截断,不会像 TCP 那样下次继续读取同一报文。要在 UDP 上实现可靠业务,应用层需自行设计请求 ID、ACK、超时重传、去重、顺序、分片和拥塞保护。
十一、生产网络故障 Runbook
11.1 先确定“慢在哪一段”
flowchart TD
A["收到网络故障告警"] --> B["记录域名IP端口和时间范围"]
B --> C{"DNS能解析吗"}
C -- "否" --> D["查DNS和缓存"]
C -- "是" --> E{"TCP能建立吗"}
E -- "否" --> F["查监听防火墙路由和SYN"]
E -- "是" --> G{"请求能发出吗"}
G -- "否" --> H["查线程发送缓冲和连接池"]
G -- "是" --> I{"服务端及时响应吗"}
I -- "否" --> J["查服务端线程池DB和下游"]
I -- "是" --> K["查读取解析和大响应"]11.2 命令分别回答什么
| 命令 | 用途 | 示例关注点 |
|---|---|---|
ss -lntp | 谁在监听 | IP 是否只绑定 127.0.0.1、端口、进程 |
ss -ant | TCP 状态分布 | ESTABLISHED、SYN_SENT、CLOSE_WAIT、TIME_WAIT |
netstat -s | 协议统计 | 重传、失败建连、丢包趋势 |
lsof -p PID | 进程文件描述符 | Socket 数是否逼近上限 |
jstack PID | Java 线程卡点 | socketRead0、连接池等待、锁、线程池饱和 |
tcpdump | 线上的真实报文 | SYN 是否有响应、RST、重传、零窗口、时延 |
命令参数在不同 Linux 发行版有差异,生产抓包要限定主机、端口和时长,防止文件过大并保护敏感数据。
11.3 典型场景
场景 A:扩容应用后仍大量超时
不要只看应用实例数。若数据库连接池、下游 QPS 或出口带宽是瓶颈,扩容会制造更多并发,把下游打得更慢。对比连接池等待、下游 P99、重传率和线程栈,再决定限流、隔离、缓存还是扩容下游。
场景 B:CLOSE_WAIT 持续增长
ss -antp确认属于哪个 Java PID 和远端;lsof -p PID看 FD 是否同步增长;jstack看业务线程是否卡住;- 审查所有成功、异常、超时分支是否关闭响应体、输入流和 Socket;
- 修复后观察连接状态与 FD 是否回落,而不是只重启。
场景 C:TIME_WAIT 暴涨并出现无法建连
确认本机是否频繁主动关闭短连接,查看连接池命中率和临时端口范围。若每次 HTTP/RPC 调用都新建连接,应启用并正确配置连接池;同时检查对端是否用 Connection: close、连接最大存活时间是否过短。
场景 D:连接存在但长连接定期断开
若断开周期固定,优先比对负载均衡器、NAT、防火墙 idle timeout 与客户端心跳周期。TCP Keepalive 默认周期可能比中间设备超时更长,需使用业务心跳并设计断线重连退避,避免所有客户端同时重连形成连接风暴。
场景 E:临时端口或 FD 耗尽
症状可能是客户端无法创建新连接、Too many open files 或大量连接状态。端口是四元组资源,FD 是进程/系统资源,两者不是同一个限制。检查连接泄漏、短连接、并发峰值、端口范围和 ulimit -n;先修复生命周期与连接复用,再评估系统参数。
十二、JDK 7、8 与现代 JDK 边界
| 能力 | JDK 7 | JDK 8 | 后续版本 |
|---|---|---|---|
Socket/ServerSocket/DatagramSocket | 成熟可用 | 基本一致 | 持续兼容 |
| NIO.2 异步 Channel | JDK 7 引入 | 可用 | 持续增强 |
| Lambda 处理回调 | 不支持,使用匿名内部类 | 支持 | 支持 |
标准 java.net.http.HttpClient | 无 | 无 | JDK 11 正式提供 |
| 虚拟线程 | 无 | 无 | JDK 21 正式提供 |
JDK 7/8 调 HTTP 通常使用项目已有的 Apache HttpClient、OkHttp 等库,或较底层的 HttpURLConnection。虚拟线程降低“一请求一线程”的线程成本,但不会提高数据库、下游或网络本身的容量,也不会替代超时、限流和拆帧。
十三、常见面试题标准回答
TCP 为什么是三次握手,不是两次
三次握手既交换初始序号和 TCP 选项,也确认双方收发路径可用。两次后服务端无法确认客户端是否收到服务端 SYN,历史失效 SYN 也可能让服务端错误分配连接资源;第三次 ACK 证明客户端收到了服务端的序号。
TCP 和 UDP 怎么选
要求可靠、有序字节流且能接受连接和重传成本,通常选 TCP;允许丢失、重视低时延、广播/组播或应用愿意自行实现可靠性时可选 UDP。不能只说“TCP 慢 UDP 快”,真实性能取决于网络、协议设计和业务容错。
粘包怎么解决
TCP 没有消息边界,一次写不对应一次读,因此应用层必须定义帧边界,例如固定长度、分隔符或“长度字段 + 消息体”。接收端要累计缓冲,只有完整帧到达后再解码,并限制最大帧长度防止内存攻击。
CLOSE_WAIT 和 TIME_WAIT 有什么区别
CLOSE_WAIT 在被动关闭方,说明收到了对端 FIN 但本地应用尚未关闭,长期大量存在常指向代码或线程卡死;TIME_WAIT 通常在主动关闭方,用于重发最后 ACK 和隔离旧报文,大量短连接会使其增加,应优先治理连接复用。
TCP Keepalive 和 HTTP Keep-Alive 一样吗
不一样。TCP Keepalive 是内核对空闲连接的存活探测;HTTP Keep-Alive 是在一条 TCP 连接上复用多次 HTTP 请求。两者都不能代替业务总超时和业务心跳。
十四、常见误区与不这样做的后果
| 误区 | 为什么错 | 后果 |
|---|---|---|
一次 write 对应一次 read | TCP 是字节流 | 解析错位、消息串包 |
| 只设置连接超时 | 建连后读取仍可无限等 | 线程池耗尽、级联雪崩 |
| 超时后立即无限重试 | 原请求可能已成功且下游仍过载 | 重复扣款、放大故障 |
cancel/close 等同业务回滚 | 网络中断不撤销已提交业务 | 数据重复或状态不确定 |
| 大量 CLOSE_WAIT 调内核参数 | 根因通常是应用未关闭 | FD 最终耗尽 |
| 禁用 Nagle 就解决粘包 | 消息边界与 Nagle 无关 | 协议仍错误且小包增多 |
十五、关联知识点与学习验收
完成以下任务才算掌握:
- 能画出业务代码到服务端的分层链路;
- 能用序号解释三次握手、累计 ACK 和重传;
- 能写出有长度上限的长度字段协议,并故意拆成多次发送仍正确解析;
- 能解释读超时为什么不是业务总超时;
- 面对
CLOSE_WAIT、TIME_WAIT、连接拒绝和连接超时,能说出第一条命令、证据和下一步,而不是只会重启。
