阻塞IO与非阻塞IO
阻塞IO

非阻塞IO

IO多路复用

select函数
cs
//头文件:
#include <sys/socket.h>
#include <sys/types.h>
//函数原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);

poll函数
cs
//头文件:
#include <sys/socket.h> #include <sys/types.h>
//函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数详解
| 参数名 | 类型 | 作用说明 |
|---|---|---|
fds |
struct pollfd* |
指向 pollfd 结构体数组的指针,存储待监控的 FD 及关注的事件(输入 / 输出结合)。 |
nfds |
nfds_t |
需监控的 pollfd 结构体数量(即数组有效长度),nfds_t 是无符号整数类型(通常为 unsigned int)。 |
timeout |
int |
超时时间(单位:毫秒),控制 poll() 的阻塞行为: - timeout > 0:阻塞 timeout 毫秒后返回(超时返回 0); - timeout = 0:不阻塞,立即返回(轮询模式); - timeout = -1:无限阻塞,直到至少一个 FD 发生事件或被信号中断。 |
返回值:成功返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0,失败返回-1(并重置错误码)
pollfd 结构体
cs
struct pollfd {
int fd; // 待监控的文件描述符(FD)
short events; // 用户关注的事件类型(输入参数,由用户设置)
short revents; // 内核返回的实际发生的事件(输出参数,由内核填充)
};
fd:待监控的文件描述符
- 作用 :指定需要
poll()监控的文件描述符(如 socket、管道、标准输入等)。 - 特殊值 :
- 若
fd = -1:poll()会忽略该结构体 (不监控任何事件),且对应的events和revents会被忽略。 - 用途:动态管理监控列表时(如移除某个 FD),无需修改数组长度,只需将
fd设为-1即可。
- 若
2. events:用户关注的事件(输入参数)
用户通过设置 events 的二进制位,指定需要监控的事件类型。常见事件常量定义在 <poll.h> 中,可通过按位或(|) 组合多个事件。
| 事件常量 | 取值(十六进制) | 含义说明 |
|---|---|---|
POLLIN |
0x001 |
有普通数据 / 优先数据可读(如 socket 收到客户端数据、标准输入有输入)。 |
POLLPRI |
0x002 |
有紧急数据可读(如 socket 的带外数据 OOB)。 |
POLLOUT |
0x004 |
有普通数据 / 优先数据可写(如 socket 可发送数据,无阻塞)。 |
POLLERR |
0x008 |
对应 FD 发生错误 (无需用户设置,内核自动返回至 revents)。 |
POLLHUP |
0x010 |
对应 FD 发生挂起(如 peer 关闭连接,管道写端关闭)(内核自动返回)。 |
POLLNVAL |
0x020 |
对应 fd 无效(如 FD 未打开、已关闭)(内核自动返回)。 |
POLLRDNORM |
0x040 |
有普通数据 可读(与 POLLIN 功能重叠,部分系统兼容用)。 |
POLLWRNORM |
0x080 |
有普通数据 可写(与 POLLOUT 功能重叠,部分系统兼容用)。 |
POLLRDBAND |
0x100 |
有优先带数据 可读(如 TCP 带外数据,与 POLLPRI 关联)。 |
POLLWRBAND |
0x200 |
有优先带数据可写。 |
3. revents:内核返回的实际事件(输出参数)
- 作用 :
poll()调用成功后,内核会根据 FD 的实际状态,在revents中填充发生的事件(二进制位)。 - 关键特性 :
- 只读性 :用户无需设置
revents,仅需在poll()返回后读取其值。 - 自动包含错误事件 :即使用户未在
events中设置POLLERR、POLLHUP、POLLNVAL,内核若检测到这些事件,也会自动将其写入revents。 - 与
events的关联 :revents中的事件是events中用户关注事件的子集或超集(超集源于错误事件的自动添加)。
- 只读性 :用户无需设置

多线程并发服务器
服务器模型

