作者:小 琛
欢迎转载,请标明出处
文章目录
多路转接IO的存在意义
多路转接IO,通常也称为多路复用IO。
作用:IO多路转接可以实现对大量描述符的监控,监控的事件可以为可读、可写、异常等。
当我们使用多路转接的时候,若某时刻该接口发现了监控下的某个描述符就绪,就会通知进程,进而针对该描述符进行操作,而其它未就绪的描述符则继续监控。
优点:避免了当其它文件描述符未就绪时,进程陷入阻塞的情况
poll(了解即可)
与select的对比
poll无法像select一样跨平台移植,只能在Linux下使用,也是采用轮询遍历的方式,但效率有提升
poll不再限制文件描述符个数,poll引入了关心文件描述符的事件
接口函数
int poll (struct pollfd* fds,nfds_t nfds,int timeout)
参数:
- struct pollfd* fd:事件结构体的数组,内含两个成员:int fd、short events(POLLIN&POLLOUT),使用时需要自定义这个结构体
- nfds_t nfds:事件结构体数组的成员个数
- 超时等待时间:小于0为阻塞、等于0为非阻塞、大于0为具体数值,单位为毫秒
返回值:
小于0:poll出错了
等于0:监控超时
大于0:就绪的文件描述符个数
poll的性能分析
优点:
- poll采用了事件结构的方式,简化了编写复杂度
- poll不再限制文件描述符个数
缺点:
- poll仍需要轮询遍历事件结构体数组,随着文件描述符增多,性能仍会下降
- poll不支持跨平台
- poll仍未告诉使用者哪一个文件描述符就绪,需要遍历查询
epoll(重点学习)
epoll的评价
目前公认的Linux下性能最优的多路转接IO模型
接口函数
函数功能:创建epoll操作句柄
函数名称:int epoll_creat(int size)
参数:
- size:定义epoll最大监控个数,但再Linux2.6.8后,size已经被弃用,内存现在采用的是扩容方式
返回值:返回epoll操作句柄
函数功能:epoll操作函数
函数名称:int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event)
函数参数:
- epfd:epoll操作句柄
- op:该函数具体执行什么功能
EPOLL_CTL_ADD:添加事件
EPOLL_CTL_MOD:修改事件
EPOLL_CTL_DEL:删除事件- fd:具体关心的文件描述符
- epoll_event* event:一个结构体
{
uint32_t events //关心事件 EPOLLIN为可读、EPOLLOUT为可写
epoll_data_t data:具体数据
}
函数功能:监控函数
函数名称:int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout)
参数:
- epfd:epoll操作句柄
- events:epoll事件结构数组,是一个出参,返回就绪的事件结构
- maxevents:最大能拷贝多少个事件结构
- timeout:超时事件
大于0:带有超时时间,单位为毫秒
等于0:非阻塞
小于0:阻塞返回值:
- 大于0:返回就绪的文件描述符个数
- 等于0:等待超时
- 小于0:监控出错
epoll的两种触发方式
水平触发
EPOLLLT -> epoll的默认工作模式,与select和poll相同
对于可读事件:当接收缓存区数据大于水平标记位(1字节),会一直触发可读事件就绪,直到接收缓冲区当中无数据可读
对于可写事件:当发送缓存区剩余数据空间大于水平标记位(1字节),会一直触发可读事件就绪,直到发送缓冲区无空间
边沿触发
EPOLLET -> 只有epoll支持
开启方式:在对应的文件描述符关心事件中,按位或EPOLLET即可,例如:struct epoll_event ev;ev.events=EPOLLIN | EPOLLET;
对于可读事件:只有当新数据来的时候,才会触发可读,言外之意,每次来一个新数据,仅仅会通知一次,即使接收缓冲区中仍有数据未读完,也不会通知,直到下一个数据到来。因此对于该模式,在读数据的时候,一定要将所需数据读完,否则将不会再收到通知
对于可写事件:只有发送缓冲区的剩余空间从不可写变为可写才会触发可写事件就绪
解析epoll的底层
在内核中,对于就绪事件用红黑树存储,内核通过遍历红黑树进行监控,搜索效率为O(logn)
epoll使用的例子 -> epoll与tcp通信
epoll_lt_svr.hpp
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <vector>
#include "tcpsvr.hpp"
class EpollSvr
{
public:
EpollSvr()
{
epoll_fd_ = -1;
}
~EpollSvr()
{
}
bool InitSvr()
{
epoll_fd_ = epoll_create(10);
if(epoll_fd_ < 0)
{
return false;
}
return true;
}
bool AddFd(int fd)
{
//1.组织事件结构
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);
if(ret < 0)
{
perror("epoll_ctl");
return false;
}
return true;
}
bool DeleteFd(int fd)
{
int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, NULL);
if(ret < 0)
{
perror("epoll_ctl");
return false;
}
return true;
}
bool EpollWait(std::vector<TcpSvr>* out)
{
//10
struct epoll_event fd_arr[10];
int ret = epoll_wait(epoll_fd_, fd_arr, sizeof(fd_arr)/sizeof(fd_arr[0]), -1);
if(ret < 0)
{
perror("epoll_wait");
return false;
}
else if(ret == 0)
{
printf("epollwait timeout\n");
return false;
}
//防止数组越界
if(ret > sizeof(fd_arr)/sizeof(fd_arr[0]))
{
ret = sizeof(fd_arr)/sizeof(fd_arr[0]);
}
//剩下的逻辑都是ret大于0的逻辑了
for(int i = 0; i < ret; i++)
{
TcpSvr ts;
ts.SetFd(fd_arr[i].data.fd);
out->push_back(ts);
}
return true;
}
private:
//epoll操作句柄
int epoll_fd_;
};
tcpsvr.hpp
#pragma once
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <string>
class TcpSvr
{
public:
TcpSvr()
{
sockfd_ = -1;
}
~TcpSvr()
{
}
//创建套接字
bool CreateSocket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(sockfd_ < 0)
{
perror("socket");
return false;
}
int i = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(int));
return true;
}
//绑定地址信息
bool Bind(const std::string& ip, uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = bind(sockfd_, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0)
{
perror("bind");
return false;
}
return true;
}
//侦听
bool Listen(int backlog = 5)
{
int ret = listen(sockfd_, backlog);
if(ret < 0)
{
perror("listen");
return false;
}
return true;
}
//获取连接
//bool Accept(struct sockaddr_in* peeraddr, int* sockfd)
//peeraddr:出参,返回客户端的地址信息
//ts:出参,返回一个TcpSvr类的实例化指针,在这个类的实例化指针当中保存新创建出来的套接字描述符,上层调用者可以使用返回的类的实例化指针和客户端进行通信
bool Accept(struct sockaddr_in* peeraddr, TcpSvr* ts)
{
socklen_t addrlen = sizeof(struct sockaddr_in);
int serverfd = accept(sockfd_, (struct sockaddr*)peeraddr, &addrlen);
if(serverfd < 0)
{
perror("accept");
return false;
}
ts->sockfd_ = serverfd;
return true;
}
//发起连接(client)
bool Connect(std::string& ip, uint16_t port)
{
struct sockaddr_in destaddr;
destaddr.sin_family = AF_INET;
destaddr.sin_port = htons(port);
destaddr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret = connect(sockfd_, (struct sockaddr*)&destaddr, sizeof(destaddr));
if(ret < 0)
{
perror("connect");
return false;
}
return true;
}
//发送数据
bool Send(std::string& data)
{
int sendsize = send(sockfd_, data.c_str(), data.size(), 0);
if(sendsize < 0)
{
perror("send");
return false;
}
return true;
}
//接收数据
//data:是一个出参,将接收到的数据返回给调用者
bool Recv(std::string* data)
{
char buf[1024] = {0};
int recvsize = recv(sockfd_, buf, sizeof(buf) - 1, 0);
if(recvsize < 0)
{
perror("recv");
return false;
}
else if(recvsize == 0)
{
printf("peer shutdown connect\n");
return false;
}
(*data).assign(buf, recvsize);
return true;
}
//关闭套接字
void Close()
{
close(sockfd_);
sockfd_ = -1;
}
void SetFd(int fd)
{
sockfd_ = fd;
}
int Getfd()
{
return sockfd_;
}
private:
int sockfd_;
};
test.cpp
#include "epoll_lt_svr.hpp"
#define CHECK_RET(p) if(p != true){return -1;}
int main()
{
TcpSvr listen_ts;
CHECK_RET(listen_ts.CreateSocket());
CHECK_RET(listen_ts.Bind("0.0.0.0", 19997));
CHECK_RET(listen_ts.Listen());
EpollSvr es;
CHECK_RET(es.InitSvr());
es.AddFd(listen_ts.Getfd());
while(1)
{
//1.监控
std::vector<TcpSvr> vec;
if(!es.EpollWait(&vec))
{
continue;
}
for(size_t i = 0; i < vec.size(); i++)
{
//2.接收新的连接
if(listen_ts.Getfd() == vec[i].Getfd())
{
struct sockaddr_in peeraddr;
TcpSvr peerts;
listen_ts.Accept(&peeraddr, &peerts);
printf("Have a new connection : [ip] : %s [port] : %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
//将新创建出来的套接字添加到select的事件集合当中去
es.AddFd(peerts.Getfd());
}
//3.连接上有数据
else
{
std::string data;
bool ret = vec[i].Recv(&data);
if(!ret)
{
es.DeleteFd(vec[i].Getfd());
vec[i].Close();
}
printf("client send data is [%s]\n", data.c_str());
}
}
}
return 0;
}