基于VS2019 C++的跨平台(Linux)开发(2.6)——SOCKET-IO复用技术

一、前言

首先来分析一下进程版通信和线程版通信

  • 进程版通信:一个客户端一个进程
  • 线程版通信:一个客户单一个线程

进程是资源分配的基本单位,如果客户端数量剧增,每一个进程都会有代码段、数据段、堆栈段,所以使用进程开销大, 且进程间数据还不能共享,也不可能每个进程都写一个IPC实现通信。

如果使用线程,一个进程如果开出N个线程,一旦这个进程被杀死,就会导致N个线程全部销毁。而且在一些计算机上一个进程最多开300个进程(现实中肯定不够用,就像QQ平均每天在线人数达到7亿多),但这时候操作系统也被卡死了。

线程虽然解决了进程的开销问题,但同时也存在弊端。所以就要使用网络通道的复用技术

二、五个I/O模型

  • 阻塞I/O
  • 非阻塞I/O
  • I/O复用(select和poll)
  • 信号驱动I/O
  • 异步I/O

1、阻塞I/O

        当系统read不到数据,就会一直阻塞在等待数据这个阶段,直到read到数据或者发生错误,才会继续往下走,即在系统调用read函数到read结束这个期间都是阻塞的,在阻塞期间CPU可以去做其他事。如下图

2、非阻塞I/O

        read在没有读取到数据的时候会返回-1,然后继续读,读到数据也立即返回(没有阻塞操作,系统会一直处在判断read是否成功(数据是否准备好)的循环(轮询)中,直到read成功才会继续往下走),这样导致cpu非常的繁忙,造成cpu时间上极大的浪费,一般只在专门提供某种功能的系统中才会用到。如下图

阻塞I/O与非阻塞I/O的区别

阻塞I/Oread只调用一次,非阻塞I/O中read一直循环调用(cpu没有休息)

 示例:

非阻塞IO采用轮询机制,就好像我们不断给快递小哥打电话询问,占用了大量时间

三、多路复用IO

首先以一个收快递的例子,来说说多进程IO和多路复用IO

多个客户端上线(connect)存在多个socket通道(代码中就是调用accept函数可以看到返回值是4、5、6......)

1、执行过程及原理

事件队列(等待状态)
socket——>发送数据等操作——>到就绪队列
就绪队列(准备状态)
socket唤醒——>到主进程
socket做完操作——>回到事件队列
主进程——>(线程池可解决队列先进先出)

详细过程说明:   

        无论对接了多少socket,在没有传递消息的时候,socket全部存放在事件队列中(还没有发生业务,如客户端发送数据或者掉线,socket全部处于等待状态),如果其中一个socket需要发送数据,就会移出到就绪队列。就绪队列就会唤醒进程来操作socket。就好像快递小哥把快递暂时存储在就绪队列,通过给我们发取件码或者消息告诉我们快递到了。

        如果就绪队列中的第一个socket操作完了不是销毁,而是还回去到事件等待队列中(如下图),主进程接着唤醒第二个socket进行操作,即这个进程是为所有socket服务的(同一个read、write针对多个客户端)。客户端可能先注册后登录再聊天,他们先后产生事件,事件发生送到就绪队列执行,执行完再返回事件队列(因为socket表示一个客户端,必须存在)。       

 2、复用技术

        目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。他们的目的都一样,都是为了实现IO复用。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小

1、select、poll、epoll区别:

①从时间复杂度和IO效率来看:
select、poll都需要轮询事件队列的fd(客户端),时间复杂度为o(n),CPU不会闲置,客户端越多,处理时间越长;epoll不需要轮询,只需要等待即可,时间复杂度为O(1),CPU会闲置,与活跃的客户端数有关,不会随着描述符数目的增加而下降。

②从结构来看:
select一般采用整数数组(存描述符fd); poll采用链表epoll采用红黑树和双向链表    

③从连接数来看:
select在32位系统下,最多连接3232,同理,64位系统最多连接6464;poll采用链表,理论上是无限的;epoll有上限,1G内存能连接10万左右客户端

④从消息传递方式来看:
select和poll:将消息从内核空间传递到用户空间,都需要内核拷贝动作;epoll:可以通过内核和用户空间共享一块内存来实现。

2、

select、poll的缺点如下

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 内核需要遍历队列中存放的每个fd,这个开销在fd很多时也会很大
  • 其中select最大连接数有限制,默认是1024

epoll解决了select和poll存在的3个缺点。

  • 每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  • 不是轮询的方式,不会随着fd数目的增加导致效率下降。只有活跃可用的fd才会调用callback函数。
  • 虽然连接数有上限,但是很大,能打开的fd的上限远大于1024 (1G的内存上能监听约10万个端口)。

 四、epoll使用流程

头文件#include <sys/epoll.h>

  1. 准备好事件结构体
  2. 结构体初始化——bzero
  3. 绑定当前准备好的socketfd (可用网络对象)
  4. 绑定事件为客户端接入事件
  5. 创建epoll——epoll_create
  6. 将已经准备好的网络描述符添加到epoll事件队列中——epoll_ctl(EPOLL_CTL_ADD)
  7. 等待客户端上线:循环判断事件队列里是否有客户端上线(epolEventArray[i].data.fd == socketfd),有上线就把客户端acceptfd 绑定事件并添加到epoll
  8. 客户端产生了事件以后,就进行read,返回值>0表示接收到数据,返回值<=0表示客户端掉线,那么就从epoll中删除客户端描述符——epoll_ctl(EPOLL_CTL_DEL)。

