一、前言
首先来分析一下进程版通信和线程版通信
- 利用进程,一个客户端一个进程
- 利用线程,一个客户单一个线程
进程是资源分配的基本单位,如果客户端数量剧增,每一个进程都会有代码段、数据段、堆栈段,所以使用进程开销大, 且进程间数据还不能共享,也不可能每个进程都写一个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>
- 准备好事件结构体
- 结构体初始化——bzero
- 绑定当前准备好的socketfd (可用网络对象)
- 绑定事件为客户端接入事件
- 创建epoll——epoll_create
- 将已经准备好的网络描述符添加到epoll事件队列中——epoll_ctl(EPOLL_CTL_ADD)
- 等待客户端上线:循环判断事件队列里是否有客户端上线(epolEventArray[i].data.fd == socketfd),有上线就把客户端acceptfd 绑定事件并添加到epoll
- 客户端产生了事件以后,就进行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;
}
六、运行结果