基于TCP的多路复用

news/2024/5/18 15:29:27 标签: 服务器, c语言, 网络, udp

1. 知识点

目前支持I/O多路复用的系统调用有select,pselect,poll,epoll。与多进程和多线程技术相

比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进

程/线程,从而大大减小了系统的开销。

I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般

是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都

是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是

阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用

户空间

2. select 函数

0 表示标准输入   STDIN_FILENO

1 表示标准输出     STDOUT_FILENO

2 表示标准错误输出 STDERR_FILENO

2.1 select存在三个问题

[1] 每次调用select,都需要把被监控的fds集合从用户态空间拷贝到内核态空间,高并发场景

下这样的拷贝会使得消耗的资源是很大的。

[2] 能监听端口的数量有限,单个进程所能打开的最大连接数由FD_SETSIZE宏定义,监听上

限就等于fds_bits位数组中所有元素的二进制位总数,32位机默认1024个,64位默2048。

[3] 被监控的fds集合中,只要有一个有数据可读,整个socket集合就会被遍历一次,用户线程并不知道哪些 fds 收到数据只能挨个遍历每个socket来收集可读事件了。

2.2 函数接口

1)用户进程需要监控某些资源 fds,在调用 select 函数后会阻塞,操作系统会将用户线程加入这些资源的等待队列中。

2)直到有描述符就绪(有数据可读、可写或有 except)或超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。

3)select 函数返回后,中断程序唤起用户线程。用户可以遍历 fds,通过 FD_ISSET 判断具体哪个 fd 收到数据,并做出相应处理。

select 函数优点明显,实现起来简单有效,且几乎所有操作系统都有对应的实现。

2.2.1 接口

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval  *timeout);

2.2.2 参数:

int nfds:管理的最⼤的⽂件描述符+1

fd_set *readfds:⽂件描述符表(集合),监视管理⽂件描述符的读操作是否就绪

fd_set *writefds:⽂件描述符表(集合),监视管理⽂件描述符的写操作是否就绪,没有就 写NULL

fd_set *exceptfds:⽂件描述符表(集合),监视管理⽂件描述符的异常

struct timeval *timeout:timeout:超时设置。

Null:一直阻塞直到有文件描述符就绪或出错

时间值为0:仅仅检测文件描述符集的状态,然后立即返回

时间值不为0:在指定时间内,如果没有事件发生,则超时返回。

超时设置过后,如果select超时了,那么返回值是0, 并且超时时间的结构体会变成 0s

2.2.3 返回值:

成功:返回监视到就绪的⽂件描述符的个数,会把监视的表修改为只剩下就绪的⽂件描述符

失败:返回-1

2.2.4 操作⽂件描述符表:

void FD_CLR(int fd, fd_set *set);//把⽂件描述符fd从set表删除

int   FD_ISSET(int fd, fd_set *set);//判断fd是否在set集合中,返回值描述符存在集合里返回真1,不存在返回假0

void FD_SET(int fd, fd_set *set);//把fd 加⼊到set表,将 fd_set 结构中对应的位设置为1,表示便通过 select 函数对文件描述符 fd进行监视。

void FD_ZERO(fd_set *set);//清空表,将其所有位都设置为0。

2.3 多路复用实现通信

服务端select_serve.c