五、epoll示例

服务器核心代码如下

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>   
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <iostream>//cout
#include <map>
#include <sys/epoll.h>

using namespace std;

int main() {

	struct epoll_event epolEvent;
	struct epoll_event epolEventArray[5];//看起来是数组,内部是队列的形式
	int epollfd = 0;
	int epolfd;
	int epol_waitfd;
	int opt_val = 1;


	struct sockaddr_in s_addr;

	int socketfd = 0;
	int length = 0;
	int acceptfd = 0;//客户端的文件描述符
	char ser_buf[66] = { 0 };
	int pid = 0;
	//初始化网络
	socketfd = socket(AF_INET, SOCK_STREAM, 0);
	if (socketfd == -1)
	{
		perror(" socket error");
	}
	else
	{
		//确定使用那个协议族 ipv4
		s_addr.sin_family = AF_INET;
		//系统自动获取本机ip地址
		s_addr.sin_addr.s_addr = INADDR_ANY;
		//端口65535,10000以下是操作系统使用,自己定义需要10000以后
		s_addr.sin_port = htons(10086);

		length = sizeof(s_addr);
		//端口复用,解决 地址重用问题
		setsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR,(const void *)opt_val,sizeof(opt_val));
		//绑定ip地址和端口号
		if (bind(socketfd, (struct sockaddr*)&s_addr, length) == -1)
		{
			perror(" bind error");
		}
		//监听这个地址和端口有没有客户端连接

		if (listen(socketfd, 10) == -1)
		{
			perror(" listen error");
		}
		cout << "服务器网络通道准备好了" << endl;

		//事件结构体初始化
		bzero(&epolEvent,sizeof(epolEvent));
		//绑定当前准备好的socketfd(可用网络对象)
		epolEvent.data.fd = socketfd;
		//绑定事件为客户端接入事件
		epolEvent.events = EPOLLIN;
		//创建epoll
		epolfd = epoll_create(5);
		//将已经准备好的网络描述符添加到epoll事件队列中
		epoll_ctl(epolfd,EPOLL_CTL_ADD,socketfd,&epolEvent);

		while (true)
		{
			cout << "epoll wait client。。。"<<endl;
			epol_waitfd = epoll_wait(epolfd,epolEventArray,5,-1);//5和前面要对应

			if (epol_waitfd < 0)
			{
				perror("epoll_wait error");
			}
			for (int i = 0; i < epol_waitfd; i++)
			{
				//判断是否有客户端上线
				if (epolEventArray[i].data.fd == socketfd)
				{
					cout << "网络开始工作  等待客户端上线" << endl;
					acceptfd = accept(socketfd,NULL,NULL);
					cout << "acceptfd = " << acceptfd << endl;

					//上线的客户端描述符是acceptfd 绑定事件添加到epoll
					epolEvent.data.fd = acceptfd;
					epolEvent.events = EPOLLIN;
					epoll_ctl(epolfd,EPOLL_CTL_ADD, acceptfd,&epolEvent);
				}
				//客户端产生了事件以后
				else if (epolEventArray[i].events & EPOLLIN)
				{
					bzero(ser_buf,sizeof(ser_buf));
					int res = read(epolEventArray[i].data.fd, ser_buf,sizeof(ser_buf));

					if (res > 0)
					{
						cout << "服务器接收到数据   buf =  " << ser_buf <<endl;

					}
					else if (res <= 0)
					{
						cout << "客户端掉线。。。 " << ser_buf << endl;
						close(epolEventArray[i].data.fd);

						//从epoll中删除客户端描述符
						epolEvent.data.fd = epolEvent.data.fd;
						epolEvent.events = EPOLLIN;
						epoll_ctl(epollfd,EPOLL_CTL_DEL, epolEventArray[i].data.fd,&epolEvent);
					}

				}
			}
		}

		
	}



	return 0;
}


 注意:

1、前面的socketfd表示的是服务器打通的网络通道,这个时候没有上线的客户端(还没有客户端对接),只代表服务器本身自己的通道入口

而下面的acceptfd,表示有客户端上线进行连接。

所以,epoll中存放的所有的socketfd通道包含两种,一种是服务器本身的通道,用来做等待(accept),另一种是接入的客户端,即调用accept的返回值(acceptfd)

2、客户端上线之后要进行read,之前都是用acceptfd,但是这里acceptfd给了事件结构体中的fd,所以这里read要使用产生事件后的fd,即epolEventArray[i].data.fd

3、 客户端主动下线,发出数据告诉服务器,可以读到数据。如果是掉线, 要么是网络断开连接了,要么是客户端程序奔溃(异常下线)  ,这时候要关闭fd,从epoll中删除客户端描述符

4、排错方法
接收read和write返回值,并打印出来,打印大小和发送、接收的大小一致,表示接收、发送成功

5、解决地址重用问题(address already use)

进行端口复用——绑定端口(bind)之前设置

  setsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR,(const void *)opt_val,sizeof(opt_val));

六、运行结果

 

 ref:

http://my.oschina.net/xianggao/blog/663655

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ze言

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值