当前位置: 首页 > news >正文

网络编程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 就绪通知”,未解决 “数据拷贝异步”“全异步”:从 “IO 就绪” 到 “数据拷贝完成” 全程异步
内核与应用的职责划分内核仅通知 “就绪”,数据拷贝需应用程序主动做内核包办 “就绪检测 + 数据拷贝”,应用程序仅用结果
工业界定位早期异步 IO 的过渡方案,已被淘汰现代高并发 / 高性能 IO 的标准方案

        简单来说:信号驱动 IO 是 “让内核喊你‘饭好了’,但你得自己去盛饭”;现代异步 IO 是 “内核把‘饭盛好端到你面前’,你直接吃就行”—— 后者才是真正意义上 “无感知等待、无主动操作” 的异步 IO

5、IO多路复用模型

1)概念

        一个线程监控多个文件描述符(fd),当其中任意一个就绪时通知程序进行处理

        n个客户端-->>用一个线程或进程服务器去答复

        优点:避免创建大量线程/进程,节省资源,适合高并发场景(如 Web 服务器)

        常见函数:select()、poll()、epoll()

2)函数介绍

① select

头文件:        #include <sys/select.h>

函数原型:

                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; //设置这个等于最初的表
  • 一般在循环内进行系统调用
  • 具体内容如下
  • 最前面建立tcp网络连接的基本步骤

  • 利用select函数实现步骤

  • 加上定时定次功能

优点
  • 内核负责轮询,减少用户态频繁切换

  • 支持跨平台(Windows/Linux 均可用

缺点
  • 最大监听数受限:FD_SETSIZE 默认 1024(Linux)

  • 每次调用需重置 fd_set:内核会修改集合,必须每次重新 FD_SET

  • 用户态与内核态拷贝开销大

  • 返回后仍需遍历所有 fd 才能知道哪个就绪

  • 效率随 fd 数量增长下降明显

知识点
  • stdin        --->0
  • stdout      --->1
  • error        --->2

② poll

头文件:        #include <poll.h>

函数原型:  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

  • 无需重置集合:eventsrevents 分离

  • 更清晰的事件机制

  • 效率更高:仅遍历传入的数组,不遍历整个 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对端关闭连接(TCP 半关闭)
EPOLLERR错误(自动监听)
EPOLLHUP挂起(自动监听)
EPOLLET边沿触发模式(Edge Triggered)
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)        //出错处理{perror("epoll fail");return -1;}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 <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <poll.h>
#include <sys/epoll.h>#include <unistd.h>
#include <fcntl.h>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){perror("epoll fail");return -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)函数对比

特性selectpollepoll
平台兼容性高(POSIX)仅 Linux
最大连接数~1024无限制(但性能差)无限制
时间复杂度O(n)O(n)O(1)
用户/内核拷贝每次全量拷贝每次全量拷贝共享内存
是否修改输入参数是(需备份)否(revents 分离)
触发模式仅 LT仅 LTLT + ET
遍历开销高(需遍历所有 fd)中(遍历数组)低(只处理就绪)
适用场景小规模连接、跨平台中小规模连接大规模高并发(如 Nginx)

4)应用建议

场景推荐方案
小型工具程序(<100 连接)select(简单、跨平台)
中等规模服务(几百连接)pollselect
高并发服务器(数千以上)epoll(Linux)
需跨平台(如 Windows)selectlibevent/libuv 封装

5)总结

整体使用思路:

        1.准备监控表

        2.添加监控的文件描述符

        3.调用函数监控事件发生

http://www.dtcms.com/a/358451.html

相关文章:

  • MQTT 连接建立与断开流程详解(二)
  • 项目管理在企业中的作用
  • 小迪Web自用笔记7
  • Redission 实现延迟队列
  • 鸿蒙NEXT布局全解析:从线性到瀑布流,构建自适应UI界面
  • Notepad++常用设置
  • 金仓数据库迁移评估系统(KDMS)V4正式上线,助力企业高效完成数据库国产化替代
  • 果蔬采摘机器人:自动驾驶融合视觉识别,精准定位,高效作业
  • 【SoC】【W800】基于W800的PWM实现
  • 类和反射的机制
  • hashmap计算key的hash的时候为什么要右移16位
  • 鸿蒙ArkTS 核心篇-16-循环渲染(组件)
  • Ruoyi-vue-plus-5.x第一篇Sa-Token权限认证体系深度解析:1.3 权限控制与注解使用
  • 【计算机组成原理】LRU计数器问题
  • Vue3 + GeoScene 地图点击事件系统设计
  • Selenium + PO 框架进阶实践:接入 Allure 报告与 Jenkins 持续集成
  • macOs上ffmpeg带入libx264库交叉编译
  • docker 启动一个clickhouse , docker 创建ck数据库
  • Python远程文件管理移动端适配与跨平台优化实战
  • vue3多个el-checkbox勾选框设置必选一个
  • 【OpenGL ES】光栅化插值原理和射线拾取原理
  • Day17(前端:JavaScript基础阶段)
  • Cocos游戏中自定义按钮组件(BtnEventComponent)的详细分析与实现
  • HAProxy 负载均衡全解析:从基础部署、负载策略到会话保持及性能优化指南
  • Spring : 事务管理
  • 音视频学习(六十一):H265中的VPS
  • Prompt Engineering:高效构建智能文本生成的策略与实践
  • 深层语义在自然语言处理中的理论框架与技术融合研究
  • AI大模型:(二)5.2 文生视频(Text-to-Video)模型训练实践
  • FPGA增量式方差与均值计算