linux——IO多路复用

一、为什么要用 IO 多路复用?

因为:

单线程 + 阻塞 IO 只能处理一个客户端 多进程 / 多线程太耗资源

所以需要:

一个线程,同时监视很多个 fd,哪个有数据就处理哪个 → 这就是 IO 多路复用

二、IO 模型

Linux 中常用的 IO 模型,最实用的三个

1. 阻塞 IO(最常用)

意思:

read 等不到数据 → 一直等等数据到了 → 才返回

用过的阻塞 IO:

  • TCP 的 read ()
  • TCP 的 accept ()
  • UDP 的 recvfrom ()

优点:

简单、稳定、绝大部分程序用的就是它

缺点:

一个 fd 阻塞,整个进程卡住你不能同时处理多个客户端。

2. 非阻塞 IO(忙等,极浪费 CPU)

意思:

read 不会等 → 没数据就立即返回报错你得自己循环不停问:"有数据没?有数据没?"

缺点:

CPU 爆高、浪费、极不推荐

3. IO 多路复用(终极方案)

一个线程 / 一个进程,同时监视多个 fd, 只要有一个 fd 有数据,我就处理它。

最大优点:

CPU 不浪费、一个线程处理大量客户端 这是 Nginx、Redis、PHP-FPM 等软件的核心模型。

三、IO 多路复用是什么?

本质:

先把 fd 交给系统内核 → 让内核帮你监视哪些 fd 有数据

数据准备好了 → 内核再通知你 → 你再去 read/write

