DAYS 38 TCP并发服务器模型详解

前言

在网络编程中,TCP 服务端如果只处理单客户端连接,实用性很差。实际项目需要支持多个客户端同时接入 ,这就引出 TCP 并发服务器。 本文基于 Linux C 网络编程,讲解多线程并发服务器模型、4 种 IO 模型,重点拆解selectepoll多路复用接口,包含原理、函数说明、优缺点对比,适合嵌入式 / Linux 后台开发同学学习。

阅读范围:TCP 并发服务器模型及其以下知识点

一、TCP 并发服务器模型之多线程

1. 核心思想

TCP 服务端accept()阻塞等待客户端连接,每接收到一个新客户端,就创建一个子线程专门负责和该客户端通信。主线程继续回去 accept 等待下一个客户端接入,以此实现多客户端并发访问服务器。

2. 优缺点

✅优点:

  • 逻辑简单,上手容易;
  • 线程间内存共享,数据交互方便。

❌缺点:

  1. 每个线程拥有独立栈空间(Linux 默认 8M),客户端并发数量很高时,创建大量线程会消耗巨大内存,系统有崩溃风险
  2. 线程创建、销毁、调度存在 CPU 开销;
  3. 线程数量不能无限增大。

💡优化方案:实际工程一般搭配线程池,提前创建一批线程复用,避免频繁创建销毁线程。

简单流程梳理

  1. socket 创建监听套接字;
  2. bind 绑定 IP 端口;
  3. listen 开启监听;
  4. 循环accept()获取新客户端 cfd;
  5. pthread_create 创建子线程,把 cfd 传给子线程,子线程内完成 recv/send 数据收发;
  6. 主线程继续 accept 等待下一个连接。

二、四种常见 IO 模型

IO 模型解决的核心问题:程序如何等待文件描述符上可读、可写事件

在网络编程中文件描述符就是 socket。

1. 阻塞 IO(默认)

  • 调用recv / accept等函数,没有数据到达时,线程直接挂起阻塞,不占用 CPU;
  • 有数据到来才返回,继续往下执行。
  • ✅优点:CPU 开销小,代码简单
  • ❌缺点:一个线程只能处理一个 fd,要处理多个 fd 必须开多线程 / 多进程。

2. 非阻塞 IO

把文件描述符设置为非阻塞。调用 IO 函数,如果没有就绪数据,不会阻塞,直接返回错误。 程序需要循环不断轮询,反复调用 recv/accept 查看是否有事件。

  • ✅优点:单个线程可以处理多个 fd;
  • ❌缺点:CPU 空转轮询,CPU 占用率很高,实际很少直接使用。

3. 信号驱动 IO

将文件描述符设置为异步 IO,内核监测 fd 事件;当 fd 可读可写,内核给用户程序发送SIGIO信号; 程序提前注册信号捕捉函数,在信号处理函数中做 IO 读写。

  • ✅不用轮询,数据就绪才通知;
  • ❌信号处理逻辑复杂,不适合大量文件描述符场景。

4. IO 多路复用(重点)

单个线程,同时监听大量文件描述符,当集合中任意一个 fd 发生可读 / 可写事件,函数返回,用户再去处理就绪 fd。

两大实现:select /poll/epoll,epoll 是 Linux 高性能 IO 多路复用实现。

三、select 多路复用

1. 核心 API

复制代码
void FD_CLR(int fd, fd_set *set);    // 从集合删除fd
int  FD_ISSET(int fd, fd_set *set);  // 判断fd是否还在集合(是否产生事件)
void FD_SET(int fd, fd_set *set);    // 将fd加入监听集合
void FD_ZERO(fd_set *set);           // 清空整个fd集合

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
参数解析
  1. nfds最大的被监听文件描述符 + 1
  2. readfds:监听读事件的 fd 集合(可读、新连接到来都属于读事件);
  3. writefds:监听写事件 fd 集合;
  4. exceptfds:异常事件集合;
  5. timeout超时设置:
    • NULL:永久阻塞,直到有事件发生;
    • 设置时间,超时后直接返回 0。
返回值
  • 0:产生事件的文件描述符总个数;

  • 0:时间超时,没有事件;
  • -1:出错。

2. select 工作机制

  1. 每次调用 select 前,都要重新构建 fd_set 监听集合;
  2. 把用户态 fd 集合拷贝到内核;
  3. 内核阻塞监听,当有事件就绪,内核会修改 fd_set 集合:把没有事件的 fd 从集合清除,只保留发生事件的 fd
  4. select 返回,回到用户态,循环FD_ISSET遍历所有 fd,判断哪个 fd 发生事件,执行业务处理。

3. select 缺点

  1. fd 集合有最大数量限制,默认 1024;
  2. 每次调用 select 都要把 fd 集合从用户空间拷贝内核空间;
  3. 返回之后不知道具体哪个 fd 就绪,必须循环遍历全部 fd
  4. 每次调用都要重新初始化 fd 集合。

四、epoll --- Linux 高性能 IO 多路复用

epoll 是 Linux 独有的,解决 select/poll 的性能痛点,高并发网络服务器(Nginx)底层就是 epoll。 epoll 分为 3 个核心函数:epoll_createepoll_ctlepoll_wait

