Skip to content

Java 网络与 TCP/UDP 全过程

网络编程不是只会创建 Socket。数据库连接、Redis、HTTP、RPC、MQ 和 Netty 最终都要经过操作系统网络协议栈。本章从 JDK 7/8 可运行代码出发,讲清一次请求怎样建立连接、怎样可靠传输、为什么会粘包,以及线上连接异常从哪里开始查。

学习目标

学完应能独立回答并验证:

  • IP、端口、DNS、路由和 Socket 各自解决什么问题;
  • 一个 TCP 连接为什么由四元组唯一标识,监听 Socket 与已连接 Socket 有何不同;
  • 三次握手为什么不是两次,四次挥手为什么经常不是严格四个报文;
  • TCP 如何依靠序号、ACK、重传、流量控制和拥塞控制提供可靠字节流;
  • 为什么 TCP 没有“消息边界”,粘包、半包应该在哪一层解决;
  • 连接超时、读超时、TCP Keepalive、业务心跳分别保护什么;
  • 如何定位 Connection refusedConnect timed outRead timed outCLOSE_WAITTIME_WAIT 和端口耗尽。

一、先建立完整心智模型

1.1 从业务方法到网络

mermaid
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 APISocket、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 与子网掩码判断目标是否在本地网段:

mermaid
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 可用以下代码观察解析结果:

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 连接通常由以下四元组唯一识别:

text
源IP + 源端口 + 目的IP + 目的端口

因此,同一台服务端可以在 10.0.0.8:8080 上同时接收成千上万个客户端连接,因为客户端的 IP 或临时端口不同。

3.2 监听 Socket 与连接 Socket

ServerSocket 绑定本地地址并监听。accept() 返回的是一个新的、已连接的 Socket

mermaid
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

mermaid
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。握手的核心不只是“打招呼”,而是:

  1. 确认双方发送和接收路径可用;
  2. 交换初始序号,后续才能确认字节;
  3. 协商 MSS、窗口扩大、SACK 等 TCP 选项;
  4. 避免历史失效连接请求直接让服务端建立错误连接。

4.2 为什么两次不够

若服务端发出第二个报文后立即认为连接成立,它不知道客户端是否收到自己的 SYN。历史网络中滞留的旧 SYN 也可能突然到达,使服务端为已经不存在的客户端分配连接资源。第三次 ACK 证明客户端收到了服务端的序号和响应。

“三次握手绝对消除所有半开连接”也不准确:第三次 ACK 可能丢失,服务端会重传 SYN+ACK,客户端收到后再回 ACK。TCP 靠状态机和重传收敛。

4.3 建连失败怎么区分

现象常见含义优先检查
Connection refused目标主动返回 RST,常见于端口未监听地址、端口、服务进程、监听地址
Connect timed outSYN 多次无响应或路径被丢弃防火墙、安全组、路由、网络、对端过载
No route to host本机无法找到可用路由或收到不可达路由表、网关、网段配置
域名解析失败尚未进入 TCP 建连DNS 配置、域名、缓存

五、TCP 为什么可靠

TCP 提供的是“可靠、有序、无重复的字节流”,不是保证业务一定成功。服务端收到支付请求后连接断开,客户端不知道业务是否落库,仍需业务幂等和结果查询。

5.1 序号和 ACK

TCP 按字节编号。若发送 seq=1001、长度 500 字节,接收方返回 ack=1501,表示 1501 之前的字节已连续收到,下一字节期待 1501。ACK 通常是累计确认,不需要每段一一确认。

5.2 丢包与重传

mermaid
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 是全双工,两个方向要分别关闭:

mermaid
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,主要用于:

  1. 最后一个 ACK 丢失时还能重新发送;
  2. 让旧连接的延迟报文在网络中消失,避免污染相同四元组的新连接。

大量短连接会产生 TIME_WAIT。正确治理通常是连接池和 Keep-Alive、减少无意义重连、检查谁主动关闭、评估临时端口范围。不要先照抄内核参数强行复用,否则可能掩盖架构问题或引入报文混淆风险。

6.3 半关闭

Socket.shutdownOutput() 只发送本方向 FIN,表示“不再发送,但仍可接收”。close() 通常关闭整个 Socket。某些协议使用 EOF 表示请求结束时会用到半关闭,但业务协议更常使用长度字段。

七、TCP 为什么会粘包和半包

“粘包”不是 TCP 把两个包错误粘在一起。TCP 本来就是字节流,没有应用消息边界:一次 write 不对应一次 read

mermaid
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 消息体。

java
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();
        }
    }
}

客户端核心代码:

java
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 连接超时与读取超时

java
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

java
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 的拥塞控制和流量控制。

java
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 先确定“慢在哪一段”

mermaid
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 -antTCP 状态分布ESTABLISHED、SYN_SENT、CLOSE_WAIT、TIME_WAIT
netstat -s协议统计重传、失败建连、丢包趋势
lsof -p PID进程文件描述符Socket 数是否逼近上限
jstack PIDJava 线程卡点socketRead0、连接池等待、锁、线程池饱和
tcpdump线上的真实报文SYN 是否有响应、RST、重传、零窗口、时延

命令参数在不同 Linux 发行版有差异,生产抓包要限定主机、端口和时长,防止文件过大并保护敏感数据。

11.3 典型场景

场景 A:扩容应用后仍大量超时

不要只看应用实例数。若数据库连接池、下游 QPS 或出口带宽是瓶颈,扩容会制造更多并发,把下游打得更慢。对比连接池等待、下游 P99、重传率和线程栈,再决定限流、隔离、缓存还是扩容下游。

场景 B:CLOSE_WAIT 持续增长

  1. ss -antp 确认属于哪个 Java PID 和远端;
  2. lsof -p PID 看 FD 是否同步增长;
  3. jstack 看业务线程是否卡住;
  4. 审查所有成功、异常、超时分支是否关闭响应体、输入流和 Socket;
  5. 修复后观察连接状态与 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 7JDK 8后续版本
Socket/ServerSocket/DatagramSocket成熟可用基本一致持续兼容
NIO.2 异步 ChannelJDK 7 引入可用持续增强
Lambda 处理回调不支持,使用匿名内部类支持支持
标准 java.net.http.HttpClientJDK 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 对应一次 readTCP 是字节流解析错位、消息串包
只设置连接超时建连后读取仍可无限等线程池耗尽、级联雪崩
超时后立即无限重试原请求可能已成功且下游仍过载重复扣款、放大故障
cancel/close 等同业务回滚网络中断不撤销已提交业务数据重复或状态不确定
大量 CLOSE_WAIT 调内核参数根因通常是应用未关闭FD 最终耗尽
禁用 Nagle 就解决粘包消息边界与 Nagle 无关协议仍错误且小包增多

十五、关联知识点与学习验收

完成以下任务才算掌握:

  • 能画出业务代码到服务端的分层链路;
  • 能用序号解释三次握手、累计 ACK 和重传;
  • 能写出有长度上限的长度字段协议,并故意拆成多次发送仍正确解析;
  • 能解释读超时为什么不是业务总超时;
  • 面对 CLOSE_WAITTIME_WAIT、连接拒绝和连接超时,能说出第一条命令、证据和下一步,而不是只会重启。