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. 浏览器输入地址后做了什么?

这是一个串联全链路的经典场景。

  1. URL 解析与 DNS 查询:浏览器先查本地 hosts/缓存,没有则向 DNS 服务器发起 UDP 请求,获取域名对应的 IP。
  2. 建立 TCP 连接:浏览器向服务器 IP 发起 TCP 三次握手(见下文)。
  3. 发送 HTTP 请求:握手成功后,构建 HTTP 报文(Header + Body)发送。
  4. 服务器处理
    • 报文穿过网卡 -> IP层 -> TCP层 -> Socket 接收缓冲区
    • Web 服务器(如 Tomcat/Nginx)从 Socket 读取数据,解析 HTTP,调用 Servlet/Spring Controller 逻辑。
  5. 服务器响应:构建 HTTP 响应报文发回。
  6. 浏览器渲染:解析 HTML/CSS/JS,构建 DOM 树渲染页面。
  7. 断开连接:TCP 四次挥手。

3. TCP 三次握手与为什么不能两次

3.1 三次握手流程

目的是同步双方的序列号 (ISN) 并确认收发能力。

  1. SYN_SENT: 客户端发送 SYN=1, seq=x
  2. SYN_RCVD: 服务端回复 SYN=1, ACK=1, seq=y, ack=x+1
  3. ESTABLISHED: 客户端回复 ACK=1, seq=x+1, ack=y+1。此时连接建立。

3.2 深度原理:为什么不能两次?

很多人只回答“确认双方收发能力”,这不够深。核心原因有两个:

  1. 防止已失效的连接请求报文突然传到服务端(主要原因):
    • 场景:Client 发出的第一个 SYN 包在网络滞留,Client 超时重发并建立了连接然后结束了。
    • 后果:后来那个滞留的 SYN 到了 Server。如果是两次握手,Server 收到就认为连接建立了,开始等待 Client 发数据,但 Client 根本没想连。Server 会白白浪费资源挂起连接。
    • 三次握手解法:Server 收到滞留 SYN 回复 SYN+ACK,Client 发现“我没想连啊”,回一个 RST (Reset) 报文拒绝。
  2. 同步序列号:TCP 是可靠传输,必须确认双方的初始序列号 (ISN),两次握手只能确认发起方的 ISN,无法确认接收方的 ISN 是否被对方收到。

4. TCP 四次挥手与 TIME_WAIT 状态

TCP 是全双工的,断开需要双向分别关闭。

4.1 四次挥手流程

  1. Client -> FIN: “我发完了”。(Client 进入 FIN_WAIT_1)
  2. Server -> ACK: “知道了,但我可能还有数据要发”。(Server 进入 CLOSE_WAIT,Client 进入 FIN_WAIT_2)
    • 注意:此时处于半关闭状态。
  3. Server -> FIN: “我也发完了”。(Server 进入 LAST_ACK)
  4. Client -> ACK: “好的,再见”。(Client 进入 TIME_WAIT,Server 收到后进入 CLOSED)

4.2 为什么要进入 TIME_WAIT (2MSL)?

TIME_WAIT 是主动关闭方必须经历的状态,时长通常是 2MSL (Maximum Segment Lifetime,报文最大生存时间,约2-4分钟)。

  1. 确保最后一个 ACK 能到达 Server:如果 Client 发完 ACK 直接跑路,而这个 ACK 丢了,Server 会重发 FIN。如果 Client 没了,Server 就会报错。TIME_WAIT 状态下,Client 还能处理重传的 FIN。
  2. 防止“旧连接的幽灵数据”干扰新连接:确保本连接产生的所有报文都在网络中消失。否则,如果你立马用相同端口建新连接,旧连接迷路的包可能会混入新连接,造成数据错乱。

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 (拥塞窗口)。

  1. 慢启动cwnd 指数增长 (1, 2, 4, 8…),试探网络极限。
  2. 拥塞避免:到达阈值 (ssthresh) 后,线性增长 (n+1)。
  3. 拥塞发生
    • 超时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 原理

  1. 非对称加密(公钥/私钥):用于握手阶段,协商传输数据的“会话密钥”。
  2. 对称加密(会话密钥):用于真正传输数据,速度快。

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() 的源码,你会看到调用链:

  1. 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;
    }
    
  2. implAccept(Socket s):

    最终会调用到 SocketImpl 类的实现。在现代 JDK 中,通常是 DualStackPlainSocketImpl (Windows) 或 PlainSocketImpl (Linux)。

  3. 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) 机制,一个线程管理成千上万个连接。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