1. epoll_create

复制代码
int epoll_create(int size);
  • 功能:创建内核 epoll 事件监听表,返回 epoll 的文件描述符 epfd;
  • 参数 size:旧版本提示内核预估事件数量,新版本已经忽略该参数;
  • 返回:成功返回 epfd,失败返回‑1。

2. epoll_ctl

复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

对内核事件表做增、删、改操作。

参数:

  1. epfd:epoll_create 返回的句柄;

  2. op 操作类型:

    • EPOLL_CTL_ADD:向内核事件表添加要监听的 fd;
    • EPOLL_CTL_MOD:修改已经注册 fd 的监听事件;
    • EPOLL_CTL_DEL:从内核事件表删除 fd;
  3. fd:要操作的目标 socket 文件描述符;

  4. event:告诉内核,我要监听这个 fd 的什么事件。

    struct epoll_event {
    uint32_t events; // 要监听的事件
    epoll_data_t data; // 用户数据,可以保存fd
    };

    // events常用宏
    EPOLLIN // fd可读事件
    EPOLLOUT // fd可写事件
    EPOLLET // 设置边沿触发ET模式,默认是水平触发LT

3. epoll_wait

复制代码
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);

阻塞等待事件发生,获取已经就绪的事件。

参数:

  1. epfd:epoll 句柄;
  2. events:数组,内核会把已经就绪的事件拷贝到这个数组,返回给用户
  3. maxevents:数组最多能存放多少个事件;
  4. timeout:超时时间,单位 ms,‑1 代表永久阻塞。

返回值:

  • 0:就绪事件数量;

  • 0:超时;
  • -1:错误。

4. LT 水平触发 VS ET 边沿触发

  1. LT 水平触发(默认) 只要 fd 缓冲区有数据,epoll_wait 就会反复通知用户。如果一次没有读完数据,下一次 epoll_wait 还会再次返回该事件。编程简单,但容易产生重复事件。
  2. ET 边沿触发 EPOLLET 只有状态发生变化瞬间才通知一次。缓冲区有剩余数据不会重复通知;

使用 ET 模式,读数据必须循环读,直到EAGAIN,把缓冲区全部读完,否则会造成数据残留。

5. epoll 为什么性能高(对比 select)

  1. 没有 1024 上限,支持上万 fd 并发;
  2. fd 集合只需要向内核添加一次(epoll_ctl),不需要每次调用都拷贝全部 fd;
  3. epoll_wait 返回直接拿到就绪事件数组,不需要遍历全部 fd,只处理返回过来的就绪事件;
  4. 内核采用回调机制,事件就绪直接把 fd 放到就绪链表。

五、简单对比总结

表格

IO 模型 优点 缺点 适用场景
阻塞 IO 代码简单,CPU 开销小 一个线程处理一个 fd 简单小并发程序
非阻塞 IO 单线程处理多个 fd CPU 轮询空转,占用高 极少单独使用
select 跨平台,兼容性好 上限 1024,每次遍历全部 fd 并发量不大、需要跨平台
epoll 海量并发性能好,不用遍历全部 fd Linux 平台专有 高并发服务器,Nginx,后端服务

补充:TCP 并发开发注意点

  1. 多线程版本注意线程安全,共享资源加互斥锁;
  2. IO 多路复用要注意文件描述符泄漏,连接关闭要 close;
  3. ET 边沿触发模式,读写要循环直到返回 EAGAIN;
  4. epoll 不要忘记处理对端关闭连接事件。

结尾

以上内容整理自 Linux C 网络编程学习笔记。IO 多路复用是 Linux 后台、嵌入式网络开发核心知识点。select 适合简单跨平台,生产环境高并发优先 epoll。

相关推荐
Quanqiucard1 小时前
监控球机联网总掉线?问题可能出在物联网卡上!
网络·物联网
zyf1044161 小时前
暑期实践日志 Day46:复盘第六章,规整对应实践成果
学习·计算机网络·剪辑·暑期实践·课题任务
spencer_tseng1 小时前
kylin redis-6.2.14.tar.gz
linux·redis·kylin·edis-6.2.14
三8441 小时前
应急响应之服务、文件痕迹、网络、日志排查 + 工具清单
网络·应急响应·日志排查·文件痕迹排查·应急响应报告模板·服务排查
wdfk_prog1 小时前
canopennode-rtt推荐,不只可以做从站,也可以承担主站角色
c语言·开发语言·数据库·学习·算法·深度优先
晏宁科技YaningAI1 小时前
企业通信系统技术规划方法:从业务需求到可演进架构
网络·性能优化·架构·gateway·paas
启观川1 小时前
Python基础-第 16 章 综合案例:客户信息管理系统
开发语言·笔记·python·正则表达式
昌原的儿子LEO1 小时前
Linux 网络编程:select 与 epoll IO 多路复用详解
linux·网络·数据库
啊阿狸不会拉杆1 小时前
《计算机网络-自顶向下方法》3.5 面向连接的运输:TCP 读书笔记
网络·tcp/ip·计算机网络