网络编程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 listen
	if (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.调用函数监控事件发生

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值