Linux服务器搭建 IO多路复用epoll技术 编程示例

目录

一:服务器搭建设计

二:阻塞 & 非阻塞

三:IO模型

四:IO多路复用技术

五:epoll 服务器搭建设计


一:服务器搭建设计

一个服务器可以连接多个客户端,3种设计方案

1. 一个客户端 对应 服务器上的一个进程:accept函数之后通过fork开子进程

   缺陷:进程间不能通信,一定需要借助IPC技术

2. 一个客户端 对应 服务器上的一个线程:accept函数之后pthread_create创建子线程

   缺陷:因为线程可以做到数据共享,数据不安全,解决方案是互斥量\信号量

 

主要问题:操作系统承载线程是有上限,上限会根据不同的硬件的配置有所差别,但一定不能到达百万级别

3. IO多路复用技术,在本节中重点介绍(使用epoll IO多路复用技术,就可以使得无进程、无线程的创建,依旧支持一个服务器可以连接上多个客户端)

二:阻塞 & 非阻塞

什么是阻塞?

下面以快递物流为例,理解阻塞

 

有电话通知才会去领取快递,在没有通知的情况下,可以自己做自己的事情

阻塞:空出大脑可以安心睡觉(不占用CPU宝贵的时间片)

每隔一会就催促一次,无论对于收件员还是快递员都是大量浪费时间的

非阻塞:浪费时间,浪费电话费,占用快递员时间(占用CPU,系统资源)

为什么需要前后置服务器设计?

一个简单的改进方案是在服务器端使用多线程(或多进程)

多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接

下面还是以快递物流为例,了解多线程/多进程

缺点:多个快递员同时向你电话,可能因为忙机,无法及时领取其他没有接到电话的物件(多个客户端需要排队等待数据处理返回)

IO多路复用技术 epoll(本节重点)

同样是快递物流为例, 

epoll就可以类比是一个快递站,在快递到了的时候会发送短信通知你,让你及时取件(不会出现漏接电话的情况)

三:IO模型

阻塞I/O

非阻塞I/O

I/O复用(select和poll epoll)

信号驱动I/O

异步I/O

阻塞I/O模型

最流行的I/O模型是阻塞I/O模型,缺省时,所有的套接口都是阻塞的(数据就绪后才会处理返回)

非阻塞I/O模型

当我们把一个套接口设置为非阻塞方式时,即通知内核:当请求的I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误(无论数据是否准备好都要返回,直到数据就绪后处理返回的那次才是有效的办事)

非阻塞IO模型,应用程序连续不断地查询内核,看看某操作是否准备好,这对cpu时间是极大的浪费,一般只在专门提供某种功能的系统中才会用到 

四:IO多路复用技术

select函数

select函数作用:

这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程

(仅仅知道有IO事件发生,却并不知道是哪几种流,只能做无差别轮询所有的流,找到能读出数据或者写入数据的流,对它们进行操作;select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长,select底层用的是[有限制长度]的数组)(类似这种从头遍历也就是O(n)复杂度,举个例子,比如现在有10000个客户端,但是第9999个客户端中才有数据需要处理返回,就需要从1遍历到9999,效率非常低下; 更离谱的是,如果第9999个客户端数据处理返回后,恰好这个时间第9998个客户端有数据需要处理返回,那么又要重头开始对这10000个数据再遍历一次,由此可见,select非常落后!)

Poll

Poll函数和select类似,但它是用文件描述符而不是条件的类型来组织信息的

也就是说,一个文件描述符的可能事件都存储在struct pollfd中;与之相反,select用事件的类型来组织信息,而且读,写和错误情况都有独立的描述符掩码;poll函数是POSIX:XSI扩展的一部分,它起源于UNIX System V

poll的本质和select没区别(轮询的方式没有改变,从头到尾遍历,只是数据的存储结构修改了,数组---》链表,虽然存储问题解决,但是遍历的问题还是没有解决,依旧是O(n)复杂度),select好比数组,而poll是对数组的升级变成容器,看似升级实则无用,它将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,但是它[没有最大的连接数限制],原因是因为它是基于链表来存储的  (数组有限制长度,(容器)链表没有,但是select、poll二者都是遍历O(n)复杂度,效率都是非常低下的!)

