1、TCP并发服务器
1.1TCP并发服务器问题
服务端需要同时处理多个客户端连接时,核心矛盾:
accept():阻塞等待新客户端的三次握手,若无新连接则一直阻塞;recv()/send():阻塞等待对应客户端的读写数据,若无数据则一直阻塞。单线程下,阻塞 IO 会导致服务器只能处理一个客户端,无法响应其他客户端的连接 / 数据请求,程序逻辑无法满足并发需求。
解决方法:
- TCP并发服务器线程/进程模型:
- 优点:实现简单(每个新连接创建一个线程 / 进程处理),逻辑直观;
- 缺点:资源消耗大,连接数过多时会耗尽系统资源,且线程 / 进程切换开销大;
- TCP并发服务器多路复用模型(select/poll/epoll)
- 优点:单线程 / 少量线程即可处理大量连接,资源消耗低,切换开销小;
- 缺点:编程逻辑稍复杂,select/poll 存在性能瓶颈(epoll 无此问题)。
1.2 Linux系统的4种IO模型
阻塞IO:
数据没来时,进程/线程阻塞等待,不占用CPU资源
- 优点:CPU利用率高,编程简单
- 缺点:一个线程只能处理一个fd
非阻塞IO:
系统调用立即返回,用户程序需要不断轮询检查数据
- 优点:一个线程可管理多个fd
- 缺点:大量占用CPU资源(空轮询)
多路复用IO:
用一个函数(select/poll/epoll)监听多个fd是否产生IO事件,
只要有fd就绪就返回,用户再处理对应fd
- 优点:一个线程可管理大量连接,CPU利用率高
- 缺点:编程逻辑复杂
信号驱动IO:
当fd数据就绪时,内核主动发SIGIO信号通知应用,
应用在信号处理函数中读取数据
异步IO:
应用发起调用IO接口操作后立即返回,内核完成全部IO操作
(包括数据拷贝到用户缓冲区)后才通知应用
| IO模型 | 特点 | CPU占用 | 并发能力 | 编程复杂度 |
|---|---|---|---|---|
| 阻塞IO | 数据未到阻塞 | 低 | 差 | 简单 |
| 非阻塞IO | 轮询检查 | 高 | 中 | 中等 |
| 多路复用 | 统一监听 | 低 | 高 | 复杂 |
| 信号驱动 | 信号通知就绪 | 低 | 高 | 较复杂 |
| 异步IO | 内核完成所有 | 低 | 高 | 最复杂 |
- 信号驱动 I/O:内核只负责"通知你可以做了"
- 异步 I/O:内核直接把 I/O 操作做完,完成后再通知你
1.3 函数接口
1.3.1 fcntl(设置文件描述符属性)(文件描述符控制)
- 函数原型:
cpp
int fcntl(int fd, int cmd, ... /* arg */ );
- 功能:对已打开的文件描述符
fd执行指定的控制操作(如修改属性、设置所有权、获取状态等) - 参数:
- fd:要操作的文件描述符
- cmd:操作命令(核心常用值如下)
- F_GETFL:获取文件描述符的状态标志(如 O_NONBLOCK、O_ASYNC)
- F_SETFL:设置文件描述符的状态标志(仅能修改 O_APPEND、O_NONBLOCK、O_ASYNC 等)
- F_SETOWN:设置接收 SIGIO/SIGURG 信号的进程 ID / 进程组 ID
- F_GETOWN:获取接收 SIGIO/SIGURG 信号的进程 ID / 进程组 ID
- arg:可选参数,根据 cmd 决定(如 F_SETFL 时传新的标志位,F_SETOWN 时传进程 ID)
- 返回值:
- 执行 F_GETFL:成功返回文件描述符的状态标志值(整数),失败返回 - 1
- 执行 F_SETFL/F_SETOWN:成功返回 0,失败返回 - 1
- 其他 cmd(如 F_DUPFD):成功返回新的文件描述符,失败返回 - 1
- 通用规则:失败时均会设置
errno
- eg:
cpp
// 某些系统需要定义 _GNU_SOURCE 才能看到 F_SETOWN
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
// 示例:将fd设置为信号驱动IO
void sigio_handler(int signo) {
printf("收到IO事件通知\n");
}
void set_async_io(int fd) {
int flags;
// 1. 获取当前fd的属性
flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL error");
return;
}
// 2. 设置异步IO属性(O_ASYNC)
flags |= O_ASYNC;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl F_SETFL error");
return;
}
// 3. 设置当前进程接收该fd的SIGIO信号
if (fcntl(fd, F_SETOWN, getpid()) == -1) {
perror("fcntl F_SETOWN error");
return;
}
}
// F_SETOWN 的作用:设置哪个进程(或进程组)接收该fd的SIGIO信号
fcntl(fd, F_SETOWN, getpid()); // 设置当前进程接收信号
关键注意:
O_ASYNC仅对 "支持异步的文件描述符" 有效(如套接字、终端),普通文件不支持;- 异步 IO 的核心是 "内核主动通知",但
O_ASYNC实现的是信号驱动 IO(非真正的异步 IO)。
cpp
//常用示例:
// 1. 获取 fd 的状态标志
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL");
return -1;
}
// 2. 设置 fd 为非阻塞IO
flags |= O_NONBLOCK;
if (fcntl (fd, F_SETFL, flags) == -1) {
perror ("fcntl F_SETFL");
return -1;
}
// 3. 设置 fd 的 SIGIO 信号归属当前进程
if (fcntl (fd, F_SETOWN, getpid ()) == -1) {
perror ("fcntl F_SETOWN");
return -1;
}
| 注意事项 |
1. `F_SETFL`仅能修改**可修改的标志位**
(如O_NONBLOCK、O_ASYNC),无法修改O_RDONLY、O_WRONLY等打开时的基础标志;
2. `O_ASYNC`(异步IO标志)仅对套接字、终端等"面向字符"的文件描述符有效,普通文件不支持。
1.3.2 select 函数(多路复用IO)
- 函数原型:
cpp
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
- 功能:监听多个文件描述符的读、写、异常事件,直到有事件发生或超时
- 参数:
- nfds:需要监听的最大文件描述符 + 1(select 会从 0 遍历到 nfds-1,因此必须传最大值 + 1)
- readfds:读文件描述符(事件)集合(监听是否有数据可读,如 recv、accept),传 NULL 表示不监听
- writefds:写事件集合(监听是否可写,如 send),传 NULL 表示不监听
- exceptfds:异常事件集合(如套接字带外数据:可以理解为"紧急数据"或"加急数据",它在正常数据流之外传输,即使接收缓冲区满了也能被立即处理。),传 NULL 表示不监听
- timeout:超时时间,结构体定义如下:
cpp
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
- 返回值:
- 成功:返回就绪(产生事件)的文件描述符总数(读+写+异常)
- 失败:返回-1,设置errno(如被信号中断时errno=EINTR)
- 超时:返回0(无任何事件就绪
- 注意事项 :
- select返回后,未就绪的文件描述符会被从集合中清除,因此每次调用前需重新初始化集合;
-
- 监听的文件描述符数量受限于FD_SETSIZE(默认1024),无法突破;
- 需遍历所有监听的fd才能找到就绪的fd,连接数越多效率越低。
1.3.3 fd_set相关辅助宏(操作文件描述符集合)
cpp
void FD_CLR(int fd, fd_set *set);
**功能:**将文件描述符fd从集合set中移除
**注意:**fd需是有效的、已加入集合的描述符
cpp
int FD_ISSET(int fd, fd_set *set);
**功能:**判断fd是否在就绪的集合set中
**返回值:**在集合中返回非0值,不在返回0
**核心用途:**select返回后,遍历判断哪个fd就绪
cpp
void FD_SET(int fd, fd_set *set);
**功能:**将文件描述符fd加入集合set
**注意:**fd不能超过FD_SETSIZE-1,否则行为未定义
cpp
void FD_ZERO(fd_set *set);
功能: 将集合set的所有位清0(初始化集合);必须在使用集合前调用,否则集合内数据随机
cpp
//常用示例:
fd_set read_fds;
// 1. 初始化集合
FD_ZERO(&read_fds);
// 2. 将监听fd加入集合
FD_SET(listen_fd, &read_fds);
// 3. 调用select
select(listen_fd+1, &read_fds, NULL, NULL, NULL);
// 4. 判断fd是否就绪
if (FD_ISSET(listen_fd, &read_fds)) {
// 监听fd有事件(新连接)
}
// 5. 移除fd
FD_CLR(listen_fd, &read_fds);
| 集合状态 | timeout=NULL | timeout=0 | timeout>0 |
|---|---|---|---|
| 空集合 | 进程会一直阻塞,直到手动发送信号(如 Ctrl+C)中断,否则不会返回。 | 立即返回0 | 等待超时后返回0 |
| 有fd但无事件 | 阻塞直到有事件就绪/被信号中断 | 立即返回0 | 等待至超时(返回 0)或有 fd 就绪(返回就绪数) |