四、IO 多路复用(select

客户端代码

复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<strings.h>
#include<unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>

//  ./client 192.168.88.129   5001



#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.88.129"
#define BUFSIZE 1024
#define QUIT_STR "QUIT"
#define SERV_RESP_STR "SERVER:"

int main(int argc,char **argv)
{
	int fd = -1;
	if(argc != 3)
	{
		exit(1);
	}
	int port = -1;
	port = atoi(argv[2]);

	struct sockaddr_in sin;
	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd < 0)
	{
		perror("socket");
		exit(1);
	}

	bzero(&sin,sizeof(sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);
	sin.sin_addr.s_addr = inet_addr(argv[1]);

	if(connect(fd,(struct sockaddr*)&sin,sizeof(sin)) < 0)
	{
		perror("connect");
		exit(1);
	}

	fd_set rset;
	int maxfd = -1;
	struct timeval tout;
	char buf[BUFSIZE];
	int ret = -1;
	while(1)
	{
		FD_ZERO(&rset);
		FD_SET(0,&rset);
		FD_SET(fd,&rset);

		maxfd = fd;
		tout.tv_sec = 5;
		tout.tv_usec = 0;

		select(maxfd+1,&rset,NULL,NULL,&tout);

		if(FD_ISSET(0,&rset))  //stdin have data
		{
			bzero(buf,BUFSIZE);
			do
			{
				ret = read(0,buf,BUFSIZE-1);
			}while(ret < 0);

			if(ret < 0)
			{
				perror("read");
				continue;
			}
			if(!ret) continue; //no data

			if(write(fd,buf,strlen(buf))<0)
			{
				perror("write");
				continue;
			}

			if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
			{
				printf("client is exited!\n");
				break;
			}
		}
		if(FD_ISSET(fd,&rset))   //server have data  
		{
			bzero(buf,BUFSIZE);
			do
			{
				ret = read(fd,buf,BUFSIZE-1);
			}while(ret < 0);
			if(ret < 0)
			{
				perror("read");
				continue;
			}
			if(!ret) break;
			printf("server said:%s\n",buf);
			if((strlen(buf) > strlen(SERV_RESP_STR)) && !strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
			{
				printf("client is exited!\n");
				break;
			}
		}
	}

	return 0;
}

这个客户端用 select 同时监听 2 个 "数据源":

  1. 键盘输入(文件描述符 0)
  2. 服务器发来的消息(socket fd)谁有数据,就处理谁 → 不阻塞、不卡顿、双向聊天!

①定义 select 用到的变量

复制代码
fd_set rset;         // 文件描述符集合(select 用的"监视表")
int maxfd;           // 最大的文件描述符(select 必须要)
struct timeval tout; // 超时时间

②超级核心:while 循环里的 select 四步

复制代码
FD_ZERO(&rset);        // 第一步:清空集合
FD_SET(0, &rset);      // 把【键盘】加入监听
FD_SET(fd, &rset);     // 把【服务器】加入监听

maxfd = fd;            // 最大 fd 就是 socket fd
tout.tv_sec = 5;       // 最多等 5 秒

// 第二步:调用 select,开始监听
select(maxfd+1, &rset, NULL, NULL, &tout);
  • FD_ZERO:清空监视表
  • FD_SET(0, ...):我要监听键盘
  • FD_SET(fd, ...):我要监听服务器
  • select(...):内核帮我盯着这两个,谁有数据我就醒过来!

③select 醒来后:判断是谁发的数据

1.键盘有输入(你打字了)

复制代码
if(FD_ISSET(0, &rset))
{
    read(0, buf);      // 读键盘
    write(fd, buf);    // 发给服务器
}
  1. 服务器发消息来了

    if(FD_ISSET(fd, &rset))
    {
    read(fd, buf); // 读服务器
    printf("server said:%s\n", buf);
    }

最关键的 3 个 select 函数(你必须背)

函数 作用
FD_ZERO 清空集合
FD_SET 把一个 fd 加入监听
FD_ISSET 判断这个 fd 是否有数据

为什么客户端要用 select?(超级重要)

不用 select:

  • 你调用 read(键盘)
  • 程序会卡住不动
  • 服务器发消息你收不到

用 select:

  • 同时等键盘 + 服务器
  • 谁先来,处理谁
  • 不卡、不等、双向通信

select 工作流程

复制代码
1. 清空集合
2. 加入键盘(0) + 服务器(fd)
3. select 阻塞等待
4. 键盘有输入 → 处理键盘
5. 服务器发消息 → 处理服务器
6. 回到第一步,循环

服务器端代码

复制代码
#include<stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include<stdlib.h>
#include <strings.h>
#include <arpa/inet.h>
#include<string.h>
#include <pthread.h>
#include<signal.h>
#include <sys/wait.h>

#define QUIT_STR "QUIT"
#define BUFSIZE 1024
#define BACKLOG 5
#define SERV_IP 5001
#define SERV_IP_ADDR "192.168.88.129"
#define SERV_RESP_STR "SERVER:"

void child_data_handle(int signum)
{
	if(SIGCHLD == signum)
	{
		waitpid(-1,NULL,WNOHANG);
	}
}
void* client_data_handle(void* arg);
int main()
{
	int fd = -1;
	signal(SIGCHLD,child_data_handle);
	struct sockaddr_in sin;
	//1.socket
	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd<0)
	{
		perror("socket");
		exit(1);
	}

	bzero(&sin,sizeof(sin));

	sin.sin_family = AF_INET;
	sin.sin_port = htons(SERV_IP);
	//sin.sin_addr.s_addr = inet_addr(SERV_IP_ADDR);  
	sin.sin_addr.s_addr = INADDR_ANY;
	
	//2.bind
	if(bind(fd,(struct sockaddr *)&sin,sizeof(sin)) <0)
	{
		perror("bind");
		exit(0);
	}
	//3.listen
	if(listen(fd,BACKLOG) < 0)
	{
		perror("listen");
		exit(1);
	}

	//4.accept

	pid_t pid;
	int newfd = -1;
	struct sockaddr_in cin;
	socklen_t addrlen = sizeof(cin);
	while(1)
	{
		newfd = accept(fd,(struct sockaddr *)&cin,&addrlen);
		if(newfd < 0)
		{
			perror("accept");
			break;
		}
		pid = fork();
		if(pid < 0)
		{
			perror("fork");
			break;
		}
		if(pid == 0)
		{
			char ipv4_addr[16];
			if(!inet_ntop(AF_INET,(void *)&cin.sin_addr,ipv4_addr,sizeof(cin)))
			{
				perror("inet_ntop");
				exit(1);
			}
			printf("Client:(%s,%d) is connect\n",ipv4_addr,ntohs(cin.sin_port));
			
			client_data_handle(&newfd);
			close(fd);
		}
		if(pid > 0)
		{
			close(newfd);
		}

	}
	close(fd);

	return 0;
}

void* client_data_handle(void* arg)
{
	int newfd = *(int *)arg;

	char buf[BUFSIZE];
	int ret = -1;
	printf("client handle process:newfd = %d\n",newfd);
	char resp_buf[BUFSIZE];
	while(1)
	{
		do
		{
			bzero(buf,BUFSIZE);
			ret = read(newfd,buf,BUFSIZE-1);
		}while(ret < 1);
		if(ret < 0)
		{
			exit(1);
		}
		if(!ret)
		{
			break;
		}
		printf("receive data:%s\n",buf);
		
		bzero(resp_buf,BUFSIZE);
		strncpy(resp_buf,SERV_RESP_STR,strlen(SERV_RESP_STR));
		strcat(resp_buf,buf);
		do
		{
			ret = write(newfd,resp_buf,strlen(resp_buf));
		}while(ret < 0);

		if(!strncasecmp(buf,QUIT_STR,strlen(QUIT_STR)))
		{
			printf("Client is exiting!\n");
			break;
		}
	}

	close(newfd);
	return NULL;
}

服务器这段代码:负责接收消息 + 回复消息

客户端发什么,服务器就加前缀 SERVER: 发回给你!

客户端发送代码(select 部分)

复制代码
if(FD_ISSET(0,&rset))  // 你键盘输入
{
    read(0,buf);       // 读你输入的内容
    write(fd,buf);     // 发给服务器
}

服务器接收 + 处理

复制代码
void* client_data_handle(void* arg)
{
	int newfd = *(int *)arg;   // 客户端连接的 socket
	char buf[BUFSIZE];
	char resp_buf[BUFSIZE];

这里 newfd 就是和客户端通信的通道

客户端 write() → 服务器 newfd 收到

①服务器读取客户端发来的消息

复制代码
do
{
    bzero(buf,BUFSIZE);
    ret = read(newfd, buf, BUFSIZE-1);
} while(ret < 1);

对应客户端:

复制代码
write(fd, buf, ...);  // 客户端发
read(newfd, ...);    // 服务器收

作用:

阻塞等待客户端发消息 客户端一发,服务器立刻读到 buf

②服务器打印收到的消息

复制代码
printf("receive data:%s\n",buf);

③ 服务器拼接回复消息(重点!)

复制代码
bzero(resp_buf,BUFSIZE);
strncpy(resp_buf, SERV_RESP_STR, strlen(SERV_RESP_STR));
strcat(resp_buf, buf);

SERV_RESP_STR 就是:

复制代码
#define SERV_RESP_STR "SERVER:"

④服务器把拼接好的消息发回客户端

复制代码
do
{
    ret = write(newfd, resp_buf, strlen(resp_buf));
} while(ret < 0);

这一句直接对应客户端 select 的另一部分

复制代码
if(FD_ISSET(fd,&rset))   // 服务器有数据
{
    read(fd, buf, ...);  // 客户端接收服务器的回复
    printf("server said:%s\n",buf);
}
相关推荐
两点王爷2 小时前
Ubuntu 机器安装解压软件和ip工具
linux·运维·ubuntu
在深圳搬砖2 小时前
使用Qemu安装Ubuntu教程
linux·运维·ubuntu
北京耐用通信2 小时前
破局工业通讯壁垒!耐达讯自动化EtherCAT转RS232网关,老设备焕新核心桥梁
服务器·网络·人工智能·科技·物联网·网络协议·自动化
m0_694845572 小时前
VoxCPM部署教程:构建AI语音交互系统
服务器·人工智能·后端·自动化
ZHECSDN2 小时前
Ubuntu内存优化实战:告别卡死,让Linux内存管理更智能
linux·ubuntu
源图客2 小时前
Linux系统部署Postgres数据库(ubuntu22.04)
linux·运维·数据库
唐朝板栗丶TDC3 小时前
Windows下使用WSL2创建Ubuntu子系统(更改安装位置与启动图形桌面)
linux·windows·经验分享·ubuntu
Elnaij3 小时前
Linux系统与系统编程(4)——Linux软件包管理器、Vim与gcc
linux
齐齐大魔王3 小时前
linux-进程通信
linux·运维·服务器