#include <stdio.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/time.h>
//基于TCP 的IO多路复用通信
//服务端
int main()
{

	//创建套接字
	int sock_fd = socket(PF_INET,SOCK_STREAM,0);
	
	//初始化本机地址和端口
	struct sockaddr_in srvaddr;
	srvaddr.sin_family = PF_INET;
	srvaddr.sin_port = htons(10000);
	srvaddr.sin_addr.s_addr = inet_addr("192.168.124.151");
	socklen_t srvaddr_len = sizeof(srvaddr);

	//绑定套接字
	bind(sock_fd,(struct sockaddr *)&srvaddr,srvaddr_len);
	//监听
	listen(sock_fd,4);
	
	//等待连接
	printf("等待连接中......\n");
	int conn_fd = accept(sock_fd,NULL,NULL);
	printf("连接成功!!\n");
	
	//定义一个集合
	fd_set jihe;

	while(1)
	{

		FD_ZERO(&jihe);//清空集合,将其所有位都设置为0。
	
		FD_SET(conn_fd,&jihe);//把conn_fd套接字(套接字说白了也是文件描述符)添加进集合

		//标准输入文件STDIN_FILENO-------0
		FD_SET(STDIN_FILENO,&jihe);//把标准输入添加进集合
		
		//多路复用的系统调用,这里监控读操作,
		//一旦有操作select就会被select监控到,接着配合后面的FD_ISSET()判断出具体时集合中的哪个文件描述符
		int ret = select(conn_fd+1,&jihe,NULL,NULL,NULL);
		if(-1 == ret)//监控失败
		{
			perror("select failed");
			continue;
		}
		if(0 == ret)//超时
		{
			continue;
		}
		
		//判断文件描述符是否在集合中 如果已连接套接字在集合,则进行读操作,读取客户端发来的信息
		if(FD_ISSET(conn_fd,&jihe) == 1)
		{
			char rbuf[128]={0};
			read(conn_fd,rbuf,sizeof(rbuf));
			printf("from cli:%s\n",rbuf);
			
			//FD_CLR(conn_fd,&jihe);
		}
		
		//如果我们有标准输入在集合中,则写入,即通过已连接套接字发送给客户端
		if(FD_ISSET(STDIN_FILENO,&jihe) == 1)//判断出是STDIN_FILENO标准输入
		{
			char wbuf[128]={0};//定义缓冲区
			fgets(wbuf,sizeof(wbuf),stdin);//键盘输入
			write(conn_fd,wbuf,sizeof(wbuf));
			
			//FD_CLR(STDIN_FILENO,&jihe);
		}
		
	}
	
}



客户端select_cilent.c

#include <stdio.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/time.h>
//基于TCP 的IO多路复用通信
//客户端
int main()
{
	//创建套接字
	int sock_fd = socket(PF_INET,SOCK_STREAM,0);
	
	//初始化服务端网络地址
	struct sockaddr_in srvaddr;
	srvaddr.sin_family = PF_INET;
	srvaddr.sin_port = htons(10000);
	srvaddr.sin_addr.s_addr = inet_addr("192.168.124.151");
	
	socklen_t srvaddr_len = sizeof(srvaddr);
	
	//这个绑定可有可无
	// bind(sock_fd,(struct sockaddr *)&srvaddr,srvaddr_len);
	
	//listen(sock_fd,4);
	
	//请求连接
	connect(sock_fd,(struct sockaddr *)&srvaddr,srvaddr_len);
	
	//定义一个集合
	fd_set jihe;

	
	while(1)
	{
		FD_ZERO(&jihe);//清空集合
	
		FD_SET(sock_fd,&jihe);//把sock_fd套接字(套接字说白了也是文件描述符)添加进集合
		//标准输入文件STDIN_FILENO-------0
		FD_SET(STDIN_FILENO,&jihe);//把标准输入添加进集合
		
		//多路复用的系统调用,这里监控读操作
		int ret = select(sock_fd+1,&jihe,NULL,NULL,NULL);
		if(-1 == ret)
		{
			perror("select failed");
			continue;
		}
		if(0 == ret)
		{
			continue;
		}
		
		//判断文件描述符是否在集合中 如果已连接套接字在集合,则进行读操作,读取客户端发来的信息
		if(FD_ISSET(sock_fd,&jihe) == 1)
		{
			char rbuf[128]={0};
			read(sock_fd,rbuf,sizeof(rbuf));
			printf("from srv:%s\n",rbuf);
			
			//FD_CLR(sock_fd,&jihe);
		}
		//如果我们有标准输入文件描述符在集合中,则写入,即通过已连接套接字发送给服务端
		if(FD_ISSET(STDIN_FILENO,&jihe) == 1)
		{
			char wbuf[128]={0};
			fgets(wbuf,sizeof(wbuf),stdin);
			write(sock_fd,wbuf,sizeof(wbuf));
			
			//FD_CLR(STDIN_FILENO,&jihe);
		}
		
	}
	
}



3. poll

3.1 优点

poll 函数与 select 原理相似,都需要来回拷贝全部监听的文件描述符,不同的是:

1)poll 函数采用链表的方式替代原来 select 中 fd_set 结构,因此可监听文件描述符数量不受限。

2)poll 函数返回后,可以通过 pollfd 结构中的内容进行处理就绪文件描述符,相比 select 效率要高。

3)新增水平触发:也就是通知程序 fd 就绪后,这次没有被处理,那么下次 poll 的时候会再次通知同个 fd 已经就绪。