多线程并发服务器原理

服务端
cs
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8888
#define SERVER_IP "192.168.136.130"
#define BUFFER_SIZE 128
//自定义结构体 -> 封装客户端连接信息,用于在线程间传递
typedef struct MSG{
int acceptfd;//客户端套接字描述符
struct sockaddr_in clientaddr;//客户端地址信息
}msg_t;
//创建线程处理函数
void *deal_read_write(void *arg){
msg_t msg = *(msg_t*)arg;
char buf[BUFFER_SIZE] = {0};
int nbytes = 0;
printf("客户端【%s:%d】连接到了服务器\n",inet_ntoa(msg.clientaddr.sin_addr),ntohs(msg.clientaddr.sin_port));
//客户端数据接收与发送
while(1){
nbytes = recv(msg.acceptfd, buf, BUFFER_SIZE, 0);
if(nbytes == -1){
perror("读取客户端消息失败:");
}else if(nbytes == 0){
printf("客户端【%s:%d】断开连接\n",inet_ntoa(msg.clientaddr.sin_addr),ntohs(msg.clientaddr.sin_port));
break;
}
printf("客户端【%s:%d】发来数据:%s\n",inet_ntoa(msg.clientaddr.sin_addr),ntohs(msg.clientaddr.sin_port),buf);
//回显数据给客户端
strcat(buf, "--服务器");
if(send(msg.acceptfd, buf, BUFFER_SIZE, 0) == -1){
perror("回显数据失败:");
}
}
close(msg.acceptfd);
pthread_exit(NULL);
}
int main(){
//创建套接字-配置服务器地址结构体
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
perror("创建套接字失败:");
return -1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
socklen_t serveraddr_len = sizeof(server_addr);
//绑定、监听、处理客户端的连接
if(bind(sockfd, (struct sockaddr *)&server_addr, serveraddr_len) == -1){
perror("绑定失败:");
close(sockfd);
return -1;
}
if(listen(sockfd, 5) == -1){
perror("监听失败:");
close(sockfd);
return -1;
}
struct sockaddr_in client_addr;
socklen_t clientaddr_len = sizeof(client_addr);
int acceptfd = 0;
pthread_t pthread_id = 0;
msg_t msg;
int ret = 0;
//主线程:接受连接并创建子线程
while(1){
if((acceptfd = accept(sockfd, (struct sockaddr *)&client_addr, &clientaddr_len)) == -1){
perror("接受连接失败:");
return -1;
}
msg.acceptfd = acceptfd;
msg.clientaddr = client_addr;
//创建子线程
ret = pthread_create(&pthread_id, NULL, deal_read_write, &msg);
if(ret == -1){
printf("错误码:%d,错误信息:%s\n",ret,strerror(ret));
close(sockfd);
return -1;
}
ret = pthread_detach(pthread_id);
if(ret == -1){
printf("错误码:%d,错误信息:%s\n",ret,strerror(ret));
close(sockfd);
return -1;
}
}
close(sockfd);
return 0;
}
多进程并发服务器

