C++ select模型详解(多路复用IO)

186 篇文章 24 订阅

C++ select模型详解(多路复用IO)

TIPS:以下内容纯属博主自主查阅资料并结合个人理解得出,各位读者仅做参考即可。

关于其它的IO模型可参考链接(阻塞I/O、非阻塞I/O、多路复用I/O、信号I/O、异步I/O):简要概述网络I/O与并发_ufgnix0802的博客-CSDN博客_io并发

引言

  在什么情况下需要使用select模型呢?阻塞I/O问题我们可以使用多线程处理或者将阻塞I/O改为非阻塞I/O,系统调用之后就返回结果,可是完成I/O(即获取数据),我们不得不使用polling(轮询),可每次轮询都是一次系统调用,这将导致一些情况下非阻塞I/O甚至不如阻塞I/O。既然应用进程需要一直polling(轮询),且内核也需要频繁的操作,这时就出现了新的模型——多路复用IO模型

  让内核代理应用进程去做polling(轮询),然后应用进程只有数据准备之后再发起I/O请求不就可以了吗?的确,多路复用I/O就是这样的原理。由内核负责监控应用进程指定的socket文件描述符,当socket准备好数据(可读或可写或异常)的时候,通知应用进程。那么有一个问题是内核如何通知应用进程呢?这个时候我们就可以联想到设计模式中的观察者模式了。当目标的状态发生改变之后,通知其观察者(也就是应用进程)。这个过程中的关键逻辑就是内核保存了所有socket文件描述符,内核通过应用进程特定的回调事件通知应用进程(这个过程在C++ select模型已经透明,即默认使用select模型时,应用进程已经在内核创建了相应的回调事件)。

  在多数情况下,select更多配合的都是非阻塞I/O方式使用。如下:

效果图

APP proces:用户态(即应用进程)

Kernel:内核(内核态)

polling:非阻塞I/O,在这个过程进行轮询

Recv:系统请求数据调用

data waiting右侧:从硬件(磁盘、网卡)的数据读取数据到内核buffer中

cpu copy:从内核buffer中拷贝数据到app buffer中(这个过程需要使用CPU,即整个进程处于阻塞状态)

handle data:应用进程处理数据

  多路复用I/O的本质就是多路监听+阻塞/非阻塞I/O。一般多路复用I/O配合非阻塞I/O进行使用。因为读写socket的时候,并不确定读到什么时候才能读完数据。在一个循环读的过程中,如果设置为阻塞状态,那么进程就会挂起,这将导致多路复用I/O跟阻塞I/O不存在本质区别,即比较好的做法是设置为非阻塞状态。

  多路复用IO几乎成为了主流server方式。尤其是epoll,成为了nginx、redis,tornado等软件高性能的基石。select、poll、epoll是目前主流的多路复用I/O技术。

select模型的原理

  网络通信过程在Unix系统中通常被抽象为文件的读写过程。select模型中的一个socket文件描述符通常可以看成一个由设备驱动程序管理的一个设备,驱动程序可以知道自身的数据是否可用。同时,该设备支持阻塞操作并实现了一组自身的等待队列,如读/写等待队列用户支持上层(用户层)所需的block(阻塞)和non-block(非阻塞)操作。设备的资源如果可用(可读/可写)则会通知应用进程。反之则会让进程睡眠,等待数据到来的时候,再唤醒应用进程。

  多个这样的设备的文件描述符被放在一个队列中,然后select调用的时候遍历这个队列,如果对应的文件描述符可读/可写则会返回该文件描述符(调用应用进程的回调事件)。当遍历结束之后,如果仍然没有一个可用的文件描述符,select会让用户进程睡眠,直到等待资源可用的时候再唤醒用户进程并返回对应的文件描述符(调用应用进程的回调事件),select每次遍历都是线性的。

select模型的不足

  尽管select模型使用很便利,且具有跨平台的特性。但是select模型还是存在一些问题。select模型需要遍队列中的文件描述符,并且这个队列还有最大限制(64)。随着文件描述数量的增长,用户态和内核的地址空间的复制引发的开销也会线性增长。即使监视的文件描述符长时间不活跃,select模型还是会进行线性扫描它。

  为了解决这些问题,操作系统又提供了poll方案,但是poll模型和select模型大致相当,只是改变了一些限制。目前Linux最先进的方式是epoll模型。

select模型与C/S模型的不同点

在互联网应用中,多数架构是C/S模型,即client发出请求,server接收请求,处理之后返回响应。

  • C/S模型中accept()会阻塞一直等待socket连接
  • select模型只解决accept()的等待问题,但并未解决recv()、send()执行带来的阻塞问题

C++中select()函数

<span style="color:#333333"><span style="background-color:#f9f5e9"><code class="language-c">	select(
    _In_ int nfds,//最大SAOCKET + 1,目前直接填0即可,已经无意义
    _Inout_opt_ fd_set FAR * readfds,//可读文件描述符的集合
    _Inout_opt_ fd_set FAR * writefds,//可写文件描述符的集合
    _Inout_opt_ fd_set FAR * exceptfds,//异常文件描述符的集合
    _In_opt_ const struct timeval FAR * timeout//超时时间,如果不填写(nullptr/0)表示进程永远阻塞在select,直到出现可读/可写/异常才返回,如果填写时间代表多久之后返回,即超时时间。
    );
//select返回值大于0,表示响应的socket的数量;如果等于0表示超时;如果小于0表示发生错误
</code></span></span>

select模型的调用时间复杂度是线性的,即O(n)。

select模型是线程不安全的。

