前言: 服务器网络编程是构建高性能、可扩展、可靠网络服务(如 Web 服务器、数据库、游戏后端、微服务等)的核心技术。其核心围绕 网络通信模型、I/O 模型、并发处理、协议实现与性能优化 展开。以下从底层到高层,系统详解服务器网络编程的关键技术。
一、基础:网络通信模型
1. OSI 与 TCP/IP 模型
- 服务器编程主要工作在 传输层(TCP/UDP) 和 应用层(HTTP、gRPC 等)。
- TCP:面向连接、可靠、有序、流量控制 → 适用于大多数业务场景(如 Web、数据库)。
TCP协议基础: 传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议.交互流程如下图:

- 注意:超时才能解决物理连接中断了默认的超时,可能长达两个小时;弥补:心跳包的机制,强制询问双方是否在线,如果终止,或者异常连续超过若干次(三次)。
- UDP:无连接、不可靠、低延迟 → 适用于实时音视频、DNS、游戏等。
2. 套接字(Socket)
- 是网络编程的抽象接口,由 IP 地址 + 端口号 唯一标识一个通信端点。

- 基本操作流程(以 TCP服务端 为例):
1
socket() → bind() → listen() → accept() → read()/write() → close()
- 监听套接字(Listening Socket):用于接收新连接。
- 已连接套接字(Connected Socket):用于与客户端通信。
- 服务端代码:
1 |
|
- 基本操作流程(以 TCP客户端 为例):
1
socket() → connect() → read()/write() → close()
- 客服端代码:
1 |
|
- 基于TCP服务端/客户端的函数调用关系

3. TCP 内部原理
- 三次握手
一个小故事:TCP 三次握手好比在一个夜高风黑的夜晚,你一个人在小区里散步,不远处看见小区里的一位漂亮妹子迎面而来,但是因为路灯有点暗等原因不能100%确认,所以要通过招手的方式来确定对方是否认识自己。
你首先向妹子招手(syn),妹子看到你向自己招手后,向你点了点头挤出了一个微笑(ack)。你看到妹子微笑后确认了妹子成功辨认出了自己(进入established状态)。
但是妹子有点不好意思,向四周看了一看,有没有可能你是在看别人呢,她也需要确认一下。妹子也向你招了招手(syn),你看到妹子向自己招手后知道对方是在寻求自己的确认,于是也点了点头挤出了微笑(ack),妹子看到对方的微笑后确认了你就是在向自己打招呼(进入established状态)。

- 四次挥手

