Java 开发者必读:计算机网络底层原理与架构全解
Java 开发者必读:计算机网络底层原理与架构全解
1. TCP/IP 四层模型
与 OSI 七层模型不同,工业界实际标准是 TCP/IP 四层模型。Java 开发主要关注应用层与传输层。
| 层级 | 名称 | 核心协议 | Java 中的对应 |
|---|---|---|---|
| 4 | 应用层 (Application) | HTTP, FTP, DNS, SMTP | HttpServlet, OkHttp, Spring MVC |
| 3 | 传输层 (Transport) | TCP, UDP | java.net.Socket, ServerSocket |
| 2 | 网络层 (Internet) | IP, ICMP, ARP | InetAddress (Java 对 IP 的操作有限) |
| 1 | 网络接口层 | Ethernet, Wi-Fi | 操作系统网卡驱动处理 |
2. 浏览器输入地址后做了什么?
这是一个串联全链路的经典场景。
- URL 解析与 DNS 查询:浏览器先查本地 hosts/缓存,没有则向 DNS 服务器发起 UDP 请求,获取域名对应的 IP。
- 建立 TCP 连接:浏览器向服务器 IP 发起 TCP 三次握手(见下文)。
- 发送 HTTP 请求:握手成功后,构建 HTTP 报文(Header + Body)发送。
- 服务器处理:
- 报文穿过网卡 -> IP层 -> TCP层 -> Socket 接收缓冲区。
- Web 服务器(如 Tomcat/Nginx)从 Socket 读取数据,解析 HTTP,调用 Servlet/Spring Controller 逻辑。
- 服务器响应:构建 HTTP 响应报文发回。
- 浏览器渲染:解析 HTML/CSS/JS,构建 DOM 树渲染页面。
- 断开连接:TCP 四次挥手。
3. TCP 三次握手与为什么不能两次
3.1 三次握手流程
目的是同步双方的序列号 (ISN) 并确认收发能力。
- SYN_SENT: 客户端发送
SYN=1, seq=x。 - SYN_RCVD: 服务端回复
SYN=1, ACK=1, seq=y, ack=x+1。 - ESTABLISHED: 客户端回复
ACK=1, seq=x+1, ack=y+1。此时连接建立。
3.2 深度原理:为什么不能两次?
很多人只回答“确认双方收发能力”,这不够深。核心原因有两个:
- 防止已失效的连接请求报文突然传到服务端(主要原因):
- 场景:Client 发出的第一个 SYN 包在网络滞留,Client 超时重发并建立了连接然后结束了。
- 后果:后来那个滞留的 SYN 到了 Server。如果是两次握手,Server 收到就认为连接建立了,开始等待 Client 发数据,但 Client 根本没想连。Server 会白白浪费资源挂起连接。
- 三次握手解法:Server 收到滞留 SYN 回复 SYN+ACK,Client 发现“我没想连啊”,回一个 RST (Reset) 报文拒绝。
- 同步序列号:TCP 是可靠传输,必须确认双方的初始序列号 (ISN),两次握手只能确认发起方的 ISN,无法确认接收方的 ISN 是否被对方收到。
4. TCP 四次挥手与 TIME_WAIT 状态
TCP 是全双工的,断开需要双向分别关闭。
4.1 四次挥手流程
- Client -> FIN: “我发完了”。(Client 进入
FIN_WAIT_1) - Server -> ACK: “知道了,但我可能还有数据要发”。(Server 进入
CLOSE_WAIT,Client 进入FIN_WAIT_2)- 注意:此时处于半关闭状态。
- Server -> FIN: “我也发完了”。(Server 进入
LAST_ACK) - Client -> ACK: “好的,再见”。(Client 进入
TIME_WAIT,Server 收到后进入CLOSED)
4.2 为什么要进入 TIME_WAIT (2MSL)?
TIME_WAIT 是主动关闭方必须经历的状态,时长通常是 2MSL (Maximum Segment Lifetime,报文最大生存时间,约2-4分钟)。
- 确保最后一个 ACK 能到达 Server:如果 Client 发完 ACK 直接跑路,而这个 ACK 丢了,Server 会重发 FIN。如果 Client 没了,Server 就会报错。
TIME_WAIT状态下,Client 还能处理重传的 FIN。 - 防止“旧连接的幽灵数据”干扰新连接:确保本连接产生的所有报文都在网络中消失。否则,如果你立马用相同端口建新连接,旧连接迷路的包可能会混入新连接,造成数据错乱。
5. TCP 报头有哪些信息
TCP 头部标准长度 20 字节。
| 字段 | 作用 |
|---|---|
| Source Port / Dest Port | 源端口、目的端口(结合 IP 确定唯一进程)。 |
| Sequence Number (seq) | 序列号,解决乱序和重复问题。 |
| Acknowledgment Number (ack) | 确认号,期望收到的下一个字节,解决丢包问题。 |
| Data Offset | 头部长度。 |
| Control Flags | SYN (建立连接), ACK (确认), FIN (结束), RST (重置), PSH (推), URG (紧急)。 |
| Window Size | 滑动窗口大小,用于流量控制。 |
| Checksum | 校验和,保证数据完整性。 |
6. TCP 可靠传输的实现机制
TCP 通过以下四大机制实现“在不可靠的网络上提供可靠服务”。
6.1 滑动窗口 (Flow Control)
解决点对点收发速率不匹配问题。
接收方在 TCP 头部的 Window 字段告诉发送方:“我缓存区还能装 X 字节,你别发多了”。发送方根据这个值动态调整发送量。
- 若 Window=0,发送方停止发送,并启动“坚持定时器”探测窗口是否恢复。
6.2 拥塞控制 (Congestion Control)
解决整个网络堵车的问题。
发送方维护一个 cwnd (拥塞窗口)。
- 慢启动:
cwnd指数增长 (1, 2, 4, 8…),试探网络极限。 - 拥塞避免:到达阈值 (
ssthresh) 后,线性增长 (n+1)。 - 拥塞发生:
- 超时:
ssthresh降为一半,cwnd重置为 1(一夜回到解放前)。 - 3次重复 ACK (快重传):认为网络轻度拥塞,
ssthresh降一半,cwnd降一半(快恢复),不回零。
- 超时:
6.3 超时重传 (RTO)
发送数据时启动定时器。如果在 RTO (Retransmission Time-Out) 时间内没收到 ACK,重发数据。RTO 是根据网络 RTT (往返时间) 动态计算的。
7. 应用层协议详解
7.1 HTTP 状态码
- 2xx (成功):
200 OK - 3xx (重定向):
301: 永久重定向 (浏览器会缓存新地址)。302: 临时重定向。
- 4xx (客户端错误):
400: 请求参数语法错误。401: 未认证。403: 禁止访问 (有权限但被拒)。404: 资源找不到。
- 5xx (服务端错误):
500: 代码抛异常了。502 Bad Gateway: Nginx 连不上后端 Tomcat。504 Gateway Timeout: 后端处理太慢,Nginx 等烦了。
7.2 HTTP vs HTTPS 的区别
| 特性 | HTTP | HTTPS |
|---|---|---|
| 安全性 | 明文传输,易被抓包篡改 | SSL/TLS 加密传输 |
| 端口 | 80 | 443 |
| 证书 | 无需 | 需要 CA 证书 |
| 性能 | 快 | 握手阶段耗时,CPU 计算加密消耗资源 |
HTTPS 原理:
- 非对称加密(公钥/私钥):用于握手阶段,协商传输数据的“会话密钥”。
- 对称加密(会话密钥):用于真正传输数据,速度快。
8. Socket 通信流程与 Java 源码剖析
Socket 是操作系统提供的 TCP 通信接口。在 Java 中,Socket 是客户端,ServerSocket 是服务端。
8.1 完整通信代码 (BIO 模型)
服务端 (Server):
Java
import java.io.*;
import java.net.*;
public class BioServer {
public static void main(String[] args) throws Exception {
// 1. 创建ServerSocket,绑定端口
// 底层对应 OS: socket() -> bind() -> listen()
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端启动,等待连接...");
while (true) {
// 2. 阻塞等待客户端连接
// 底层对应 OS: accept()。如果没有连接,线程卡死在这里
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress());
// 3. 每一个新连接,通常开一个新线程处理 (BIO弊端)
new Thread(() -> {
try {
InputStream in = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String msg;
// 4. 阻塞读取数据
// 底层对应 OS: recv()。如果没有数据发过来,线程卡死在这里
while ((msg = reader.readLine()) != null) {
System.out.println("收到消息: " + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
客户端 (Client):
Java
import java.io.*;
import java.net.*;
public class BioClient {
public static void main(String[] args) throws Exception {
// 1. 建立连接
// 底层对应 OS: socket() -> connect() -> 发起三次握手
Socket socket = new Socket("127.0.0.1", 8888);
// 2. 发送数据
OutputStream out = socket.getOutputStream();
PrintWriter writer = new PrintWriter(out, true);
writer.println("Hello, TCP!");
writer.println("I am learning Network.");
// 3. 关闭连接 -> 发起四次挥手
socket.close();
}
}
8.2 Java 源码深层原理解析
如果你点开 ServerSocket.accept() 的源码,你会看到调用链:
-
ServerSocket.accept():Java
public Socket accept() throws IOException { if (isClosed()) throw new SocketException("Socket is closed"); if (!isBound()) throw new SocketException("Socket is not bound yet"); Socket s = new Socket((SocketImpl) null); implAccept(s); // 核心入口 return s; } -
implAccept(Socket s):最终会调用到
SocketImpl类的实现。在现代 JDK 中,通常是DualStackPlainSocketImpl(Windows) 或PlainSocketImpl(Linux)。 -
socketAccept(SocketImpl s)(Native Method):Java
// 这是一个 native 方法,直接调用 C 语言编写的本地库 void socketAccept(SocketImpl s);底层 C 代码逻辑 (OpenJDK 源码):
- 调用操作系统的
accept(fd, ...)系统调用。 - 关键点:如果 socket 设置为阻塞模式(Java 默认),操作系统的
accept函数会将当前线程挂起(放入等待队列),直到 TCP 全连接队列(Accept Queue)中有新的连接完成三次握手。 - 一旦三次握手完成,内核唤醒线程,返回一个新的文件描述符(File Descriptor)代表这个新连接。
- 调用操作系统的
8.3 为什么 BIO 性能差?
从源码可知,Java 的 accept() 和 read() 都会直接映射到操作系统的阻塞调用。
- 如果客户端连上了但不发数据,服务端的
read()线程就一直傻等(Thread Blocked)。 - 为了处理多个并发,必须
new Thread(),线程上下文切换开销极大。 - 解决方案:NIO (Non-blocking I/O),利用 OS 的 IO 多路复用 (epoll/kqueue) 机制,一个线程管理成千上万个连接。
更多推荐


所有评论(0)