epoll 

epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次

Linux中提供的epoll相关函数如下:

int epoll_create(int size);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的IO事件通知给程序员/主进程,epoll实际上是事件驱动(每个事件关联上fd),此时我们对这些流的操作就是有意义的,复杂度降低了变成了O(1)

上图可以加深对epoll的理解,打个比方,

上图中的事件队列 可以类比成我们平时坐动车时的动车站候车厅,

而上图中的就绪列表 可以类比成动车在到达时的广播提醒我们,我们就会立马去检票口排队,通知我们坐上动车(上图中的进程),

不过进程在处理完后,会将fd返回给事件队列(在客户端没有下线情况下,可多次为客户端服务) 

五:epoll 服务器搭建设计

 查看epoll_create函数使用说明

查看epoll_ctl函数使用说明 

查看epoll_wait函数使用说明 

核心源码:

服务器完整代码(使用epoll IO多路复用技术 无需进程、线程创建)

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

using namespace std;

int main()
{
	struct epoll_event epollevent;
	//事件结构体数组 编写代码中用作判断使用
	struct epoll_event epolleventArray[5];
	int epollfd = 0;
	int epollwaitfd = 0;

	char buf[50] = { 0 };
	struct sockaddr_in addr;
	int len = 0;
	int acceptfd = 0;

	//初始化网络 识别当前计算机是否可以联网
	//第一个参数:采用IPV4 IP地址 第二个参数:网络分配TCP 
	int socketfd = socket(AF_INET, SOCK_STREAM, 0);
	if (socketfd == -1)
	{
		perror("socket error");
	}
	else
	{
		cout << "socketfd = " << socketfd << endl;
		//确定用IPV4地址
		addr.sin_family = AF_INET;
		//服务器开放自己的IP地址给客户端连接使用   INADDR_ANY生成默认的可以联网的IP地址
		addr.sin_addr.s_addr = INADDR_ANY;
		//绑定服务器端口号0-65535  10000以下系统默认使用
		addr.sin_port = htons(10086);

		len = sizeof(addr);

		int opt_val = 1;
		//解决 address already is use 报错
		//端口复用 设置,一定在bind函数前
		setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt_val,sizeof(opt_val));

		//bind 绑定ip地址 绑定端口号
		if (bind(socketfd, (struct sockaddr*)&addr, len) == -1)
		{
			perror("bind error");
		}

		if (listen(socketfd, 10) == -1)
		{
			perror("listen error");
		}

		cout << "网络搭建成功" << endl;

		cout << "epoll创建" << endl;
		//事件结构体初始化
		bzero(&epollevent, sizeof(epollevent));
		//绑定当前准备好的socketfd(服务器可使用的网络通道文件描述符)上线使用/acceptfd发数据使用
		epollevent.data.fd = socketfd;
		//绑定有可能触发的事件 当前是socketfd 如果有事件发生一定就是 客户端连接
		epollevent.events = EPOLLIN;
		
		//创建epoll
		epollfd = epoll_create(5);
		//epoll事件队列添加socketfd 它感兴趣的事件是epollevent
		epoll_ctl(epollfd, EPOLL_CTL_ADD, socketfd,&epollevent);

		while (1)
		{
			cout << "epoll wait........." << endl;
			//阻塞式函数 等待事件发生
			epollwaitfd = epoll_wait(epollfd, epolleventArray, 5, -1);
			if (epollwaitfd < 0)
			{
				perror("epoll_wait error");
			}
			for (int i = 0; i < epollwaitfd; i++)
			{
				//判断是否有客户端上线
				if (epolleventArray[i].data.fd == socketfd)
				{
					cout << "服务器有客户端连接........." << endl;
					//服务器等待客户端连接 阻塞式函数 acceptfd在服务器代表已经连接成功的客户端
					acceptfd = accept(socketfd, NULL, NULL);
					cout << "有客户端成功连接 acceptfd = " << acceptfd << endl;

					epollevent.data.fd = acceptfd;
					epollevent.events = EPOLLIN;
					epoll_ctl(epollfd, EPOLL_CTL_ADD, acceptfd, &epollevent);
				}
				else if(epolleventArray[i].events & EPOLLIN)
				{
					//有客户端发来数据
					cout << "有事件发生 但不是socketfd 是客户端" << acceptfd << endl;
					bzero(buf, sizeof(buf));
					int res = read(epolleventArray[i].data.fd, buf, sizeof(buf));
					if (res > 0)
					{
						cout << "服务器收到客户端发来的数据.....buf = " << buf << endl;

					}
					else if(res <= 0)
					{
						cout << "客户端掉线............." << acceptfd << endl;
						
						//从epoll中删除该fd
						epollevent.data.fd = epolleventArray[i].data.fd;
						epollevent.events = EPOLLIN;
						epoll_ctl(epollfd, EPOLL_CTL_DEL, epolleventArray[i].data.fd, &epollevent);
						//关闭这个fd所对应的网络通道
						close(epolleventArray[i].data.fd);
					}

				}
			}
		}
	}
	return 0;
}