cs
#include <stdio.h> // 标准输入输出库(打印日志、错误信息)
#include <sys/types.h> // 系统数据类型定义(pid_t、socket相关类型等)
#include <sys/socket.h> // socket核心库(创建、绑定、监听、收发等接口)
#include <netinet/ip.h> // IPv4协议相关定义(补充sockaddr_in结构体支持)
#include <netinet/in.h> // 网络地址结构体定义(sockaddr_in、字节序转换函数)
#include <string.h> // 字符串处理库(memset、strcat等)
#include <stdlib.h> // 标准库(exit函数等)
#include <arpa/inet.h> // IP地址转换库(inet_ntoa:网络字节序IP转字符串)
#include <unistd.h> // 系统调用库(close、fork、kill、getpid等)
#include <signal.h> // 信号处理库(signal、sig_func信号回调)
#include <sys/wait.h> // 进程等待库(wait函数:回收子进程资源)
// 服务器监听端口号(自定义,需确保未被占用,范围1024-65535)
#define PORT 8888
// 服务器绑定的IP地址(根据实际网卡配置修改,需与客户端在同一网段)
#define SERVER_IP "192.168.26.128"
// 数据收发缓冲区大小(单次最大传输128字节,避免缓冲区溢出)
#define BUFFER_SIZE 128
/**
* @brief 信号处理函数:专门回收子进程资源,避免僵尸进程
* @param signum 接收到的信号编号(此处固定为SIGUSR1)
* 核心逻辑:wait(NULL) 会阻塞等待任意子进程终止,回收其PCB资源
* 避免子进程退出后成为僵尸进程(占用系统进程表资源)
*/
void sig_func(int signum){
// wait(NULL):不关心子进程退出状态,仅回收资源;若没有子进程可回收,会立即返回
wait(NULL);
}
int main(){
// 服务器监听套接字描述符(用于接受客户端连接请求,不直接收发数据)
int sockfd = 0;
// 客户端连接套接字描述符(每个客户端对应一个,用于与该客户端收发数据)
int acceptfd = 0;
// 进程ID(fork返回值:父进程返回子进程PID,子进程返回0,失败返回-1)
pid_t pid = 0;
// 函数返回值临时变量(用于判断系统调用是否成功)
int ret = 0;
/**
* 1. 创建TCP套接字(监听用)
* 参数说明:
* AF_INET:使用IPv4协议族
* SOCK_STREAM:流式套接字(对应TCP协议,可靠、面向连接)
* 0:默认协议(由系统自动匹配TCP协议,无需手动指定)
* 返回值:成功返回非负套接字描述符,失败返回-1
*/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
perror("套接字创建失败:"); // 打印系统错误原因(如权限不足、资源耗尽)
return -1; // 套接字创建失败,直接退出程序
}
// 服务器地址结构体:存储服务器的IP、端口、协议族等信息
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体(避免随机垃圾值影响)
server_addr.sin_family = AF_INET; // 协议族:IPv4
server_addr.sin_port = htons(PORT); // 端口号:htons将主机字节序转为网络字节序(大端)
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址:inet_addr将字符串IP转为网络字节序
socklen_t serveraddr_len = sizeof(server_addr); // 地址结构体长度(传给bind函数)
/**
* 2. 绑定套接字:将监听套接字与服务器IP+端口绑定(确定服务地址)
* 若绑定失败(如IP错误、端口已被占用),需关闭套接字避免资源泄漏
*/
if(bind(sockfd, (struct sockaddr*)&server_addr, serveraddr_len) == -1){
perror("绑定失败:");
close(sockfd); // 释放已创建的套接字资源
return -1; // 绑定失败,退出程序
}
/**
* 3. 监听连接:将套接字转为监听状态,等待客户端发起连接
* 参数说明:
* 第二个参数5:监听队列大小(最大同时等待连接的客户端数,超过则拒绝)
*/
if(listen(sockfd, 5) == -1){
perror("监听失败:");
close(sockfd); // 释放套接字资源
return -1; // 监听失败,退出程序
}
// 客户端地址结构体:存储连接成功的客户端IP、端口信息
struct sockaddr_in client_addr;
socklen_t clientaddr_len = sizeof(client_addr); // 客户端地址结构体长度
/**
* 4. 注册信号处理函数:绑定SIGUSR1信号与sig_func回调
* 作用:子进程退出前发送SIGUSR1信号,触发sig_func回收子进程资源
*/
signal(SIGUSR1, sig_func);
printf("服务器启动成功!监听IP:%s,端口:%d\n", SERVER_IP, PORT);
/**
* 主循环:持续接受客户端连接(服务器核心逻辑)
* 循环特性:accept是阻塞函数,无客户端连接时会一直阻塞等待
*/
while(1){
/**
* 5. 接受客户端连接:
* 参数说明:
* sockfd:监听套接字
* &client_addr:输出参数,存储连接客户端的IP和端口
* &clientaddr_len:输入输出参数,传入结构体长度,返回实际使用长度
* 返回值:成功返回客户端套接字描述符(acceptfd),失败返回-1
*/
acceptfd = accept(sockfd, (struct sockaddr*)&client_addr, &clientaddr_len);
if(acceptfd == -1){
perror("接受连接失败:");
close(sockfd); // 接受连接失败,关闭监听套接字
break; // 退出主循环,程序终止
}
/**
* 6. 创建子进程:用fork创建子进程处理当前客户端通信
* 原因:父进程继续监听新连接,子进程专注于单个客户端的数据收发(并发处理)
*/
pid = fork();
if(pid == -1){ // fork失败(如系统进程数达到上限)
perror("创建子进程失败:");
close(acceptfd); // 关闭当前客户端套接字(避免资源泄漏)
break; // 退出主循环,程序终止
}else if(pid == 0){ // 子进程逻辑(pid=0表示当前是子进程)
// 子进程专属:与客户端进行数据收发
char buf[BUFFER_SIZE] = {0}; // 数据收发缓冲区(初始化清空,避免脏数据)
int nbytes = 0; // 实际接收的字节数
// 打印客户端连接信息:inet_ntoa转网络字节序IP为字符串,ntohs转网络字节序端口为主机字节序
printf("客户端【%s:%d】连接到服务器\n",
inet_ntoa(client_addr.sin_addr), // 客户端IP
ntohs(client_addr.sin_port)); // 客户端端口
/**
* 7. 接收客户端数据:
* 参数说明:
* acceptfd:客户端套接字
* buf:接收缓冲区
* BUFFER_SIZE:缓冲区最大长度
* 0:默认阻塞模式(无数据时子进程阻塞等待)
* 返回值:nbytes>0表示实际接收字节数;=0表示客户端断开;=-1表示接收失败
*/
nbytes = recv(acceptfd, buf, BUFFER_SIZE, 0);
if(nbytes == -1){ // 接收数据失败(如网络异常)
perror("接收失败:");
}else if(nbytes == 0){ // 客户端主动断开连接(调用close)
printf("客户端【%s:%d】断开连接\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
break; // 退出子进程内部逻辑,准备关闭资源
}
// 打印客户端发送的数据(nbytes确保只打印实际接收的内容)
printf("客户端【%s:%d】发来数据:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buf);
// 拼接回显标记:在客户端数据后添加"--来自服务端",告知客户端数据已处理
strcat(buf, "--来自服务端");
/**
* 8. 回显数据给客户端:将处理后的数据发送回客户端
*/
if(send(acceptfd, buf, BUFFER_SIZE, 0) == -1){
perror("回显失败:");
}
close(acceptfd); // 子进程通信完成,关闭客户端套接字(释放资源)
kill(getpid(), SIGUSR1); // 向自身发送SIGUSR1信号,触发父进程回收资源
exit(0); // 子进程正常退出(必须调用,否则子进程会继续执行主循环)
}else if(pid > 0){ // 父进程逻辑(pid>0表示当前是父进程,pid为子进程ID)
// 父进程无需与客户端通信,关闭客户端套接字(避免文件描述符泄漏)
// 注:子进程会复制acceptfd,父进程关闭不影响子进程使用
close(acceptfd);
}
}
close(sockfd); // 程序退出前,关闭服务器监听套接字(释放资源)
return 0;
}
IO多路复用并发服务器

服务端
cs
#include <stdio.h> // 标准输入输出库(打印日志、错误信息)
#include <stdlib.h> // 标准库(无直接使用,保留兼容)
#include <sys/types.h> // 系统数据类型定义(socket、fd相关类型)
#include <string.h> // 字符串处理库(memset清空缓冲区/结构体)
#include <sys/socket.h> // socket核心库(创建、绑定、监听、收发、select等接口)
#include <arpa/inet.h> // IP地址转换库(inet_ntoa:网络字节序IP转字符串)
#include <netinet/ip.h> // IPv4协议补充定义(支持sockaddr_in结构体)
#include <netinet/in.h> // 网络地址结构体(sockaddr_in)+ 字节序转换函数(htons/ntohs)
#include <unistd.h> // 系统调用库(close关闭套接字)
#include <sys/time.h> // select机制依赖(fd_set、select函数声明)
// 服务器监听端口号(1024-65535区间,需确保未被占用)
#define PORT 8888
// 服务器绑定IP(根据实际网卡配置修改,与客户端需同一网段;0.0.0.0可监听所有网卡)
#define SERVER_IP "192.168.26.128"
// 数据收发缓冲区大小(单次最大传输128字节,避免缓冲区溢出)
#define BUFFER_SIZE 128
int main(){
// 监听套接字描述符:专门用于接受客户端连接请求,不直接收发数据
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if(listen_fd == -1){ // socket创建失败(如权限不足、系统资源耗尽)
perror("创建套接字失败:"); // 打印系统级错误原因
return -1; // 初始化失败,直接退出程序
}
// 服务器地址结构体:存储服务器的IP、端口、协议族等核心信息
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体(避免随机垃圾值干扰)
server_addr.sin_family = AF_INET; // 协议族:IPv4
server_addr.sin_port = htons(PORT); // 端口号:htons将主机字节序转为网络字节序(大端)
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址:inet_addr将字符串IP转为网络字节序
socklen_t serveraddr_len = sizeof(server_addr); // 地址结构体长度(传给bind函数的必需参数)
// 绑定套接字:将监听套接字与服务器IP+端口绑定(确定服务的网络地址)
int ret = bind(listen_fd, (struct sockaddr*)&server_addr, serveraddr_len);
if(ret == -1){ // 绑定失败(如IP错误、端口已被占用、无绑定权限)
perror("绑定失败:");
close(listen_fd); // 释放已创建的监听套接字资源(避免泄漏)
return -1; // 绑定失败,退出程序
}
// 监听连接:将套接字转为"监听状态",等待客户端发起TCP连接请求
// 第二个参数10:监听队列大小(最大同时等待连接的客户端数,超过则拒绝新连接)
if(listen(listen_fd, 10) == -1){
perror("监听失败:");
close(listen_fd); // 释放套接字资源
return -1; // 监听失败,退出程序
}
/**
* select机制核心:文件描述符集合(fd_set)管理
* 为什么需要两个集合?
* - readfd_save_set:保存所有需要监控的fd(监听fd + 所有客户端连接fd),避免每次select后丢失
* - readfd_modify_set:每次select调用的临时集合(select会修改该集合,只保留有事件的fd)
*/
fd_set readfd_save_set, readfd_modify_set;
FD_ZERO(&readfd_save_set); // 初始化集合:清空所有fd位(默认所有fd都未被监控)
FD_SET(listen_fd, &readfd_save_set); // 将监听套接字加入监控集合(监控"新连接请求"事件)
int max_fd = listen_fd; // 记录当前最大的文件描述符(select的第一个参数必需,需动态更新)
char buf[BUFFER_SIZE] = {0}; // 数据收发缓冲区(初始化清空,避免脏数据残留)
printf("开启小型的select并发服务器\n");
printf("服务器监听成功!IP:%s,端口:%d\n", SERVER_IP, PORT);
// 主循环:持续监控并处理事件(select并发的核心循环)
while(1){
// 关键:每次select前,将临时集合重置为保存集合(因为select会修改临时集合)
readfd_modify_set = readfd_save_set;
/**
* select函数:多路复用核心接口,监控多个fd的"读事件"
* 参数说明:
* 1. max_fd + 1:监控的fd范围(fd从0开始,需包含最大fd,故+1)
* 2. &readfd_modify_set:监控"读事件"的fd集合(有数据可读/新连接请求)
* 3. NULL:不监控"写事件"(此处仅需收发,无需主动监控写就绪)
* 4. NULL:不监控"异常事件"(简化逻辑,默认忽略)
* 5. NULL:阻塞模式(无任何事件时,select会一直阻塞,不占用CPU资源)
* 返回值:>0表示有事件的fd数量;-1表示失败;0表示超时(此处无超时)
*/
int fds = select(max_fd + 1, &readfd_modify_set, NULL, NULL, NULL);
if(fds == -1){ // select调用失败(如被信号中断)
perror("select失败:");
continue; // 不退出循环,继续监控(容错处理)
}
// 客户端地址结构体:存储"新连接客户端"的IP和端口信息(仅在accept时使用)
struct sockaddr_in client_addr;
socklen_t clientaddr_len = sizeof(client_addr);
/**
* 遍历所有可能的fd,判断哪个fd有事件发生
* 为什么从3开始?0=标准输入、1=标准输出、2=标准错误,均不监控
* 遍历范围:3 ~ max_fd(覆盖所有已加入监控的fd)
*/
int event_fd = 3;
for(; event_fd < max_fd + 1; event_fd++){
// FD_ISSET:判断当前fd是否在"有事件的集合"中(核心事件判断宏)
if(FD_ISSET(event_fd, &readfd_modify_set)){
// 情况1:监听套接字有事件 → 有新客户端连接请求
if(event_fd == listen_fd){
// 接受新连接:生成"客户端连接套接字"(acceptfd),专门与该客户端收发数据
int acceptfd = accept(event_fd, (struct sockaddr*)&client_addr, &clientaddr_len);
if(acceptfd == -1){ // 接受连接失败(如系统资源不足)
perror("accept失败:");
continue; // 忽略该失败连接,继续监控其他fd
}
// 将新客户端的连接fd加入"保存集合"(后续监控其读事件:是否发数据)
FD_SET(acceptfd, &readfd_save_set);
// 更新max_fd:确保select能监控到最新的fd(必须维护,否则新fd无法被监控)
max_fd = (max_fd > acceptfd) ? max_fd : acceptfd;
// 打印客户端连接信息(inet_ntoa转IP,ntohs转端口字节序)
printf("客户端【%s:%d】连接服务器\n",
inet_ntoa(client_addr.sin_addr), // 客户端IP(网络字节序→字符串)
ntohs(client_addr.sin_port)); // 客户端端口(网络字节序→主机字节序)
}
// 情况2:客户端连接fd有事件 → 客户端发送数据/断开连接
else{
memset(buf, 0, sizeof(buf)); // 清空缓冲区(避免上次数据残留)
// 接收客户端数据:从客户端连接fd中读取数据
int nbytes = recv(event_fd, buf, BUFFER_SIZE, 0);
if(nbytes == -1){ // 接收数据失败(如网络异常、客户端强制断开)
perror("获取客户端信息失败:");
continue; // 忽略该错误,继续监控其他客户端
}
// nbytes == 0:客户端主动调用close断开连接(TCP四次挥手完成)
else if(nbytes == 0){
printf("客户端【%s:%d】断开连接\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
FD_CLR(event_fd, &readfd_save_set); // 从监控集合中移除该fd(不再监控)
close(event_fd); // 关闭连接套接字(释放资源,避免泄漏)
continue; // 跳过后续逻辑,处理下一个fd
}
// 正常接收:打印客户端发送的数据
printf("客户端【%s:%d】发来数据:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buf);
// 数据回显:将接收的数据原封不动发送回客户端(验证通信正常)
nbytes = send(event_fd, buf, BUFFER_SIZE, 0);
if(nbytes == -1){ // 回显失败(如客户端已断开)
perror("回显失败:");
continue;
}
}
}
}
}
// 理论上不会执行到这里(主循环是死循环),仅作资源释放冗余处理
close(listen_fd);
return 0;
}