0
点赞
收藏
分享

微信扫一扫

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

Greatiga 2022-04-04 阅读 42

一、前言

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

  • 利用进程,一个客户端一个进程
  • 利用线程,一个客户单一个线程

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

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

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

二、五个I/O模型

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

1、阻塞I/O

没有读到数据就阻塞,进行等待,其他事情都不做

read在没有读取到数据的时候会返回-1,然后继续读,读到数据也立即返回(没有阻塞操作,而且还是循环调用read、判断,不断返回操作结果),这样导致cpu非常的繁忙,造成cpu时间上极大的浪费

三、多路复用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复用

 四、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;
}


六、运行结果

 

举报

相关推荐

0 条评论