<span style="color:#333333"><span style="background-color:#f9f5e9"><code class="language-c">struct timeval {
        long    tv_sec;         /*以秒为单位指定等待时间*/
        long    tv_usec;        /*以毫秒为单位至指定等待时间*/
};
</code></span></span>
<span style="color:#333333"><span style="background-color:#f9f5e9"><code class="language-c">//fd_set代表一系列特定的套接字socket集合。
//Windows平台下的结构如下,Linux结构不同,因为实现原理不同
#define FD_SETSIZE      64//该宏在Linux平台下通常是1024。
typedef struct fd_set {
        u_int fd_count;               //代表socket的个数
        SOCKET  fd_array[FD_SETSIZE];   //代表最多存放的套接字个数,可见最多只能存放64个套接字。
} fd_set;
</code></span></span>

如何突破64或者说1024的限制?答案是使用多线程技术,每个线程单独使用一个select进行检测。这样的话,系统能处理的并发连接数等于线程数*1024。

以下定义的宏能对fd_set进行处理

参数含义
1FD_ZERO(fd_set *fdset)清空文件描述符集合
2FD_SET(int fd,fd_set *fdset)设置需要监听的描述符(把监听的描述符置为1)
3FD_CLR(int fd,fd_set *fdset)清除监听的描述符(把监听描述符置为0)
4FD_ISSET(int fd,fd_set *fdset)判断描述符是否设置(判断描述符是否设置为1)

select( )工作流程

  1. 使用FD_ZERO宏初始化一个fd_set对象(即初始化socket队列)。实际上就是select的第2、3、4的形参。
  2. 使用FD_SET宏将socket文件描述符键入到fd_set对象中(即加入到socket队列中)。
  3. 调用select函数,等待函数返回。如果没有套接字返回,那么select函数会把fe_set对象中的socket队列清空。如果有套接字返回,那么将是返回可读/可写/异常的socket集合。其余不可读/不可写/无异常的套接字将进行清除。
  4. 使用FD_ISSET对返回的套接字集合进行检查,对相应的套接字进行操作。
  5. 之后反复执行以上几个步骤。

源码实例(简单的C++ select模型服务端server)

<span style="color:#333333"><span style="background-color:#f9f5e9"><code class="language-c">#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <WinSock2.h>
#include <vector>
#pragma comment (lib,"ws2_32")
using std::cout;
using std::endl;
using std::cin;

int main()
{
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);

	//1.创建socket
	SOCKET serverSock = socket(AF_INET, SOCK_STREAM, 0);
	if (INVALID_SOCKET == serverSock)
	{
		cout << "创建服务端SOCKET" << endl;
		return 0;
	}
	//2.ip端口和socket关联
	SOCKADDR_IN serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(7890);
	serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	if (SOCKET_ERROR == bind(serverSock, (sockaddr *)&serverAddr, sizeof(SOCKADDR_IN)))
	{
		cout << "bind 失败" << endl;
		return 0;
	}
	cout << "bind 成功" << endl;
	//3.监听端口
	if (INVALID_SOCKET == listen(serverSock, 5))
	{
		cout << "listen 失败" << endl;
		return 0;
	}
	cout << "listen 成功" << endl;
	
	std::vector<SOCKET> clients;
	while (true)
	{
		fd_set reads;
		FD_ZERO(&reads);
		//默认添加服务端socket
		FD_SET(serverSock, &reads);
		do
		{
			std::vector<SOCKET>::iterator begin = clients.begin();//等价于 auto begin = clients.begin();
			auto end = clients.end();
			for (; begin != end; begin++)
				FD_SET(*begin, &reads);
		} while (false);

		//时间参数:如果不填写时间那么会永远阻塞,直到出现可读/可写/异常才返回,如果填写时间代表多久之后返回
		//select返回的值大于0,表示响应的socket数量,如果等于0超时,如果小于0则代表发生错误
		int nRet = select(0, &reads, nullptr, nullptr, nullptr);
		if (0 == nRet)continue;
		if (-1 == nRet)break;
		
		if (FD_ISSET(serverSock, &reads))
		{
			//4.等待客户端连接
			cout << "客户端连接" << endl;

			SOCKADDR_IN clientAddr;
			int addrLen = sizeof(SOCKADDR_IN);
			SOCKET clientSock = accept(serverSock, (sockaddr *)&clientAddr, &addrLen);
			if (INVALID_SOCKET == clientSock)
			{
				std::cout << "客户端连接失败 " << std::endl;
				return 0;
			}
			cout << "客户端连接成功 " << inet_ntoa(clientAddr.sin_addr) << " port " << ntohs(clientAddr.sin_port) << endl;
			clients.push_back(clientSock);
		}
		else
		{
			//5.客户端服务端通信
			do
			{
				auto begin = clients.begin();
				auto end = clients.end();
				for (; begin != end; begin++)
				{
					if (FD_ISSET(*begin, &reads))
					{
						char recvBuffer[1024]{ 0 };
						int nRecv = recv(*begin, recvBuffer, 1024, 0);
						//当接收到的数据小于等于0时,代表客户端断开连接或者发生某种错误
						if (nRecv < 0)continue;
						if (0 == nRecv)
						{
							//关闭当前客户端
							cout << "关闭客户端\n";
							begin = clients.erase(begin);
							break;
						}

						if(nRecv > 0 )
							cout << "recvLen:" << nRecv << " 数据:" << recvBuffer << endl;
					}
				}
			} while (false);
		}
	}
	
	closesocket(serverSock);
	WSACleanup();
	return 0;
}
</code></span></span>

select模型图示(以服务端为例)

效果图

参考文章

I/0模型(select模型)_啦啦leilei的博客-CSDN博客_select模型

select模型_Echo佩雨的博客-CSDN博客_select模型

网络编程——select模型(总结) - SegmentFault 思否

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值