网络编程4-并发服务器、阻塞与非阻塞IO、信号驱动模型、IO多路复用..
一、并发服务器
1、单循环服务器(顺序处理)
一次只能处理一个客户端连接,只有当前客户端断开连接后,才能接受新的客户端连接
2、多进程/多线程并发服务器
while(1) {
connfd = accept(listenfd);
pid = fork(); // 或 pthread_create()
if (pid == 0) {
// 子进程/线程处理通信
recv(connfd, ...);
send(connfd, ...);
close(connfd);
exit(0); // 或 pthread_exit
}
close(connfd); // 父进程关闭已交给子进程的 connfd
}
优点:
-
实现真正并发
-
客户端可长时间通信
缺点:
-
创建/销毁进程或线程开销大
-
资源占用高(内存、CPU)
-
存在僵尸进程问题(需
waitpid()
回收)
二、IO 模型分类(5种)
1、阻塞IO模型
- 常见阻塞IO模型:
- i--读 scanf、getchar、fgets、read、recv
- o--写 管道:读端存在,写管道 写操作阻塞>>>>内存不足,写不进去便阻塞了
- 优点:简单、方便、要等 效率不高
2、 非阻塞IO模型
1)以读为例:
- 特点:需要不停去看,资源开销大
2)实现方法
方法一:
open()
时指定int fd = open(\"fifo\", O_RDONLY | O_NONBLOCK);
方法二:运行时用
fcntl()
修改int flag = fcntl(fd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
- 注意事项
- 适用于
read/write
等系统调用。 对recv()
可使用MSG_DONTWAIT
标志实现非阻塞
3)示例
方法一
方法二
3、信号驱动IO模型
1)使用 SIGIO
信号通知数据到达,异步但支持有限
- 有数据发个信号,然后系统调用
- 通知粒度粗:仅能告知 “有 IO 事件”,无法区分事件类型与细节
2)利用函数:fcntl 实现
3)实现步骤
// 1. 设置文件描述符支持异步通知
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);// 2. 设置信号接收者(当前进程)
fcntl(fd, F_SETOWN, getpid());// 3. 注册信号处理函数
signal(SIGIO, sig_handler);
- 不足之处
-
支持的文件类型有限(如 socket、tty)
-
不适合大量连接场景
-
实际应用较少
-
4)示例
4、异步 IO 模型
信号驱动IO和一步IO区别
简单来说:信号驱动 IO 是 “让内核喊你‘饭好了’,但你得自己去盛饭”;现代异步 IO 是 “内核把‘饭盛好端到你面前’,你直接吃就行”—— 后者才是真正意义上 “无感知等待、无主动操作” 的异步 IO
5、IO多路复用模型
1)概念
用一个线程监控多个文件描述符(fd),当其中任意一个就绪时通知程序进行处理
n个客户端-->>用一个线程或进程服务器去答复
优点:避免创建大量线程/进程,节省资源,适合高并发场景(如 Web 服务器)
常见函数:select()、
poll()、
epoll()
2)函数介绍
① select
头文件: #include
函数原型:
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);功能: 实现IO多路复用
参数:
nfds //是关心的文件描述符中最大的那个文件描述符 + 1
readfds //代表 要关心 的 读操作的文件描述符的集合
writefds //代表 要关心 的 写操作的文件描述符的集合 >>> 与read类似
exceptfds //代表 要关心 的 异常的文件描述符的集合 >>> 与read类似(error--2)
timeout //超时 设置一个超时时间
//NULL 表示select是一个阻塞调用
{0,0}
:非阻塞效果
{sec, usec}
:指定超时时间最小单位写到ms
返回值:
成功:就绪的 fd 数量(>0)
超时:返回 0
失败:返回 -1
辅助宏函数:
FD_ZERO(fd_set *set); // 清空集合
FD_SET(int fd, fd_set *set); // 添加 fd 到集合
FD_CLR(int fd, fd_set *set); // 从集合中移除 fd
FD_ISSET(int fd, fd_set *set); // 判断 fd 是否在集合中
基本实现流程
文字版过程
- 建立一张表 >>>监控 目前只关心读
- fd_set readfds;一张表
- FD_ZERO(&readfds);清空表(初始化)
- 将要监控的文件描述符 添加到表中
- FD_SET(0,&readfds);//stdin
- FD_SET(fd,&readfds);//建的管道或者文件描述符
- 准备参数
- maxfds 是关心的文件描述符中最大的那个文件描述符 + 1
- int maxfds = fd + 1;
- 每次系统调用只会留下就绪的文件描述符(每次监控都会重新遍历一遍)
- fd_set backfds; //设置这个等于最初的表
- maxfds 是关心的文件描述符中最大的那个文件描述符 + 1
- 一般在循环内进行系统调用
- 具体内容如下
- 最前面建立tcp网络连接的基本步骤
- 利用select函数实现步骤
- 加上定时定次功能
优点
-
内核负责轮询,减少用户态频繁切换
-
支持跨平台(Windows/Linux 均可用
缺点
-
最大监听数受限:
FD_SETSIZE
默认 1024(Linux) -
每次调用需重置 fd_set:内核会修改集合,必须每次重新
FD_SET
-
用户态与内核态拷贝开销大
-
返回后仍需遍历所有 fd 才能知道哪个就绪
-
效率随 fd 数量增长下降明显
知识点
- stdin --->0
- stdout --->1
- error --->2
② poll
头文件: #include
函数原型: int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能: 实现IO多路复用
参数:
struct pollfd *fds :struct pollfd {
int fd; // 文件描述符
short events; // 关注的事件(输入)
short revents; // 实际发生的事件(输出)
};nfds_t nfds:表示要监控的文件描述符的数量
timeout :时间值
返回值:
成功 表示 就绪的数量 ;0 超时情况下表示 没有就绪实际
失败 -1
事件标志:
POLLIN:数据可读(等价于
select
的读)
基本实现流程
优点
-
无 1024 限制:只要系统允许打开足够多 fd
-
无需重置集合:
events
和revents
分离 -
更清晰的事件机制
-
效率更高:仅遍历传入的数组,不遍历整个 fd 范围
缺点
-
每次调用仍需将整个
fds[]
拷贝到内核 -
返回后仍需遍历全部元素查找就绪 fd
-
时间复杂度仍是 O(n),连接数多时性能下降
③ epoll
只要缓冲区有数据就持续触发
结果展现
epoll_create
函数原型: int epoll_create(int size);
功能: 创建 epoll 实例
参数:
size
:提示内核初始分配空间大小(现已忽略)返回值: 成功 epoll 文件描述符(用于后续操作)
失败 -1
注意事项: 使用完需
close(epfd)
epoll_ctl()
函数原型: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能: 控制监听列表
参数: epfd:epoll 句柄(
epoll_create
返回op :操作类型
EPOLL_CTL_ADD
EPOLL_CTL_DEL
EPOLL_CTL_MOD
fd:要监听的目标文件描述符
event:事件结构体
struct epoll_event
返回值: 成功 epoll 文件描述符(用于后续操作)
失败 -1
struct epoll_event
epoll_event
结构体:
struct epoll_event {
uint32_t events; // 监听的事件类型
epoll_data_t data; // 用户数据(共用体)
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
常见事件类型
EPOLLIN
EPOLLOUT
EPOLLRDHUP
EPOLLERR
EPOLLHUP
EPOLLET
EPOLLONESHOT
epoll_wait()
函数原型: int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout);功能: 等待事件发生
参数: epfd:epoll 句柄(
epoll_create
返回
events
:用户提供的数组,用于接收就绪事件
maxevents
:最大接收事件数(通常 10~100
timeout
:超时(单位 ms )
-1
:永久阻塞
0
:非阻塞
>0
:等待指定毫秒
返回值: 成功 就绪事件数量(无需遍历所有 fd)
失败 -1
tcp 实现epoll并发服务器
封装添加和删除函数
完整内容
#include \"head.h\"int add_fd(int listenfd,int epfd) //将文件描述符添加到 epoll 监控列表{ struct epoll_event ev; //定义结构体 ev.events = EPOLLIN; //表示监控可读事件(文件描述符有数据可读时触发) ev.data.fd = listenfd; if (epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev))//epoll_ctl添加、修改、删除监控的文件描述符 { perror(\"epoll_ctl add fail\"); return -1; } return 0;}int del_fd(int fd,int epfd){ if (epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)) { perror(\"epoll_ctl del fail\"); return -1; } return 0;}int main(void){ int serfd = socket(AF_INET,SOCK_STREAM,0); if (serfd < 0) { perror(\"fail to socke\"); return -1; } struct sockaddr_in seraddr; seraddr.sin_family = AF_INET; seraddr.sin_port = htons(50000); seraddr.sin_addr.s_addr = inet_addr(\"127.0.0.1\"); if (bind(serfd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0) { perror(\"bind fial\"); return -1; } if (listen(serfd,5) < 0) { perror(\"listen fail\"); return -1; } struct sockaddr_in cliaddr; bzero(&cliaddr,0); socklen_t len = sizeof(cliaddr); /************正片开始******************/ char buf[1024] = {0}; int epfd = epoll_create(1); //创建,返回一个用于操作的文件描述符efd //括号内要求大于0的值就行 add_fd(serfd,epfd); //添加serfd到epoll监控 int t = 3000; struct epoll_event ret_ev[1024]; //可以容纳的个数 while (1) { int ret = epoll_wait(epfd,ret_ev,10,t); //等待事件发生,超时时间为3000ms//ret_ev数组用于存储发生的事件 //就序最多处理 10 个事件 printf(\"ret = %d\\n\",ret); if (ret 0) //遍历每个事件 { int i = 0; for (i = 0; i < ret; i++) { if (ret_ev[i].data.fd == serfd) { int connfd = accept(serfd,(struct sockaddr *)&cliaddr,&len); if (connfd < 0) { perror(\"accept fail\"); return -1; } printf(\"----client connect---\\n\"); printf(\"client ip:%s\\n\",inet_ntoa(cliaddr.sin_addr)); printf(\"port :%d\\n\",ntohs(cliaddr.sin_port)); //添加到表里 add_fd(connfd,epfd); } else //如果不是serfd我们就要开始收数据了 { recv(ret_ev[i].data.fd,buf,sizeof(buf),0); printf(\"buf = %s\\n\",buf); if (0 == strncmp(buf,\"quit\",4)) { del_fd(ret_ev[i].data.fd,epfd); //从epfd内删除 close(ret_ev[i].data.fd); } } } } } close(serfd); return 0;}
仅在状态变化时触发一次(必须配合非阻塞 IO)
两种情况:
正常数据:实际只能触发一次,但数据还在,利用循环可以打印出来,但是读完数据就没有了()---n <0
quit退出:实际只能触发一次,但数据还在,利用循环可以打印出来,读完数据就没有了,---n= 0
主要改变
注意事项:
fd 必须设置为 非阻塞
必须一次性读完所有数据(直到 read()
返回 EAGAIN
)
否则会丢失后续事件
结果展现
具体水平触发的代码区别(改动的地方)
完整代码
#include \"head.h\"#include #include #include #include #include #include #include int add_fd(int fd, int epfd){struct epoll_event ev;ev.events = EPOLLIN | EPOLLET;// EPOLLET(ET)边缘触发//$$$--改1--$$$ev.data.fd = fd; //标准输入 if ( epoll_ctl(epfd,EPOLL_CTL_ADD, fd, &ev)){perror(\"epoll_ctl add fail\");return -1;}return 0;} int del_fd(int fd, int epfd) //删除 {//struct epoll_event ev;//ev.events = EPOLLIN;//ev.data.fd = fd; //标准输入 if ( epoll_ctl(epfd,EPOLL_CTL_DEL, fd, NULL)){perror(\"epoll_ctl add fail\");return -1;}return 0;}//$$$$$$$$$$--改2--$$$$$$$$$$$$$$$void set_nonblock(int fd){int flags = fcntl(fd,F_GETFL);flags = flags | O_NONBLOCK;fcntl(fd,F_SETFL,flags); return;}int main(int argc, char const *argv[]){//step1 socket int fd = socket(AF_INET,SOCK_STREAM,0);if (fd < 0){perror(\"socket fail\");return -1;}struct sockaddr_in seraddr;bzero(&seraddr,sizeof(seraddr));seraddr.sin_family = AF_INET;seraddr.sin_port = htons(50000);seraddr.sin_addr.s_addr = inet_addr(\"127.0.0.1\");//step2 bind if (bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0){perror(\"connect fail\");return -1;}//step3 listenif (listen(fd,5) < 0){perror(\"listen fail\");return -1;}struct sockaddr_in cliaddr;bzero(&cliaddr,0);socklen_t len = sizeof(cliaddr); //1.准备表 int epfd = epoll_create(1);if (epfd < 0){perror(\"epoll_create fail\");return -1;} //2.添加 fd add_fd(fd,epfd);char buf[1024] = {0}; struct epoll_event ret_ev[10];while (1){ int ret =epoll_wait(epfd,ret_ev,10,-1); if (ret 0) { int i = 0; for (i = 0; i < ret; ++i) { if (ret_ev[i].data.fd == fd) //listenfd { int connfd = accept(fd,(struct sockaddr *)&cliaddr,&len); if (connfd < 0) { perror(\"accept fail\"); return -1; } printf(\"---client connect---\\n\"); printf(\"client ip:%s\\n\",inet_ntoa(cliaddr.sin_addr)); printf(\"port: %d\\n\",ntohs(cliaddr.sin_port)); //设置非阻塞 set_nonblock(connfd); //添加到表中 add_fd(connfd,epfd); } else //$$$$$$$$$$--改3--$$$$$$$$$$$$$$$ { while(1) { int n = recv(ret_ev[i].data.fd,buf,1,0); printf(\"n = %d buf = %s\\n\",n,buf); if (n < 0 && errno != EAGAIN) //正常数据 { perror(\"recv \"); del_fd(ret_ev[i].data.fd,epfd); close(ret_ev[i].data.fd); } if (n== 0 || strncmp(buf,\"quit\",4) == 0) //退出 { del_fd(ret_ev[i].data.fd,epfd); close(ret_ev[i].data.fd); }sleep(1); } } }} } return 0;}
3)函数对比
revents
分离)4)应用建议
select
(简单、跨平台)poll
或 select
epoll
(Linux)select
或 libevent
/libuv
封装5)总结
整体使用思路:
1.准备监控表
2.添加监控的文件描述符
3.调用函数监控事件发生