3.2 缺点

和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。poll和select同样

存在一个性能缺点就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,以及个别描述符就绪触发整体描述符集合的遍历的低效问题。而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

4. epoll

epoll 使用一个文件描述符管理多个描述符,将用户进程监控的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间只需拷贝一次。

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

4.1 优点

1)没有最大并发连接的限制,能打开的 FD 的上限远大于 1024。

2)效率提升,不是轮询的方式,不会随着 FD 数目的增加效率下降。

3)内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递,即 epoll 使用 mmap 减少复制开销。

4)新增 ET 模式。

5. 总结

5.1 select、poll、epoll区别

三种函数在的 Linux 内核里有都能够支持,其中 epoll 是 Linux 所特有,而 select 则应该是 POSIX 所规定,一般操作系统均有实现。

5.2 工作模式

1)LT模式

LT(level triggered)模式:也是默认模式,即当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,并且下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。

2)ET模式

ET(edge-triggered)模式:当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

ET 是一种高速工作方式,很大程度上减少了 epoll 事件被重复触发的次数。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。


http://www.niftyadmin.cn/n/5253676.html

相关文章

(JAVA)OpenCV的安装与使用

本安装教程属于CLI安装&#xff0c;Windows用户可以使用Power Shell 官网教程点击查看 安装OpenCV # git克隆项目 git clone git://github.com/opencv/opencv.git cd opencv # 切换对应的版本分支 git checkout 4.x mkdir build && cd build # 生成Makefile cmake -D…

特征驱动开发

FDD 方法来自于一个大型的新加坡银行项目。FDD 的创立者 Jeff De Luca 和 Peter Coad 分别是这个项目的项目经理和首席架构设计师。在 Jeff 和 Peter 接手项目时&#xff0c;客户已经经历了一次项目的失败&#xff0c;从用户到高层都对这个项目持怀疑的态度&#xff0c;项目组士…

uniapp iOS离线打包——运行项目到模拟器报错?

运行项目、打包时报错问题 记录个人在开发过程中遇到的相关问题&#xff0c;后续有时间会不定时更新 文章目录 运行项目、打包时报错问题运行到模拟器报错解决方案 打包报错解决方案 运行到模拟器报错 解决方案 选中项目工程 —> Build Settings 滑动底部 —> User-Defi…

【数据结构】堆的应用(小根堆)

知识概览 堆用来维护一个数据集合。堆是一个二叉树&#xff0c;可以说是二叉树的一个应用&#xff0c;堆还是一个完全二叉树。 小根堆&#xff1a;每个点都满足它小于等于左右两边的点。 一维数组用来存下来一棵树。在堆中&#xff0c;x的左儿子是2x&#xff0c;右儿子是2x …

RedisHelper

Redis面试题&#xff1a; 1、什么是事务&#xff1f;2、Redis中有事务吗&#xff1f;3、Redis中的事务可以回滚吗&#xff1f; 答&#xff1a; 1、事务是指一个完整的动作&#xff0c;要么全部执行&#xff0c;要么什么也没有做 2、Redis中有事务&#xff0c;Redis 事务不是严…

git 常用的使用方法

1.查看分支 $ git branch #查看本地分支 $ git branch -r #查看远程分支 $ git branch -a #查看所有分支 $ git branch -vv #查看本地分支及追踪的分支 2.创建分支 方法1 $ git branch 分支名 #创建本地分支 #将本地分支push&#xff0c;就创建了远程分支方法2 #创建本地分…

【数学建模】《实战数学建模:例题与讲解》第十讲-时间序列预测(含Matlab代码)

【数学建模】《实战数学建模&#xff1a;例题与讲解》第十讲-时间序列预测&#xff08;含Matlab代码&#xff09; 基本概念移动平均&#xff08;Moving Average, MA&#xff09;:指数平滑法&#xff08;Exponential Smoothing&#xff09;:季节性调整&#xff08;Seasonal Adju…

【工作生活】半路出家的学习清单

目录 前言 正文 1.电路原理 2.数字电路 3.模拟电路 4.微机原理 5.C语言 6.C语言 7.计算机组成原理 8.数据结构 9.操作系统 10.计算机网络 11.Linux系统编程 12.其他 13.总结 前言 前两天分享了一篇科班出生的大佬的学习经验&#xff0c;很是羡慕&#xff0c;想…