问题1

服务器 端口复用问题解决

(使用setsockopt  并且端口复用设置必须在bind执行前)

        int opt_val = 1;
		//解决 address already is use 报错
		//端口复用 设置,一定在bind函数前
		setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, 
(const void*)&opt_val,sizeof(opt_val));

		//bind 绑定ip地址 绑定端口号
		if (bind(socketfd, (struct sockaddr*)&addr, len) == -1)
		{
			perror("bind error");
		}

问题2

谁上线:socketfd(服务器可使用的网络通道文件描述符)

谁发送数据:acceptfd(客户端文件描述符)

epoll是可以支持存储多个acceptfd的,不过epollfd会占用一个位置

问题3

客户端 先于服务器下线 问题解决

if (res > 0)
{
	cout << "服务器收到客户端发来的数据.....buf = " << buf << endl;
}
else if(res <= 0)
{
	cout << "客户端掉线............." << acceptfd << endl;
						
	//从epoll中删除该fd
	epollevent.data.fd = epolleventArray[i].data.fd;
    epollevent.events = EPOLLIN;
	epoll_ctl(epollfd, EPOLL_CTL_DEL, epolleventArray[i].data.fd, &epollevent);
	//关闭这个fd所对应的网络通道
	close(epolleventArray[i].data.fd);
}

核心源码:

客户端完整代码(配合服务器测试)

#include<iostream>
#include <sys/types.h>         
#include <sys/socket.h>
#include<stdio.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
 
using namespace std;
 
int main()
{
	char buf[50] = { 0 };
	struct sockaddr_in addr;
	int len = 0;
	//初始化网络 识别当前计算机是否可以联网
	//第一个参数:采用IPV4 IP地址 第二个参数:网络分配TCP 
	int socketfd = socket(AF_INET, SOCK_STREAM, 0);
	cout << "客户端 socketfd = " << socketfd << endl;
	if (socketfd == -1)
	{
		perror("socket error");
	}
	else
	{
		//确定用IPV4地址
		addr.sin_family = AF_INET;
		//客户端主动寻找服务器IP地址 127.0.0.1本机回环地址 192.168.75.128
		addr.sin_addr.s_addr = inet_addr("192.168.75.128");
		//绑定服务器端口号0-65535  10000以下系统默认使用
		addr.sin_port = htons(10086);
 
		len = sizeof(addr);
 
		//主动去连接服务器 IP和端口
		if (connect(socketfd, (struct sockaddr*)&addr, len) == -1)
		{
			perror("connect error");
		}
		else
		{
			cout << "客户端连接服务器成功" << endl;
		}
 
		while (1)
		{
			cin >> buf;
			int res = write(socketfd, buf, sizeof(buf));
			cout << "客户端发送 res = " << res << endl;
			bzero(buf, sizeof(buf));
		}
	}
 
	return 0;
}

结果:一个服务器与多个客户端连接 可以实现socket通信 数据共享

 ps -aux 查看一下 进程

由上图不难看出,服务器就一个进程 对应客户端三个进程(服务器与客户端是一对多的关系)

也就说明了使用epoll无需创建进程

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

chenruhan_QAQ_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值