在互联网应用日益复杂的今天,网络编程与高并发处理能力是区分普通程序员与高阶程序员的重要分水岭。初级程序员关注业务逻辑的正确实现,而进阶程序员则需要深入理解网络协议栈、I/O模型、线程模型、并发控制、内存模型、以及从操作系统内核到应用层的全链路优化。
本文将围绕“网络与高并发原理”这一核心主题,从网络协议深度解析、I/O模型全面对比、操作系统内核参数调优、Reactor与Proactor模型、Java NIO/Netty源码剖析、线程模型与并发编程、内存模型与可见性、锁的优化机制、高并发系统设计模式、以及全链路压测与性能调优十个维度,带你彻底掌握网络与高并发的底层原理。
一、网络协议深度解析
1.1 TCP/IP协议栈全貌
┌─────────────────────────────────────────────────────────────────────┐
│ OSI七层模型 │
├───────────────┬─────────────────────────────────────────────────────┤
│ 应用层 │ HTTP, HTTPS, FTP, SMTP, DNS, WebSocket │
├───────────────┼─────────────────────────────────────────────────────┤
│ 表示层 │ 加密、压缩、编码转换 │
├───────────────┼─────────────────────────────────────────────────────┤
│ 会话层 │ 建立/维护/结束会话 │
├───────────────┼─────────────────────────────────────────────────────┤
│ 传输层 │ TCP, UDP │
├───────────────┼─────────────────────────────────────────────────────┤
│ 网络层 │ IP, ICMP, ARP │
├───────────────┼─────────────────────────────────────────────────────┤
│ 数据链路层 │ 以太网、WiFi │
├───────────────┼─────────────────────────────────────────────────────┤
│ 物理层 │ 网线、光纤、电磁波 │
└───────────────┴─────────────────────────────────────────────────────┘
实际网络中简化为四层模型
┌─────────────────────────────────────────────────────────────────────┐
│ 应用层 │ 应用数据 │
├──────────────┼─────────────────────────────────────────────────────┤
│ 传输层 │ TCP头 │ 数据 │
├──────────────┼─────────────────────────────────────────────────────┤
│ 网络层 │ IP头 │ TCP头 │ 数据 │
├──────────────┼─────────────────────────────────────────────────────┤
│ 链路层 │ 以太网头 │ IP头 │ TCP头 │ 数据 │ 以太网尾 │
└──────────────┴─────────────────────────────────────────────────────┘
1.2 TCP三次握手与四次挥手详解
三次握手(建立连接)
// 状态机转换
CLOSED -> LISTEN (服务器端)
LISTEN -> SYN_RCVD (收到SYN)
SYN_RCVD -> ESTABLISHED (收到ACK)
CLOSED -> SYN_SENT (客户端发起SYN)
SYN_SENT -> ESTABLISHED (收到SYN+ACK)
// 详细过程
客户端 服务器
| |
|---------> SYN=1, Seq=x ------------>| (1) 客户端请求连接
| | (服务器状态: LISTEN -> SYN_RCVD)
|<------- SYN=1, ACK=1, Seq=y, Ack=x+1 | (2) 服务器响应
| | (客户端状态: SYN_SENT -> ESTABLISHED)
|---------> ACK=1, Seq=x+1, Ack=y+1 --->| (3) 客户端确认
| | (服务器状态: SYN_RCVD -> ESTABLISHED)
为什么是三次而不是两次?
// 核心原因:防止已失效的连接请求报文段突然又传到服务器
public class ThreeWayHandshakeExplanation {
/*
* 场景:客户端第一次发送SYN后,由于网络拥堵延迟,客户端超时重传SYN并成功建立连接
* 传输完毕后连接释放。此时第一个SYN报文段到达服务器
*
* 如果是两次握手:
* 服务器收到第一个SYN后直接建立连接,会一直等待客户端发送数据,浪费服务器资源
*
* 如果是三次握手:
* 服务器收到第一个SYN后回复SYN+ACK,但客户端此时已经不是SYN_SENT状态
* 会回复RST重置连接,服务器收到RST后关闭连接
*/
}
// 查看连接状态
// netstat -antp | grep ESTABLISHED
四次挥手(断开连接)
客户端 服务器
| |
|---------> FIN=1, Seq=u ------------>| (1) 客户端主动关闭
| | (服务器状态: ESTABLISHED -> CLOSE_WAIT)
|<------- ACK=1, Seq=v, Ack=u+1 -------| (2) 服务器确认
| | (客户端状态: FIN_WAIT_1 -> FIN_WAIT_2)
| | (服务器可以继续发送未完成的数据)
|<------- FIN=1, ACK=1, Seq=w, Ack=u+1 | (3) 服务器发送FIN
| | (服务器状态: CLOSE_WAIT -> LAST_ACK)
|---------> ACK=1, Seq=u+1, Ack=w+1 -->| (4) 客户端确认
| | (客户端状态: FIN_WAIT_2 -> TIME_WAIT)
| | (服务器状态: LAST_ACK -> CLOSED)
// 客户端在TIME_WAIT状态等待2MSL后关闭
为什么需要TIME_WAIT?
public class TimeWaitExplanation {
// TIME_WAIT持续时间 = 2 * MSL (Maximum Segment Lifetime)
// MSL通常为30秒、1分钟或2分钟
/*
* 原因1:保证最后一个ACK能够到达服务器
* 如果ACK丢失,服务器会重发FIN,客户端在TIME_WAIT期间可以重发ACK
*
* 原因2:防止已失效的连接请求报文段出现在新连接中
* 等待足够长时间,让本次连接的所有报文段在网络中消失
*/
// 查看TIME_WAIT数量
// ss -tan | grep TIME-WAIT | wc -l
// netstat -tan | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
}
1.3 TCP核心机制深入
滑动窗口与流量控制
/*
* 滑动窗口:解决端到端的流量控制,防止发送方发送速度超过接收方处理能力
*
* 窗口大小 = 接收方可用缓冲区大小
* 发送方窗口 = min(接收方窗口, 拥塞窗口)
*/
// TCP报文段中的窗口字段(16位),最大65535字节
// 窗口扩大因子选项(Window Scale),可将窗口扩大至1GB
struct tcphdr {
__u16 window; // 16位窗口大小
// ...
};
// 查看TCP窗口大小
// ss -tni | grep -E "cwnd|rtt|rto"
拥塞控制算法
public class CongestionControl {
/*
* 拥塞控制四个阶段:
* 1. 慢启动 (Slow Start) -> cwnd = 1 MSS,每收到一个ACK,cwnd翻倍
* 2. 拥塞避免 (Congestion Avoidance) -> cwnd线性增长
* 3. 快重传 (Fast Retransmit) -> 收到3个重复ACK,立即重传
* 4. 快恢复 (Fast Recovery) -> cwnd = ssthresh + 3 MSS
*/
// 查看当前拥塞控制算法
// sysctl net.ipv4.tcp_congestion_control
// 可选: cubic, bbr, reno, vegas
// BBR (Bottleneck Bandwidth and RTT) - Google提出的新算法
// 基于带宽和RTT测量,而非丢包
// sysctl net.core.default_qdisc=fq
// sysctl net.ipv4.tcp_congestion_control=bbr
}
// TCP内核参数优化
// /etc/sysctl.conf
net.ipv4.tcp_tw_reuse = 1 # 允许重用TIME_WAIT的socket
net.ipv4.tcp_tw_recycle = 0 # 不建议开启(NAT环境下有问题)
net.ipv4.tcp_fin_timeout = 30 # FIN_WAIT2超时时间
net.ipv4.tcp_keepalive_time = 1200 # keepalive探测间隔
net.ipv4.tcp_keepalive_intvl = 30 # 探测失败后重试间隔
net.ipv4.tcp_keepalive_probes = 3 # 探测次数
net.ipv4.tcp_max_syn_backlog = 8192 # SYN队列长度
net.ipv4.tcp_syncookies = 1 # 防止SYN Flood攻击
net.core.somaxconn = 32768 # listen队列最大长度
net.core.netdev_max_backlog = 5000 # 网卡队列长度
1.4 TCP与UDP对比及选型
// UDP高性能场景:Kafka内部使用UDP进行节点间通信(基于自研协议)
// QUIC协议:基于UDP的可靠传输(HTTP/3)
二、I/O模型全面对比
2.1 五种I/O模型详解
┌─────────────────────────────────────────────────────────────────────────────────┐
│ 五种I/O模型对比 │
├─────────────────────────────────────────────────────────────────────────────────┤
│ 1. 阻塞I/O (Blocking I/O) │
│ 应用发起recvfrom → 内核等待数据 → 数据准备完成 → 从内核复制到用户空间 → 返回 │
│ 整个过程线程阻塞 │
│ │
│ 2. 非阻塞I/O (Non-blocking I/O) │
│ 应用发起recvfrom → 内核立即返回EWOULDBLOCK → 轮询 → 数据准备好 → 阻塞复制 │
│ 轮询消耗CPU │
│ │
│ 3. I/O复用 (I/O Multiplexing) │
│ select/poll/epoll → 内核监听多个fd → 某个fd就绪 → recvfrom → 阻塞复制 │
│ 一个线程管理多个连接 │
│ │
│ 4. 信号驱动I/O (Signal-driven I/O) │
│ 注册信号处理函数 → 内核数据准备好时发送SIGIO信号 → recvfrom → 阻塞复制 │
│ │
│ 5. 异步I/O (Asynchronous I/O) │
│ aio_read → 内核完成整个操作(等待+复制)→ 通知应用 → 直接使用数据 │
│ 真正的非阻塞 │
└─────────────────────────────────────────────────────────────────────────────────┘
2.2 select/poll/epoll 深度对比
// 1. select(POSIX标准,所有平台支持)
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// 缺点:
// - 单个进程可监视的fd数量有限制(默认1024,可重编译内核修改)
// - 每次调用需要将fd集合从用户态拷贝到内核态
// - 需要遍历所有fd才能找到就绪的
// - O(n)复杂度,n为fd总数
// 2. poll(无数量限制)
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
// 改进:无最大数量限制,但仍需遍历所有fd
// 3. epoll(Linux专属,高性能)
int epoll_create(int size); // 创建epoll实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改/删除
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// epoll的优势:
// - 红黑树存储fd,增删改查O(log n)
// - 就绪链表存储活跃fd,返回时直接复制,O(1)
// - 内存映射技术(mmap),减少用户态/内核态拷贝
// epoll的两种工作模式:
// LT(水平触发,Level Triggered):默认模式,只要fd有数据,每次epoll_wait都会通知
// ET(边缘触发,Edge Triggered):仅在fd状态变化时通知一次,需要一次性读完数据
// Java NIO中的Selector(底层基于epoll)
public class EpollSelectorDemo {
public static void main(String[] args) throws IOException {
// 创建Selector(Linux下使用EPollSelectorProvider)
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
// 注册OP_ACCEPT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 阻塞等待就绪事件(底层调用epoll_wait)
int readyCount = selector.select();
if (readyCount == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须手动移除
if (key.isAcceptable()) {
// 接受新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = client.read(buffer);
if (read == -1) {
client.close();
} else {
buffer.flip();
// 处理数据...
}
}
}
}
}
}
2.3 零拷贝技术
public class ZeroCopyDemo {
/*
* 传统文件传输:4次上下文切换 + 4次拷贝
* read() → 用户缓冲区 → write() → socket缓冲区
*
* 零拷贝:2次上下文切换 + 2次拷贝(DMA直接内存访问)
* sendfile() → 内核缓冲区 → socket缓冲区
*/
// 传统方式
public void traditionalCopy(String from, String to) throws IOException {
try (FileInputStream fis = new FileInputStream(from);
FileOutputStream fos = new FileOutputStream(to)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
}
// 零拷贝方式(FileChannel.transferTo)
public void zeroCopyTransfer(String from, String to) throws IOException {
try (FileChannel fromChannel = new FileInputStream(from).getChannel();
FileChannel toChannel = new FileOutputStream(to).getChannel()) {
// transferTo底层调用sendfile系统调用
fromChannel.transferTo(0, fromChannel.size(), toChannel);
}
}
// 网卡到网卡(Kafka使用)
// FileChannel.transferTo(position, count, socketChannel)
// 直接将文件数据从磁盘通过DMA拷贝到网卡,不经过用户态
}
// Linux sendfile系统调用
// ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);