服务器架构模型
五种网络IO模型
在 UNIX 网络编程中,网络 IO 模型指的是 “应用程序与内核之间如何协作完成 IO 操作(如数据读取)” 的机制。核心 IO 操作分为两个阶段:
- 数据准备阶段:内核将数据从网络中读取到内核缓冲区(等待数据到达);
- 数据复制阶段:内核将缓冲区中的数据复制到应用程序的用户缓冲区。
根据这两个阶段中 “应用程序是否阻塞”“阻塞的阶段”,可分为五种经典网络 IO 模型,从同步到异步依次如下:
一、阻塞 IO 模型(Blocking IO, BIO)
原理
应用程序调用 IO 函数(如recvfrom
)后,全程阻塞,直到两个阶段完成:
- 第一阶段(数据准备):应用程序阻塞等待内核将数据准备好(如等待客户端发送数据);
- 第二阶段(数据复制):数据准备好后,内核将数据从内核缓冲区复制到用户缓冲区,此过程中应用程序仍阻塞;
- 复制完成后,IO 函数返回,应用程序处理数据。
流程示例
// socket默认是阻塞的
recvfrom(sockfd, buf, BUF_SIZE, 0, &addr, &addrlen); // 调用后立即阻塞,直到数据复制完成
优缺点
- 优点:实现最简单,无需额外处理逻辑,适合低并发场景。
- 缺点:一个连接对应一个线程,若连接未就绪,线程会一直阻塞,导致资源浪费(线程休眠时不释放 CPU),并发能力极低(单机通常支持数百连接)。
适用场景
简单工具(如ping
)、低并发短连接服务(如内部小工具)。
二、非阻塞 IO 模型(Non-Blocking IO, NIO)
原理
应用程序将 socket 设置为非阻塞模式(fcntl
设置O_NONBLOCK
),调用 IO 函数时:
- 第一阶段(数据未准备):内核立即返回
EAGAIN
或EWOULDBLOCK
错误,应用程序不阻塞,可做其他事; - 应用程序通过轮询(反复调用 IO 函数) 检查数据是否就绪,直到数据准备好;
- 第二阶段(数据复制):数据准备好后,内核复制数据到用户缓冲区,此过程中应用程序仍会阻塞(复制阶段无法非阻塞);
- 复制完成后,IO 函数返回成功,应用程序处理数据。
流程示例
fcntl(sockfd, F_SETFL, O_NONBLOCK); // 设置非阻塞
while (1) {ret = recvfrom(sockfd, buf, BUF_SIZE, 0, &addr, &addrlen);if (ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {// 数据未准备好,继续轮询(可做其他事)continue;} else {// 数据读取完成,处理数据break;}
}
优缺点
- 优点:应用程序在数据未准备时不阻塞,可处理其他任务,比阻塞 IO 灵活。
- 缺点:轮询会占用大量 CPU 资源(频繁系统调用),效率低;数据复制阶段仍阻塞,未解决根本问题。
适用场景
对响应时间敏感、连接数少的场景(如实时监控小量设备)。
三、IO 多路复用模型(IO Multiplexing)
原理
通过一个IO 多路复用器(如select
/poll
/epoll
),让单个进程 / 线程同时监听多个 socket 的 IO 事件(如 “数据就绪”):
- 应用程序将需要监听的 socket 注册到多路复用器上,然后阻塞等待多路复用器的通知;
- 内核监听所有注册的 socket,当任意一个或多个 socket 的数据准备好时,多路复用器返回就绪的 socket 列表;
- 应用程序再针对就绪的 socket 发起 IO 调用(
recvfrom
),完成第二阶段(数据复制,此过程仍阻塞,但时间很短)。
核心优势
- 用一个线程管理多个连接,避免 “一连接一线程” 的资源浪费,解决了阻塞 IO 的并发瓶颈。
主流实现
select
:跨平台,但支持的 socket 数量有限(默认 1024),每次需遍历所有注册的 socket,效率低;poll
:解决select
的数量限制,但仍需遍历所有 socket,适合中小规模场景;epoll
(Linux):高效!通过 “事件回调” 机制,只返回就绪的 socket,无需遍历,支持百万级连接,是高性能服务器的核心(如 Nginx、Redis)。
流程示例(epoll)
int epfd = epoll_create(1);
struct epoll_event ev, events[1024];
ev.events = EPOLLIN; // 监听读事件
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 注册socketwhile (1) {// 阻塞等待事件就绪,返回就绪的socket数量int nfds = epoll_wait(epfd, events, 1024, -1);for (int i = 0; i < nfds; i++) {// 对就绪的socket读取数据(复制阶段,短阻塞)recvfrom(events[i].data.fd, buf, BUF_SIZE, 0, &addr, &addrlen);}
}
优缺点
- 优点:并发能力强(单机支持十万至百万级连接),资源开销低(少数线程即可),是高并发服务器的首选模型。
- 缺点:编程复杂度高于前两种模型;数据复制阶段仍阻塞(但时间极短,可接受)。
适用场景
高并发 IO 密集型服务(如 Web 服务器、消息中间件、Redis、Nginx)。
四、信号驱动 IO 模型(Signal-Driven IO)
原理
应用程序通过信号机制异步获取 “数据就绪” 通知,流程如下:
- 应用程序为 socket 注册一个信号(如
SIGIO
),并指定信号处理函数,调用后立即返回,不阻塞; - 内核在数据准备好时,向应用程序发送
SIGIO
信号; - 应用程序捕获信号后,在信号处理函数中调用 IO 函数(
recvfrom
),完成第二阶段(数据复制,此过程阻塞)。
优缺点
- 优点:数据准备阶段不阻塞,无需轮询,比非阻塞 IO 高效。
- 缺点:信号处理逻辑复杂(如信号队列溢出、信号合并),难以处理高并发;数据复制阶段仍阻塞,实际应用极少。
适用场景
几乎不用,仅在特定低并发、对轮询敏感的场景(如早期网络设备驱动)。
五、异步 IO 模型(Asynchronous IO, AIO)
原理
应用程序发起异步 IO 请求后立即返回,全程不阻塞,由内核完成所有 IO 操作:
- 应用程序调用异步 IO 函数(如
aio_read
),传入用户缓冲区、回调函数等参数,函数立即返回,应用程序可做其他事; - 内核自动完成两个阶段:等待数据准备→将数据复制到用户缓冲区;
- 所有操作完成后,内核通过回调函数或信号通知应用程序,应用程序直接处理数据即可。
关键区别
前四种模型均为同步 IO(应用程序需等待 “数据复制阶段” 完成),而异步 IO 是真正的异步:应用程序不参与任何 IO 阶段的阻塞,全程由内核处理。
主流实现
- Linux 的
libaio
(支持有限,不支持网络 IO 的异步通知); - Windows 的
IOCP
(成熟稳定,是高性能 Windows 服务的核心,如 IIS); - 编程语言层面的封装(如 Java 的
NIO.2
、Python 的asyncio
)。
优缺点
- 优点:理论上效率最高,应用程序无阻塞,资源利用率极致。
- 缺点:内核实现复杂(Linux 支持不完善),编程难度大,调试复杂。
适用场景
极致性能需求的场景(如高性能存储服务、Windows 平台高并发服务)。
总结:同步 IO vs 异步 IO
- 同步 IO(阻塞 IO、非阻塞 IO、IO 多路复用、信号驱动 IO):应用程序需等待 “数据从内核缓冲区复制到用户缓冲区”(第二阶段),此过程中可能阻塞或轮询。
- 异步 IO:应用程序无需参与任何 IO 阶段的等待,内核完成所有操作后通知应用程序。
五种模型对比表
模型 | 数据准备阶段 | 数据复制阶段 | 典型应用 |
---|---|---|---|
阻塞 IO | 阻塞 | 阻塞 | 简单工具 |
非阻塞 IO | 非阻塞(轮询) | 阻塞 | 实时监控小量连接 |
IO 多路复用 | 阻塞(等事件) | 阻塞(短) | Nginx、Redis、Netty |
信号驱动 IO | 非阻塞(信号) | 阻塞 | 极少使用 |
异步 IO | 非阻塞 | 非阻塞 | Windows IOCP、AIO 库 |
五种模型的演进,本质是减少应用程序的阻塞时间,提高资源利用率,其中 IO 多路复用因 “平衡了效率、复杂度和兼容性”,成为高并发网络编程的主流选择(如 Reactor 模式即基于 IO 多路复用)。
场景:小明去新华书店买《新华词典》
1 如果新华书店没有,就一直等着书店有了书之后才走(同步阻塞)
2 如果新华书店没有,先离开书店;然后每天都去书店逛 一次,直到书店到货了,买了就走。(同步非阻塞)
3 如果新华书店没有,留下电话号码;书店有货时,老板打电话通知他,他再去书店买书。(信号驱动IO 同步非阻塞)
4 如果新华书店没有,留下地址;书店有货时,老板直接把书送到家(异步非阻塞)
对应于程序:用户进程调用系统调用 ,用户进程对应小明,内核对应书店老板,书对应数据, 买书就是一个系统调用,而内核拷贝数据到进程这个过程近似于老板送书到风华手中
并发服务器模型
在网络编程中,并发服务器模型是指服务器同时处理多个客户端请求的设计模式。不同模型的核心差异在于 “如何管理连接”“如何分配资源处理请求”,其设计直接影响服务器的并发能力、资源开销和稳定性。以下是主流的并发服务器模型,从基础到高级依次解析:
一、基础模型:单进程 / 单线程模型(迭代服务器)
原理
- 服务器启动后创建一个进程(或线程),通过
listen
监听端口,一次只能处理一个客户端请求:处理完当前请求后,再接受下一个连接。 - 流程:
socket() → bind() → listen() → accept() → 处理请求(recv/send)→ close()
,循环往复。
它是一种单线程的应用程序,它只能使用短连接而不能使用长连接,缺点是无法充分利用多核CPU,不适合执行时间较长的服务,即适用于短连接(这样可以处理多个客户端),如果是长连接则需要在read/write之间循环,那么只能服务一个客户端。所以循环式服务器只能使用短连接,而不能使用长链接,否则无法处理多个客户端的请求,因为整个程序是一个单线程的应用程序。
优缺点
- 优点:实现最简单(几行代码即可),无进程 / 线程切换开销,资源占用极低。
- 缺点:并发能力为 0(同一时间只能处理一个客户端),若当前请求耗时(如大文件传输),其他客户端会被阻塞直至超时。
适用场景
- 仅用于低并发、短请求场景(如内部测试工具、简单命令行服务),实际生产中几乎不用。
二、多进程模型(每个连接一个进程)
原理
- 主进程负责监听端口(
listen socket
),每当有新客户端连接(accept()
返回client socket
),主进程通过fork()
创建子进程,由子进程单独处理该客户端的所有请求(读写、业务逻辑),主进程继续等待新连接。 - 子进程处理完请求后自动退出,或保持长连接持续处理。
优缺点
- 优点:
- 进程间完全隔离(内存空间独立),一个子进程崩溃不会影响主进程和其他子进程,稳定性高。
- 可利用多核 CPU(多个子进程并行运行)。
- 缺点:
- 进程创建 / 销毁开销大(
fork()
系统调用耗时,且每个进程占用独立内存空间)。 - 并发上限低(受系统进程数限制,通常几千个),不适合高并发场景。
- 进程间通信(IPC)复杂(需通过管道、共享内存等),不适合需要共享状态的服务。
- 进程创建 / 销毁开销大(
典型案例
- 早期 Apache 服务器的
prefork
模式(多进程静态资源服务)。 - 适合对稳定性要求极高、并发量中等的场景(如金融小批量交易服务)。
三、多线程模型(每个连接一个线程)
原理
- 类似多进程模型,但用线程替代进程:主线程监听端口,接受连接后创建子线程,由子线程处理客户端请求,主线程继续等待新连接。
- 线程比进程轻量:线程共享进程的内存空间(无需复制地址空间),创建 / 销毁开销远小于进程。
优缺点
- 优点:
- 并发能力高于多进程(线程更轻量,支持上万级连接)。
- 线程间共享内存,通信简单(通过全局变量 + 锁)。
- 缺点:
- 线程共享内存导致同步问题(如多个线程修改共享数据需加锁,可能引发死锁、性能损耗)。
- 仍有线程创建 / 销毁开销(高频连接场景下不可忽视)。
- 受系统线程数限制(默认栈大小几 MB,十万级线程会耗尽内存)。
典型案例
- 早期 Tomcat 的 BIO(阻塞 IO)模式(每个连接一个线程)。
- 适合中等并发、业务逻辑简单(锁竞争少)的场景。
四、线程池 / 进程池模型(预创建资源)
原理
- 解决多进程 / 多线程模型中 “动态创建资源开销大” 的问题:启动时预先创建一批进程 / 线程(池),当新连接到来时,从池中分配一个空闲进程 / 线程处理请求,处理完后放回池中复用,避免频繁创建 / 销毁。
优缺点
- 优点:
- 复用进程 / 线程,减少动态创建开销,响应速度更快。
- 控制资源总量(池大小固定),避免资源耗尽。
- 缺点:
- 池大小固定,若并发超过池容量,新连接需排队等待(可能超时)。
- 仍存在多进程 / 多线程的固有问题(进程池隔离好但开销大,线程池有锁竞争)。
典型案例
- Apache 的
worker
模式(线程池 + 多进程,平衡隔离性和效率)。 - 适合并发量稳定、请求处理时间短的场景(如静态资源服务器)。
五、IO 多路复用模型(事件驱动,Reactor 模式)
这是高并发服务器的 “核心模型”,彻底解决 “一连接一线程” 的资源瓶颈,通过单个或少数线程管理数万至百万级连接。
原理
- 基于IO 多路复用技术(select/poll/epoll/kqueue),由一个 “反应器”(Reactor)监听所有客户端 socket 的 IO 事件(连接、读就绪、写就绪),事件触发时再分配资源处理,避免无效阻塞。
- 核心是 “事件驱动 + 非阻塞 IO”:socket 设置为非阻塞,Reactor 通过 IO 多路复用等待事件,就绪后才处理,无需为每个连接阻塞等待。
常见变种(按 Reactor 数量和线程分工)
模型 | 核心设计 | 并发能力 | 适用场景 | 典型案例 |
---|---|---|---|---|
单 Reactor 单线程 | 1 个 Reactor 线程:负责监听事件、分发事件、处理业务逻辑(无线程池)。 | 高(IO 密集) | 业务逻辑简单(无阻塞操作),如 Redis(纯内存操作) | Redis |
单 Reactor 多线程 | 1 个 Reactor 线程(监听 + 分发事件)+ 线程池(处理业务逻辑,如数据库操作)。 | 高 | 业务逻辑有阻塞(如 DB 查询),中高并发 | 早期 Netty |
多 Reactor 多线程 | 主 Reactor(处理连接事件)+ 子 Reactor(多个,处理读写事件)+ 线程池(业务)。 | 极高 | 超高并发(十万级以上连接),如互联网服务 | Nginx、Netty(主流) |
优缺点
- 优点:
- 并发能力极强(单机能支持百万级连接,依赖 epoll 等高效 IO 多路复用)。
- 资源开销极低(少数线程即可管理大量连接)。
- 非阻塞 IO 避免无效等待,响应速度快。
- 缺点:
- 编程复杂度高(需处理事件注册、边缘触发 / 水平触发、非阻塞 IO 异常等)。
- 不适合 CPU 密集型业务(线程池若被 CPU 密集任务占满,会阻塞响应)。
核心优势
- 突破 “一连接一线程” 的限制,用 “事件触发” 替代 “阻塞等待”,是高并发服务器的标配(如 Nginx、Netty、Redis、MongoDB 均基于此模型)。
六、异步 IO 模型(Proactor 模式)
原理
- 与 Reactor(“IO 就绪后应用程序主动读写”)不同,Proactor 模式中,内核负责完成 IO 操作(读 / 写数据到用户缓冲区),完成后通知应用程序处理业务逻辑。
- 流程:应用程序发起异步读请求→内核异步读取数据到缓冲区→完成后通知应用程序→应用程序处理业务。
优缺点
- 优点:理论上效率更高(应用程序无需主动读写 IO)。
- 缺点:
- 内核实现复杂(Linux 的 aio 支持有限,Windows 的 IOCP 是成熟实现)。
- 编程难度极大,实际应用远少于 Reactor。
适用场景
- 仅在特定系统(如 Windows)或极致性能需求场景使用(如高性能存储服务)。
七、各模型对比与选型建议
模型 | 并发能力 | 资源开销 | 编程复杂度 | 适用场景(并发量 / 业务类型) |
---|---|---|---|---|
单进程 / 单线程 | 极低 | 极低 | 极低 | 测试工具、低并发短请求 |
多进程 | 中 | 高 | 中 | 高稳定性、低并发、强隔离需求(如金融交易) |
多线程 | 中高 | 中 | 中(需处理锁) | 中等并发、IO 密集、简单业务 |
线程池 / 进程池 | 中高 | 中 | 中 | 并发稳定、短请求(如静态资源服务) |
Reactor(IO 多路复用) | 极高 | 低 | 高 | 高并发(十万级 +)、IO 密集(如 Web 服务器、消息中间件) |
Proactor(异步 IO) | 极高 | 低 | 极高 | 特定系统(如 Windows)、极致性能需求 |
总结
并发服务器模型的演进,本质是 **“用更高效的资源管理方式支撑更高的并发”**:从 “一连接一资源(进程 / 线程)” 到 “少数资源管理多连接(事件驱动)”。实际选型需结合业务场景:
- 低并发、简单业务:选单线程或多线程模型(开发快)。
- 高并发、IO 密集(如 Web / 消息服务):必选 Reactor 模式(如基于 Netty 开发)。
- 强隔离、低并发:选多进程模型。
没有 “最优模型”,只有 “最合适的模型”—— 核心是平衡并发需求、资源成本和开发复杂度。
事件驱动
将「连接」和「业务线程」分开处理,当「连接层」有事件触发时提交给「业务线程」,避免了业务线程因「网络数据处于准备中」导致的长时间等待问题,节省线程资源,这就是大名鼎鼎的事件驱动模型
。
举个简单例子,10个士兵接到命令,在接下来将执行秘密任务,但具体时间待定;一种方式时,这10个士兵自己掌握主动权,隔一段时间就会自己询问将军是否准备执行任务,这种模式比较低下,因为士兵需要花很多精力自己去确认任务执行时间,同时也会耽搁自己的训练时间。
另一种方式为,士兵接到即将执行秘密任务的通知后,会自己做好准备随时执行,在最终执行命名没下达之前,会继续自己的日常训练;等需要执行任务时,将军会立刻通知士兵们立即行动;很显然,这种模式,士兵们的时间资源并没有浪费。这便是事件驱动的优势所在。
Reactor模型
网络模型演化过程中,将建立连接、IO等待/读写以及事件转发等操作分阶段处理,然后可以对不同阶段采用相应的优化策略来提高性能。
也正是如此,Reactor 模型在不同阶段都有相关的优化策略,常见的有以下三种方式呈现:
- 单线程模型
- 多线程模型
- 主从多线程模型
从某些方面来说,其实主要有单线程
和多线程
两种模型;其中,多线程模型就包含了多线程模型(Woker线程池)和主从多线程模型。多线程 Reactor 的演进分为两个方面:
- 升级 Handler。既要使用多线程,又要尽可能高效率,则可以考虑使用线程池。
- 升级 Reactor。可以考虑引入多个Selector(选择器),提升选择大量通道的能力。
1.单线程模型
模型图如下:
上图描述了 Reactor 的单线程模型结构,在 Reactor 单线程模型中,所有 I/O 操作(包括连接建立、数据读写、事件分发等)、业务处理,都是由一个线程完成的。单线程模型逻辑简单,缺陷也十分明显:
- 一个线程支持处理的连接数非常有限,CPU 很容易打满,性能方面有明显瓶颈;
- 当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时;
- 线程在处理 I/O 事件时,Select 无法同时处理连接建立、事件分发等操作;
- 如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用。
在单线程 Reactor 模式中,Reactor 和 Handler 都在同一条线程中执行。这样,带来了一个问题:当其中某个 Handler 阻塞时,会导致其他所有的 Handler 都得不到执行。
在这种场景下,被阻塞的 Handler 不仅仅负责输入和输出处理的传输处理器,还包括负责新连接监听的 Acceptor 处理器,可能导致服务器无响应。这是一个非常严重的缺陷,导致单线程反应器模型在生产场景中使用得比较少。
2.多线程模型
由于单线程模型有性能方面的瓶颈,多线程模型作为解决方案就应运而生了。
Reactor 多线程模型将业务逻辑交给多个线程进行处理。除此之外,多线程模型其他的操作与单线程模型是类似的,比如连接建立、IO事件读写以及事件分发等都是由一个线程来完成。
当客户端有数据发送至服务端时,Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理。
一般的请求中,耗时最长的一般是业务处理,所以用一个线程池(worker 线程池)来处理业务操作,在性能上的提升也是非常可观的。
当然,这种模型也有明显缺点,连接建立、IO 事件读取以及事件分发完全有单线程处理;比如当某个连接通过系统调用正在读取数据,此时相对于其他事件来说,完全是阻塞状态,新连接无法处理、其他连接的 IO、查询 IO 读写以及事件分发都无法完成。
对于像 Nginx、Netty 这种对高性能、高并发要求极高的网络框架,这种模式便显得有些吃力了。因为,无法及时处理新连接、就绪的 IO 事件以及事件转发等。
接下来,我们看看主从多线程模型是如何解决这个问题的。
3.主从多线程模型
主从 Reactor 模型要想解决这个问题,同样需要从我们前面介绍的几个阶段中的某一个或者多个进行优化处理。
既然是主从模式,那谁主谁从呢?哪个模块使用主从呢?
在多线程模型中,我们提到,其主要缺陷在于同一时间无法处理大量新连接、IO就绪事件;因此,将主从模式应用到这一块,就可以解决这个问题。
主从 Reactor 模式中,分为了主 Reactor 和 从 Reactor,分别处理 新建立的连接
、IO读写事件/事件分发
。
- 一来,主 Reactor 可以解决同一时间大量新连接,将其注册到从 Reactor 上进行IO事件监听处理
- 二来,IO事件监听相对新连接处理更加耗时,此处我们可以考虑使用线程池来处理。这样能充分利用多核 CPU 的特性,能使更多就绪的IO事件及时处理。
简言之,主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程都有独立的 Selector 对象。MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。
在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。