4. UDP 内部原理
- 基本操作流程(以 UDP服务端 为例):
1
socket() → bind() → recvfrom() → sendto() → close()
- 服务端代码:
1 |
|
- 基本操作流程(以 UDP客户端 为例):
1
socket() → sendto() → recvfrom() → close()
- 客服端代码:
1 |
|
二 网络通信中的阻塞和非阻塞IO的区别
阻塞 I/O(Blocking I/O) 和 非阻塞 I/O(Non-blocking I/O) 的核心区别在于:当发起 I/O 请求时,数据尚未准备好(内核缓冲区为空),操作系统是否将当前线程挂起(休眠)。
- 阻塞 I/O:如果数据没到,线程放弃 CPU 进入休眠,直到数据到达内核缓冲区,线程被唤醒,将数据拷贝到用户空间,函数才返回。
- 非阻塞 I/O:如果数据没到,线程不会被挂起,函数立即返回一个错误码(如
EAGAIN或EWOULDBLOCK),线程可以继续做其他事,后续轮询检查数据是否到达。
1. 系统调用层面的“时间线”对比
假设执行 recv(fd, buffer, size, 0) 读取 Socket 数据:
| 阶段 | 阻塞 I/O(如默认的 socket) |
非阻塞 I/O(设置 O_NONBLOCK) |
|---|---|---|
| ① 调用 recv | 线程陷入内核态,检查接收缓冲区 | 线程陷入内核态,检查接收缓冲区 |
| ② 缓冲区无数据 | 线程挂起(休眠),从运行队列移出,让出 CPU | 立即返回 -1,errno = EAGAIN,线程继续运行 |
| ③ 数据到达内核 | 网卡中断唤醒线程,放入运行队列 | 线程下次轮询(再次调用 recv)时发现数据就绪 |
| ④ 数据拷贝 | 线程从内核缓冲区拷贝数据到用户缓冲区,此过程依然占用 CPU | 线程负责拷贝数据到用户缓冲区(此过程依然会短暂阻塞,但极快) |
| ⑤ 返回 | 返回读取的字节数 | 返回读取的字节数 |
2. 阻塞 I/O 和非阻塞 I/O 的“致命误区”
很多初学者以为“非阻塞 I/O 就是完全不会卡住”,这是错的。
- 非阻塞,指的是等待数据阶段(阶段②)不卡。它只保证你在数据没准备好时不被挂起。
- 数据拷贝阶段(阶段④):无论是阻塞还是非阻塞,将数据从内核拷贝到用户空间的过程都是必须由线程执行的,且这一步是同步的、会占用 CPU 时间片。
真正的“完全不卡”需要异步 I/O(AIO),如 Windows IOCP 或 Linux io_uring,它们由内核负责全程拷贝,拷贝完成后再通知应用。
3. 编程模型与代码示例
阻塞 I/O(传统多线程模型)
1 | // 伪代码 |
- 优点:代码逻辑直观,像读文件一样顺序执行。
- 缺点:每个连接必须独占一个线程。如果 1 万个连接,就要 1 万个线程,导致巨大的上下文切换开销和内存栈占用(每个线程默认 8MB),这就是著名的 C10K 问题。
非阻塞 I/O(配合 epoll / Reactor 模式)
1 | // 1. 设置 fd 为非阻塞 |
- 优点:一个线程可以管理成千上万个 fd(
epoll监听),大大节省了线程资源。 - 缺点:需要维护复杂的状态机。例如,读取一个 HTTP 请求可能需要分多次
recv,你得手动拼接数据包。
4. 阻塞/非阻塞 与 同步/异步 的关系(四象限图)
这是网络编程中最容易混淆的维度:
| 阻塞 (Blocking) | 非阻塞 (Non-blocking) | |
|---|---|---|
| 同步 (Synchronous) | 传统阻塞 I/O (线程休眠等待) |
非阻塞 I/O + 轮询/epoll (主动检查就绪,但拷贝是自己做的) |
| 异步 (Asynchronous) | (理论上存在,实践中极少用) | 真正的 AIO (IOCP/io_uring) (内核代劳拷贝,完成后回调/唤醒协程) |
- 阻塞/非阻塞 = 调用者在等待数据就绪时是否被挂起。
- 同步/异步 = 数据从内核到用户的拷贝过程是由用户线程亲自做的(同步),还是由内核代理做的(异步)。
结论:阻塞 I/O 一定同步;非阻塞 I/O 可以是同步(轮询)也可以是异步(io_uring)。
5. 实际工程选型建议
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 连接数极少(< 100),内部工具脚本 | 阻塞 I/O + 多线程 | 开发最快,心智负担最低。 |
| 高并发服务端(如网关、游戏服务器),连接数 > 1000 | 非阻塞 I/O + epoll/Reactor | 节省线程资源,配合 C++20 协程可写回同步风格(co_await 封装 epoll)。 |
| 极致性能(数据库、消息队列),追求零拷贝 | 非阻塞 + io_uring/Proactor | 彻底解放 CPU,让内核负责搬运,应用只处理业务。 |
| 移动端/客户端 UI 编程 | 非阻塞 I/O | 绝不允许主线程卡死导致界面失去响应。 |
6. 针对你之前 QUIC 场景的关联
你之前问的 QUIC + C++20 协程,在底层如果使用 epoll,那就是非阻塞同步 I/O(Reactor);如果使用 io_uring,那就是非阻塞异步 I/O(Proactor)。
- 无论是哪种,你都不会使用阻塞 I/O,因为 QUIC 面向高并发,阻塞一个线程就损失大量性能。
- 而在 C++20 协程加持下,你写的代码是
co_await socket.async_read(),看起来像阻塞,实质底层是epoll_wait挂起协程(释放线程),数据就绪后再恢复。这是一种“用户态阻塞,内核态非阻塞”的优雅封装。
在高并发场景下处理 Socket 连接,核心原则是:绝不能用一个线程(或进程)去服务一个连接,必须采用事件驱动 + 非阻塞 I/O 的架构。
以下是现代 C/C++ 服务端处理并发连接的标准实践,从架构选型、线程模型到落地细节,逐一拆解。
三 同步IO和异步IO的区别?
同步 I/O 和异步 I/O 的核心区别在于:数据从内核缓冲区拷贝到用户缓冲区的操作,是由谁(调用者线程还是操作系统)来完成,以及调用者是否必须等待这个拷贝过程结束。
- 同步 I/O(Synchronous I/O):发起 I/O 请求后,调用者线程亲自负责数据拷贝,并且必须等待整个 I/O 操作(从内核到用户空间的拷贝)彻底完成后,函数才返回。
- 异步 I/O(Asynchronous I/O):发起 I/O 请求后,调用者立即返回,不阻塞。操作系统内核独立完成全部 I/O 操作(包括将数据拷贝到调用者指定的缓冲区),操作完成后通过回调、信号或
future通知调用者。
1. I/O 操作的两个阶段(关键拆解)
理解网络/磁盘 I/O,必须把一次完整的读操作拆解为两个阶段:
- 阶段 1:等待数据准备。对于网络 Socket,即等待数据包从网卡到达内核缓冲区(
recv等待EPOLLIN);对于磁盘,即等待数据从磁盘加载到内核页缓存。 - 阶段 2:将数据从内核空间拷贝到用户空间。即将数据从内核缓冲区搬到应用程序的
char buf[]中。
| 类型 | 阶段 1(等待数据就绪) | 阶段 2(拷贝数据到用户态) | 调用者状态 |
|---|---|---|---|
| 同步阻塞 I/O | 阻塞等待 | 调用者线程亲自拷贝,且在此过程中阻塞 | 全程阻塞 |
| 同步非阻塞 I/O(轮询) | 轮询(不阻塞,但反复检查) | 调用者线程亲自拷贝(调用 recv),拷贝时短暂阻塞 |
阶段1不阻塞,阶段2阻塞 |
| 同步 I/O 多路复用(epoll) | Reactor 通知就绪,调用 recv |
调用者线程亲自拷贝 | epoll_wait 阻塞,拷贝时阻塞 |
| 异步 I/O(AIO / io_uring) | 操作系统后台等待 | 操作系统内核负责拷贝,完成后再通知应用 | 全程不阻塞 |
一句话总结:只要“将数据从内核拷贝到用户缓冲区”这一步必须由应用程序线程亲自调用 read/recv 来完成,那么该 I/O 就是“同步”的。
2. 阻塞/非阻塞 vs 同步/异步(四象限终极澄清)
绝大多数开发者的混淆源于将“阻塞/非阻塞”与“同步/异步”混为一谈。
- 阻塞/非阻塞:描述的是 “当前线程是否因为等待数据就绪而被挂起”(阶段1是否阻塞)。
- 同步/异步:描述的是 “数据拷贝工作是由谁完成,以及调用者是否等待拷贝完毕”(阶段2的责任方)。
由此产生经典的组合对应关系:
| 阻塞等待数据 | 非阻塞轮询数据 | |
|---|---|---|
| 线程亲自拷贝数据(同步) | 阻塞 I/O(阶段1+2都阻塞) | 非阻塞 I/O + epoll(阶段1轮询,阶段2阻塞拷贝) |
| OS 代理拷贝数据(异步) | (极少见,基本无意义) | 真正的异步 I/O(如 Windows IOCP / Linux io_uring) |
重要结论:
- 同步 I/O ≠ 阻塞 I/O:非阻塞 I/O 配上
epoll/select,虽然阶段1不阻塞,但因为阶段2是线程调用recv拷贝数据,所以它依然属于同步 I/O。这就是为什么epoll被称为同步 I/O 多路复用。 - 异步 I/O = 真正不阻塞:只要用户线程发起了请求,接下来直到数据出现在用户缓冲区之前,线程完全不需要参与任何系统调用,这是唯一的异步形态。
3. 实战对比:同步 I/O(epoll) vs 异步 I/O(io_uring)
结合你之前关心的 QUIC + C++20 协程,这两种模型的实际代码如下:
同步 I/O(epoll + 非阻塞 Socket)
1 | // 1. 设置非阻塞,注册 epoll |
异步 I/O(io_uring)
1 | // 1. 提交异步读请求:io_uring_prep_recv(sqe, fd, buffer, size, 0); |
4. 现实中的适用场景
优先选择同步 I/O(epoll/Reactor)
- 短连接、小包高频(如 HTTP API、RPC 调用)。
- 业务逻辑中有复杂计算,阻塞拷贝开销远小于业务处理。
- 需要更简单的错误处理(同步调用栈清晰,
errno直接可查)。 - 代表组件:Nginx、Redis、Netty(底层基于 epoll)。
优先选择异步 I/O(io_uring/IOCP)
- 大文件传输、视频流、数据库磁盘读写(拷贝数据量极大,若用同步 I/O 会长时间占用 CPU 进行内存搬运,拖垮事件循环)。
- 追求零拷贝与极致吞吐量,希望 CPU 专注业务逻辑而非搬运数据。
- IO 密集型场景,CPU 等待时间占比超过 70%。
- 代表组件:ScyllaDB(高性能 NoSQL)、现代存储引擎。
5. 针对你 C++20 协程开发的关键启示
你之前的构想“C++20 协程 + QUIC 避免回调”,在两种模式下的实现感受是相同的(都是 co_await async_read),但底层的性能特征完全不同:
- 如果在 epoll(同步 I/O) 上跑协程:
co_await挂起协程等待可读,恢复协程后调用recv拷贝数据。拷贝动作依然占用当前线程,如果 QUIC 数据包极大,会拖慢其他协程的调度。 - 如果在 io_uring(异步 I/O) 上跑协程:
co_await提交读请求给内核,内核完成后恢复协程。拷贝动作由内核异步完成,完全不占用业务线程。
最终选型建议:追求极致的低延迟和稳定的吞吐,io_uring(Proactor/异步) 是未来方向;追求成熟稳定、社区生态完善,epoll(Reactor/同步) 依然是线上生产的主力,配合 C++20 协程已经足够优雅。
四 Socket通信中如何处理并发连接?
在高并发场景下处理 Socket 连接,核心原则是:绝不能用一个线程(或进程)去服务一个连接,必须采用事件驱动 + 非阻塞 I/O 的架构。
以下是现代 C/C++ 服务端处理并发连接的标准实践,从架构选型、线程模型到落地细节,逐一拆解。
1. 架构演进:从 C10K 到 C1000M
处理并发连接,本质上是在**资源(线程/内存)与性能(吞吐/延迟)**之间做权衡。
| 模型 | 实现方式 | 并发上限 | 适用场景 |
|---|---|---|---|
| 多进程/多线程(阻塞 I/O) | accept 后 fork/pthread_create |
~几百(受线程栈 8MB 限制) | 内部管理工具,并发极低 |
| 进程池/线程池(阻塞 I/O) | 预先创建固定线程,accept 抢锁 |
~几千 | 传统 Java BIO,已被淘汰 |
| I/O 多路复用(非阻塞 I/O + epoll) | 单线程/少量线程管理上万 fd | 10万 ~ 100万 | 当前业界主流(Nginx/Redis/Netty) |
| 异步 I/O(io_uring / IOCP) | 内核代理完成拷贝,应用只收完成通知 | 百万级 + 零拷贝 | 未来方向(高性能数据库/消息队列) |
2. 工业级标准实现:Reactor 模式(epoll + 非阻塞 I/O)
这是当前 C++ 后端最成熟、最普遍的方案,几乎所有知名的网络库(libevent、muduo、Boost.Asio)都基于此。
线程模型选型(关键决策)
- 单 Reactor 单线程(如 Redis):适合业务处理极快(纯内存操作),不存在耗时计算。若业务逻辑涉及磁盘 I/O 或 RPC,会阻塞整个事件循环。
- 主从 Reactor 多线程(如 Nginx、Netty 的
EventLoopGroup):- 主 Reactor(1 个):专门负责
accept新连接,极轻量,只做新连接的准入。 - 从 Reactor(N 个,通常 = CPU 核数):每个线程拥有独立的
epoll实例,负责已连接 Socket 的读写事件分发和业务处理。 - 连接分发:主 Reactor
accept后,通过轮询(Round-Robin)或哈希将新 Socket 分配给某个从 Reactor 线程,保证该连接的生命周期始终由同一个线程管理(避免锁竞争)。
- 主 Reactor(1 个):专门负责
1 | // 伪代码:主从 Reactor 事件循环结构 |
3. 必须掌握的底层优化细节(避坑指南)
- 必须设置 Socket 为非阻塞(
O_NONBLOCK):epoll的触发机制依赖于此。如果在EPOLLIN事件到达后,你用阻塞的recv去读取大包,依然会卡住线程。 - 边缘触发(ET, Edge-Triggered) vs 水平触发(LT, Level-Triggered):
- 追求极致性能时选 ET(如 Nginx)。ET 只在状态变化(从空变非空)时通知一次,要求你必须循环读取直到返回
EAGAIN,这能显著减少epoll_wait的系统调用次数。 - 为了开发便捷选 LT(如 Redis)。LT 会重复通知直到数据处理完毕,逻辑简单,不容易漏读,但系统调用频率稍高。
- 追求极致性能时选 ET(如 Nginx)。ET 只在状态变化(从空变非空)时通知一次,要求你必须循环读取直到返回
- 惊群效应(Thundering Herd):
- 传统多进程
accept会导致所有进程被唤醒抢同一个新连接。解决方式:- Linux 内核 3.9+ 使用
SO_REUSEPORT选项,允许多个 Socket 绑定同一端口,内核自动负载均衡,彻底解决惊群。 - 或主线程独占
accept,其他线程只处理读写(主从 Reactor 天然规避)。
- Linux 内核 3.9+ 使用
- 传统多进程
- 处理“粘包”与“半包”:TCP 是流式协议,必须自行维护接收缓冲区(Buffer)。应用层需要定义协议(如固定头 + body 长度),解析出完整包后再交给业务逻辑,绝不能假设单次
recv返回的就是一个完整消息。
4. 如果业务逻辑耗时较长怎么办?(异步化)
事件循环线程(Reactor)绝对不能阻塞在 CPU 密集型计算或磁盘 I/O 上。
- 方案一:将耗时任务提交给独立的线程池。Reactor 线程读完数据后,将任务打包放入队列,由后台工作线程(Worker Threads)计算,计算完成后再通过
eventfd或pipe唤醒 Reactor 线程发送结果。 - 方案二:使用 Proactor /
io_uring。将磁盘读写等重 I/O 完全丢给内核,Reactor 只负责接收“完成事件”,处理纯业务逻辑。
5. 现代 C++ 的终极解法:协程 + Reactor
这也是你之前关心的方向。C++20 协程本质上是对 Reactor(epoll)的优雅封装。
1 | // 传统状态机写法(晦涩) |
这一层封装极大降低了开发者的心智负担,让你用同步编程的逻辑享受到非阻塞 I/O 的高并发红利。
总结:高并发 Socket 处理的三层架构清单
- 监听层:
SO_REUSEPORT+ 主 Reactor(或内核自动均衡),避免 accept 竞争。 - 读写与解析层:多从 Reactor(线程数 = CPU 核数),非阻塞 I/O + ET 触发,每连接配独立 Buffer 解析协议。
- 业务处理层:若业务纯内存且极快,直接在 Reactor 线程处理;若耗时,通过无锁队列交给线程池,或利用
io_uring异步完成。
五 IO多路复用的实现机制?
I/O 多路复用(I/O Multiplexing) 的本质是:单个线程通过一次系统调用(如 select/poll/epoll),同时监听多个文件描述符(Socket/文件),内核返回哪些描述符已就绪(可读/可写/异常),从而避免为每个连接创建线程。
现代 Linux 下,epoll 是性能最优、最通用的实现。下面我将从 通用原理 和 具体实现机制 两个维度,层层剥开其内部工作原理。
1. 三大实现机制对比(select / poll / epoll)
| 机制 | 数据结构 | 最大监视数 | 内核态扫描方式 | 数据拷贝方式 | 触发模式 |
|---|---|---|---|---|---|
| select | 固定大小的位图(fd_set) |
1024(硬编码限制) | 轮询(O(n)) 扫描全部 fd | 每次调用将整个 fd 集合在用户态与内核态之间全量拷贝 | 仅水平触发(LT) |
| poll | 动态链表(pollfd 数组) |
无上限(受系统内存限制) | 轮询(O(n)) 扫描全部 fd | 每次调用将整个 fd 数组在用户态与内核态之间全量拷贝 | 仅水平触发(LT) |
| epoll | 内核红黑树 + 就绪链表(epitem) |
无上限(受系统内存限制) | 事件驱动(回调),仅返回就绪 fd,O(1) 复杂度 | 零拷贝,使用 epoll_wait 通过共享内存(mmap)返回事件,无需全量拷贝 |
支持 LT(水平触发) 和 ET(边缘触发) |
2. 深入底层:epoll 的“杀手锏”实现机制
理解 epoll 为什么快,只需看它的三个核心 API 和内核数据结构。
2.1 三个核心 API
epoll_create():在内核中创建一颗红黑树(用于存储所有被监听的 fd) 和一个双向就绪链表(用于存储已就绪的 fd),返回一个 epfd 句柄。epoll_ctl(epfd, ADD/MOD/DEL, fd, event):将目标 fd 添加到红黑树中(或修改/删除),并注册一个回调函数(callback) 与网卡中断关联。epoll_wait(epfd, events, maxevents, timeout):检查就绪链表是否为空。若为空且未超时,当前线程进入睡眠;若不为空,则将就绪事件通过共享内存复制到用户态的events数组并返回。
2.2 为什么 epoll 不需要全量拷贝?(关键零拷贝)
select/poll 每次调用时,用户态需要把全部 fd 数组整体拷贝进内核,内核处理完再整体拷出来。这是 O(n) 的内存复制开销。
epoll 的做法:epoll_wait 只返回就绪事件列表(数量通常远小于全量 fd),且通过内核和用户共享同一块内存(mmap 映射),避免了内核态到用户态的数据拷贝开销。
2.3 事件就绪通知机制(回调 vs 轮询)
- select/poll:内核在系统调用期间,暴力遍历(O(n)) 所有被监视的 fd,挨个检查它们的
sk_wait_data队列状态。 - epoll:当网卡收到数据包触发软中断时,中断处理程序会调用该 Socket 的回调函数,将就绪的
epitem节点直接挂接到内核的就绪链表中。epoll_wait只用检查该链表是否为空即可,复杂度 O(1)。
3. 水平触发(LT)与边缘触发(ET)的本质区别
这是 epoll 独有的设置,也是面试必问点。
- LT(Level Triggered,水平触发):默认模式。只要 fd 缓冲区中还有数据未读完,
epoll_wait就会反复通知应用程序。这与select/poll行为一致,不容易漏读数据,编码简单安全。 - ET(Edge Triggered,边缘触发):高速模式。仅当 fd 的缓冲区从“空”变为“非空”(或“不满”变为“满”)的状态变化瞬间通知一次。要求应用程序必须循环读取,直到
recv返回EAGAIN为止。否则剩余数据将永远沉睡,导致漏读。
ET 的优势:减少了 epoll_wait 系统调用的触发次数,极大降低了上下文切换成本。Nginx、Memcached 等高性能组件默认使用 ET 模式。
4. 完整的 epoll 事件循环伪代码(经典 Reactor 模式)
1 | // 1. 创建 epoll 实例 |
5. select/poll 的“致命缺陷”(为什么被 epoll 取代)
- 内存拷贝开销大:每次调用
select/poll都要在内核和用户空间之间复制整个 fd 集合。如果有 10 万个连接,即使只有 1 个活跃,也得复制 10 万个 fd 的数据结构。 - 时间复杂度高:内核需要通过遍历整个数组来检查就绪状态,O(n)。当 n 达到十万级时,
select/poll的系统调用延迟会急剧上升(超过 1 毫秒),而epoll_wait仍能保持在微秒级。
6. 补充:其他操作系统的 I/O 多路复用
| 操作系统 | 接口 | 特点 |
|---|---|---|
| Linux | epoll |
基于回调 + 红黑树,最成熟、最推荐 |
| FreeBSD/macOS | kqueue |
功能类似 epoll,支持多种事件类型(文件、信号、进程) |
| Windows | IOCP(Proactor) |
真正的异步 I/O,而非 I/O 多路复用(接口思路不同) |
| 跨平台封装 | libevent / Boost.Asio |
底层自动选择 epoll/kqueue/IOCP,提供统一 API |
总结:到底该怎么选?
- 并发连接数很少(小于 1000):
select/poll足够简单,无需引入 epoll 的复杂性。 - 高并发服务端(连接数 > 5000):必须用 epoll(Linux 平台)。配合非阻塞 I/O + ET 模式,搭配线程池或 C++20 协程,可以支撑百万连接。
- 涉及磁盘文件 I/O:注意
epoll不支持普通磁盘文件的监听(会返回EPERM)。大文件读写应配合io_uring或线程池。
核心认知:I/O 多路复用是“同步 I/O”(因为数据拷贝仍是线程亲自做),但它解决了“等待数据就绪”阶段的阻塞问题,是高性能网络编程的基石。
六、现代服务器开发框架/库
表格
| 语言 | 框架/库 | 模型 |
|---|---|---|
| C/C++ | libevent, libuv, Boost.Asio | Reactor |
| Java | Netty, Undertow | Reactor + 多线程 |
| Go | net/http, gnet | Goroutine(协程) |
| Python | asyncio, Tornado | Event-loop + 协程 |
| Rust | Tokio, async-std | Async/Await + Reactor |
七、性能调优要点
- 避免阻塞操作:数据库查询、文件 I/O 应异步化或交由线程池。
- 合理设置 backlog:
listen(sockfd, backlog)控制等待队列长度。 - TCP 参数调优:-
SO_REUSEADDR:快速重启绑定端口TCP_NODELAY:禁用 Nagle 算法(降低延迟)SO_RCVBUF/SO_SNDBUF:调整缓冲区大小
- 使用 ET 模式 + 非阻塞 socket:最大化 epoll 效率。
- 负载均衡:单机性能有限,需结合 LVS/Nginx 做集群。
八、安全考虑
- 防止 DDoS:连接数限制、速率限制
- 防止 缓冲区溢出:严格校验输入长度
- 使用 TLS/SSL:加密通信(OpenSSL、BoringSSL)
- 避免 Slowloris 攻击:设置读写超时
总结
服务器网络编程的核心在于:
“用最少的资源,高效、可靠地处理海量并发连接。”
掌握以下四点即可构建高性能服务器:
- 理解 I/O 模型(尤其是 epoll/kqueue)
- 选择合适的并发模型(Reactor / 协程)
- 处理 TCP 流特性(粘包、心跳、超时)
- 避免性能陷阱(阻塞、内存拷贝、锁竞争)
详解IPv4 和 IPv6
IP 地址(Internet Protocol Address)是用于在网络中唯一标识设备的逻辑地址。它使得设备之间能够相互通信。目前广泛使用的 IP 地址主要有两种版本:IPv4 和 IPv6。下面分别详细说明它们的组成、格式、特点及区别。
一、IPv4 地址
1. 基本组成
- IPv4(Internet Protocol version 4)使用 32 位(4 字节) 的地址空间。
- 总共可表示
255*255*255*255 =4,294,967,296232个地址(约 43 亿个)。
2. 表示方式
- 采用 点分十进制(Dotted Decimal Notation) 表示法:- 将 32 位地址分为 4 个 8 位(1 字节)的段,每段转换为十进制数,用点号
.分隔。- 例如:
192.168.1.1
- 例如:
3. 结构划分
IPv4 地址由两部分组成:
- 网络号(Network ID):标识所属网络。
- 主机号(Host ID):标识该网络中的具体设备。
根据地址的前几位,IPv4 地址被划分为 A、B、C、D、E 五类:
表格
| 类别 | 范围(首字节) | 网络位数 | 主机位数 | 用途 |
|---|---|---|---|---|
| A | 1 – 126 | 8 位 | 24 位 | 大型网络 |
| B | 128 – 191 | 16 位 | 16 位 | 中型网络 |
| C | 192 – 223 | 24 位 | 8 位 | 小型网络 |
| D | 224 – 239 | — | — | 组播(Multicast) |
| E | 240 – 255 | — | — | 保留(实验用) |
注意:127.x.x.x 是回环地址(如 127.0.0.1),不用于实际网络通信。
- 以0开头的地址,都是不可以用的
- 以0结尾的地址,表示的是网段,而不是具体的地址
- 224开头到239开头的地址,是组播地址,不可用于点对点的传输
4. 特殊地址
- 0.0.0.0:表示默认路由或“任意地址”。
- 255.255.255.255:受限广播地址。
- 127.0.0.1:本地回环地址。
- 169.254.x.x:APIPA(自动私有 IP 寻址),当 DHCP 失败时自动分配。
- 私有地址(不可在公网路由):- A 类:10.0.0.0 – 10.255.255.255
- B 类:172.16.0.0 – 172.31.255.255
- C 类:192.168.0.0 – 192.168.255.255
5. 子网掩码(Subnet Mask)
- 用于区分网络号和主机号。
- 例如:
255.255.255.0表示前 24 位是网络号,后 8 位是主机号(即 /24)。
二、IPv6 地址
1. 基本组成
- IPv6(Internet Protocol version 6)使用 128 位(16 字节) 地址空间。
- 总共可表示 2128≈3.4×10382128≈3.4×1038 个地址,几乎无限。
2. 表示方式
- 采用 冒号十六进制(Colon-Hexadecimal) 表示法:- 将 128 位地址分为 8 段,每段 16 位(4 个十六进制数字),用冒号
:分隔。- 例如:
2001:0db8:85a3:0000:0000:8a2e:0370:7334
- 例如:
压缩规则(简化写法):
- 前导零可省略:
2001:db8:85a3:0:0:8a2e:370:7334 - 连续全零段可用
::代替(仅一次):- 上例可进一步压缩为:2001:db8:85a3::8a2e:370:7334
3. 地址类型
IPv6 不再使用“类别”概念,而是按功能分为三类:
表格
| 类型 | 说明 |
|---|---|
| 单播(Unicast) | 标识单个接口,数据包发往一个目标。 |
| 组播(Multicast) | 标识一组接口,数据包发往多个目标(取代广播)。 |
| 任播(Anycast) | 标识一组接口,但数据包只送达“最近”的一个(按路由距离)。 |
IPv6 没有广播地址,广播功能由组播实现。
4. 常见 IPv6 地址前缀
::1/128:本地回环地址(相当于 IPv4 的 127.0.0.1)::/128:未指定地址(类似 0.0.0.0)fe80::/10:链路本地地址(Link-Local),仅在同一物理/逻辑链路内有效fc00::/7:唯一本地地址(ULA,Unique Local Address),相当于 IPv4 的私有地址2000::/3:全球单播地址(Global Unicast),可在互联网上路由
5. 地址结构(以全球单播为例)
典型的全球单播 IPv6 地址结构如下:
text编辑
1 | | 3 bits | 45 bits | 16 bits | 64 bits | |
- Global Routing Prefix:由 ISP 分配的全球路由前缀。
- Subnet ID:组织内部子网划分。
- Interface ID:设备接口标识,常由 MAC 地址通过 EUI-64 生成,或使用隐私扩展(随机生成)。
三、IPv4 与 IPv6 主要区别总结
表格
| 特性 | IPv4 | IPv6 |
|---|---|---|
| 地址长度 | 32 位 | 128 位 |
| 地址数量 | ~43 亿 | ~3.4×10³⁸ |
| 表示法 | 点分十进制 | 冒号十六进制 |
| 配置方式 | 手动 / DHCP | 自动配置(SLAAC)、DHCPv6 |
| 广播 | 支持 | 不支持(用组播替代) |
| 安全性 | 依赖附加协议(如 IPsec) | 内置 IPsec 支持 |
| 报头结构 | 较复杂,含校验和等 | 更简洁,无校验和,提高路由效率 |
| 移动性 | 有限支持 | 原生支持移动 IPv6 |
| NAT 需求 | 广泛使用(因地址不足) | 基本不需要(地址充足) |
四、过渡技术(IPv4 → IPv6)
由于 IPv4 仍在广泛使用,IPv6 的部署通常采用以下过渡机制:
- 双栈(Dual Stack):设备同时运行 IPv4 和 IPv6。
- 隧道(Tunneling):如 6to4、GRE,将 IPv6 包封装在 IPv4 中传输。
- 协议转换(Translation):如 NAT64,实现 IPv6 与 IPv4 互访。