一、TCP 并发服务器模型:多线程
多线程可以实现 TCP 并发:每来一个客户端,就创建一个子线程专门处理该客户端通信。
缺点
- 每个线程要独占独立栈内存,连接数量上限有限,大量并发客户端,系统有崩溃风险。
- 线程切换会带来 CPU 开销。
优化方案 :使用线程池,预先创建一批线程,复用线程,避免频繁创建销毁线程。
二、4 种 IO 模型
1. 阻塞 IO
没有数据的时候,CPU 把任务挂起,任务不占用 CPU,等待数据就绪后再返回。
优点:编码简单;缺点:一个线程只能处理一个 IO,无法同时处理多个连接。
2. 非阻塞 IO
没有数据时,函数不会阻塞,直接返回;用户代码要不停循环轮询,反复调用 IO 函数检查数据是否到来。
缺点:CPU 空转,消耗大量 CPU 资源。
3. 信号驱动 IO(异步 IO)
- 将文件描述符设置为异步 IO,向进程注册信号事件。
- 内核监测 fd,当 IO 事件发生,内核发送 SIGIO 信号通知用户进程。
- 用户程序写信号捕获函数,在回调里面读写数据。
缺点:信号处理框架不好控制,实际网络开发很少用。
4. IO 多路复用
核心:一个进程 / 线程,同时监听一堆文件描述符,哪个 fd 有事件就绪就处理哪个。
主流三种实现:select、poll、epoll。
三、select
工作原理
select 监听一组文件描述符集合;集合中只要至少一个 fd 产生事件,select 就解除阻塞。它会原地修改 fd 集合:保留发生事件的 fd,把没有事件的 fd 从集合清掉。
fd_set 集合操作宏
| 函数 | 作用 |
|---|---|
FD_ZERO(fd_set *set) |
清空集合,全部置 0 |
FD_SET(int fd, fd_set *set) |
把 fd 加入集合 |
FD_CLR(int fd, fd_set *set) |
把 fd 从集合移除 |
FD_ISSET(int fd, fd_set *set) |
判断 fd 是否还在集合中(是否发生事件) |
select 函数原型
c
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds:最大文件描述符值 + 1readfds:读事件集合;writefds:写事件集合;exceptfds:异常事件集合timeout:超时时间。传NULL代表永久阻塞等待
返回值
- > 0:就绪 fd 的总数量
- -1:出错
- 0:超时时间到,没有任何事件
select 缺点
- fd 集合有最大上限,默认 1024;
- 每次调用都会修改集合,循环调用前必须重新
FD_ZERO、FD_SET; - 返回后需要循环遍历全部 fd 判断
FD_ISSET; - 用户态和内核态之间,每次调用都要拷贝整个 fd 集合。
四、epoll(Linux 高性能 IO 多路复用)
epoll 把事件表放在内核,不需要每次调用都拷贝全部 fd,适合高并发海量连接。
1. epoll_create
c
int epoll_create(int size);
- 参数
size:早期版本提示事件表大小,现在已经被忽略,填大于 0 数字即可 - 返回值:成功返回 epoll 的文件描述符 epfd;失败返回 -1
作用:在内核创建 epoll 事件表。
2. epoll_ctl
向内核事件表增、删、改监听的 fd 和事件。
c
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll_create 返回的句柄op操作类型EPOLL_CTL_ADD:向事件表添加 fdEPOLL_CTL_MOD:修改已经注册 fd 的监听事件EPOLL_CTL_DEL:从事件表删除 fd
fd:要操作的文件描述符event:绑定这个 fd 的事件
c
struct epoll_event {
uint32_t events; // EPOLL_IN / EPOLL_OUT / EPOLLET
epoll_data_t data; // 用户自定义数据,一般存 fd
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常用事件:
EPOLLIN:读事件,有数据可读EPOLLOUT:写事件,可以发送数据EPOLLET:边沿触发模式,默认是水平触发
3. epoll_wait
阻塞等待事件就绪。
c
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
epfd:epoll 句柄events:输出参数,内核把发生事件存到这个数组maxevents:events 数组最大元素个数timeout:毫秒,-1 代表永久阻塞
返回值
- > 0:就绪事件数量,就是需要处理的连接个数
- 0:超时,没有事件
- -1:出错
epoll 两种触发模式
- 水平触发 LT(默认):只要缓冲区有数据,epoll_wait 就反复通知。
- 边沿触发 ET:只有状态发生变化那一刻通知一次;必须一次性把缓冲区数据全部读完,否则不会再次提醒。
epoll 优势
- 没有 1024 连接上限;
- 不需要每次传入全部 fd,内核维护事件表;
- epoll_wait 直接拿到就绪事件数组,不用遍历全部 fd;
- 海量空闲连接下性能远高于 select。
五、简单对比小结
| 模型 | 优点 | 缺点 |
|---|---|---|
| select | 跨平台,简单 | 上限 1024,每次拷贝集合,需要遍历所有 fd |
| epoll | 高性能,海量连接 | Linux 专属,接口略复杂 |