程序员必备的十大技能(进阶版)之网络与高并发原理(一)

简介: 教程来源 http://yyvgt.cn/ 本文深度解析网络与高并发核心原理,涵盖TCP/IP协议栈、三次握手/四次挥手、滑动窗口、拥塞控制、五种I/O模型(含epoll机制)、零拷贝、Reactor/Proactor、Java NIO/Netty源码及全链路调优,助你突破性能瓶颈,成为高阶工程师。

在互联网应用日益复杂的今天,网络编程与高并发处理能力是区分普通程序员与高阶程序员的重要分水岭。初级程序员关注业务逻辑的正确实现,而进阶程序员则需要深入理解网络协议栈、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对比及选型
image.png

// 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);

来源:
http://bncne.cn/

相关文章
|
21小时前
|
安全 Java C++
【Java基础】集合框架: ConcurrentHashMap核心原理:JDK1.7 vs 1.8+ 区别、线程安全实现、分段锁 vs CAS+synchronized、扩容机制
ConcurrentHashMap是Java高并发场景下线程安全的哈希表实现,JDK1.7采用Segment分段锁(16段独立加锁),JDK1.8升级为CAS+synchronized细粒度桶锁,并引入红黑树与多线程协助扩容,显著提升性能与扩展性。
|
21小时前
|
存储 缓存 安全
【Java基础】集合框架: ArrayList vs LinkedList 核心区别、扩容机制(附《思维导图》+《面试高频考点清单》)
本文深入解析ArrayList与LinkedList的核心差异:前者基于动态数组,支持O(1)随机访问、尾部增删高效,但中间/头部操作需移动元素;后者基于双向链表,头部/尾部增删为O(1),但随机访问O(n)且内存开销大4–5倍。重点剖析ArrayList的1.5倍扩容机制及CPU缓存优势,澄清“LinkedList更适合队列”等常见误区。
|
21小时前
|
人工智能 安全 测试技术
基于Harness + Langgraph + A2A 写一个 Agent Team,实现一支硅基团队自己 写代码
基于Harness + Langgraph + A2A 写一个 Agent Team,实现一支硅基团队自己 写代码
基于Harness + Langgraph + A2A 写一个 Agent Team,实现一支硅基团队自己 写代码
|
21小时前
|
存储 安全 Java
【Java基础】集合框架: HashMap核心原理:JDK1.7 vs 1.8+ 区别、数据结构、哈希函数、扩容机制、put/get全流程、红黑树转换阈值(附《思维导图》+《面试高频考点清单》)
本文系统对比JDK1.7与1.8+中HashMap的底层原理,涵盖数据结构(数组+链表→+红黑树)、哈希函数、扩容机制、插入方式及并发问题等核心差异,助你深入理解性能优化逻辑与面试高频考点。
|
21小时前
|
NoSQL Java 关系型数据库
吐血整理:2026大厂后端技术岗笔面试高频100题
本文揭秘2026大厂后端面试新趋势:题库未变,但考法剧变——从死记硬背转向考察源码理解、线上排障与设计权衡三大能力。通过真实案例对比与可落地的准备方法,帮你告别无效刷题,直击面试官真实意图。
|
21小时前
|
安全 Java 数据建模
【Java基础】JDK17:密封类、模式匹配、Record类(附《思维导图》+《面试高频考点清单》)
Java 17作为LTS版本,重磅引入密封类、模式匹配与Record类三大特性:Record简化不可变数据建模,密封类精准控制继承边界,模式匹配(instanceof+switch)提升类型安全与代码简洁性。三者协同可优雅实现代数数据类型,标志着Java迈向更安全、简洁、表达力更强的现代编程语言。
【Java基础】JDK17:密封类、模式匹配、Record类(附《思维导图》+《面试高频考点清单》)
|
21小时前
|
人工智能 自然语言处理 安全
Claude Code 全攻略:命令大全+三种模式+记忆体系+实战工作流完整手册
Claude Code 是当前最流行的终端级 AI 编程助手,能够直接在命令行中完成代码生成、项目理解、文件修改、命令执行、错误修复等全流程开发工作。它不依赖图形界面、不占用额外资源,却能深度理解项目结构,自动生成规范代码,大幅提升研发效率。
2236 3
|
21小时前
|
前端开发 机器人 API
用两行代码将 AgentRun 集成到你的应用
AgentRun支持OpenAI协议,改两行代码即可将Agent无缝接入现有应用,兼容Python/Node.js/Java等;同时提供SDK、UI嵌入、IM机器人、云事件触发五种集成方式,开箱即用全链路能力。
|
21小时前
|
SQL 人工智能 自然语言处理
准确率 100% 的智能问数(Text2SQL)实践,还要关心什么指标?
润乾NLQ创新采用“规范文本+规则编译”架构,将口语转为可验证的中间语言,再确定性生成SQL,实现规范文本→SQL环节100%准确率。规避大模型幻觉,支持多表JOIN、子查询、聚合等复杂场景,实施门槛低、结果稳定可控。(239字)
|
21小时前
|
程序员 Windows
程序员必备的十大技能(进阶版)之网络与高并发原理(二)
教程来源 http://hllft.cn/ Reactor与Proactor是高性能I/O的两大核心模型:Reactor基于同步非阻塞+事件就绪通知(如epoll),由用户线程主动读写;Proactor基于异步I/O+操作完成通知(如IOCP/AIO),内核代为完成数据搬运。二者分别支撑Netty与Windows服务器等高并发架构。