1.I/O模型
阻塞等待
多进程、多线程实现的并发,消耗资源,cpu未充分利用
系统为每个并发的客户分配一个进程,该进程只处理该客户信息。由于IO事件的时间较慢,所以进程通常都是阻塞在IO处,浪费系统资源,cpu未充分利用。

非阻塞等待(轮询)
只有一个进程,依次轮询每个连接套接字connfd的状态。状态发生改变时,应用层进行处理。浪费cpu(轮询)

多路I/O复用
只有一个进程,所有套接字(包括listenfd和connfd)都交给内核监听,套接字状态发生时,通知应用层处理。
内核监听的函数通常有:select,poll,epoll

2 select
原理
-
select 能监听的文件描述符个数受限于 FD_SETSIZE,一般为 1024
-
解决 1024 以下客户端时使用 select 是很合适的,但如果链接客户端过多,select 采用的是轮询模型,会大 大降低服务器响应效率,不应在 select 上投入更多精力。
-
(1)单进程可以打开fd有限制,1024,内核FD_SETSIZE决定;
(2)对文件描述符进行扫描时是线性扫描,即采用轮询的方法,效率较低;
(3)用户空间和内核空间的复制非常消耗资源;
-
如果开启大量客户端连接后,任何又关闭,nfds将被撑大,轮询时间变长(可用文件描述符重定向解决 dup2())
-
大量并发,少量活跃,效率低
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加 1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3 种情况
1.NULL,永远等下去
2.设置 timeval,等待固定时间
3.设置 timeval 里时间均为 0,检查描述字后立即返回,轮询
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
int FD_ISSET(int fd, fd_set *set); //文件描述符集合里 fd 是否置1 (判断fd是否在文件描述符集合中)
void FD_CLR(int fd, fd_set *set); //文件描述符集合里 fd 位置0 (把fd从文件描述符中删除)
void FD_SET(int fd, fd_set *set); //文件描述符集合里 fd 位置1 (把fd添加到文件描述符集合中)
void FD_ZERO(fd_set *set); //文件描述符集合里所有位置 0 (把文件描述符集合清空)

sever.cpp
#include<iostream>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/select.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include"wrap.h"
using namespace std;
int main(int argc, char *argv[]){
//1.创建套接字socket
int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//2.创建ipv4地址,并和socket绑定
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
cout<<"setsockopt reuseaddr error!"<<endl;
Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//3.监听
Listen(listenfd,10);
//4.初始化要检测的文件描述符集合
fd_set readset,tempset;
FD_ZERO(&readset);//清理
FD_SET(listenfd,&readset);//将listenfd添加到读集合中
int nfds = listenfd+1;//监控的文件描述符集里最大文件描述符
struct sockaddr_in clientaddr;//客户端地址
socklen_t clientlen =sizeof(clientaddr);
char rec[1024]={0};
while(1){
tempset=readset;
int cnum = select(nfds,&tempset,NULL,NULL,NULL);//响应的个数
for(int i=listenfd;i<nfds;i++){//遍历集合
cout<<"轮询文件描述符:"<<i<<endl;
if(i==listenfd&&FD_ISSET(listenfd,&tempset)){//listenfd在集合中
int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
char rback[]="Connect successly!";
write(connfd,rback,sizeof(rback));
memset(rback,0,sizeof(rback));
cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
FD_SET(connfd,&readset);
nfds = connfd+1>nfds?connfd+1:nfds;
memset(&clientaddr,0,sizeof(clientaddr));
if(--cnum<=0)continue;//如果只有listenfd响应了,后面的步骤跳过
}
else{
if(FD_ISSET(i,&tempset)){
int readlen = Readline(i,rec,sizeof(rec));
if(readlen==0){//接收的数据长度为0时,退出子进程
FD_CLR(i,&readset);
Close(i);
cout<<"close a client"<<endl;
continue;
}
getpeername(i, (struct sockaddr*)&clientaddr,&clientlen);
cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port);
cout<<" "<<rec<<endl;
memset(rec,0,sizeof(rec));
memset(&clientaddr,0,sizeof(clientaddr));
}
}
}
}
return 0;
}
3 poll
原理
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
fds: 监听数组(存放pollfd的结构体)的首元素地址
nfds: 数组有效元素的最大下标+1 监控数组中有多少文件描述符需要被监控
timeout: 超时时间 毫秒级等待
0: 立即返回,不阻塞进程
-1: 阻塞, 检测的fd有变化解除阻塞
>0: 阻塞时长, 单位毫秒
返回值:
-1: 失败
>0(n): 检测的集合中有n个文件描述符发生状态变化
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};
// events事件
- POLLIN -> 检测读
- POLLOUT -> 检测写
struct pollfd myfd;
myfd.fd = 5;
myfd.events = POLLIN | POLLOUT; // 检测读写
struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

调用过程和select类似
相较于 select 而言,poll 的优势:
- 传入、传出事件分离。无需每次调用时,重新设定监听事件。
- 采用链表的方式替换原有fd_set数据结构,而使其没有连接数的上限,能监控的最大上限数可使用配置文件调整。
server.cpp
#include<iostream>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/select.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<poll.h>
#include"wrap.h"
using namespace std;
#define OPEN_MAX 100
int main(int argc, char *argv[]){
//1.创建套接字socket
int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//2.创建ipv4地址,并和socket绑定
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
cout<<"setsockopt reuseaddr error!"<<endl;
Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//3.监听
Listen(listenfd,100);
//4.poll
struct pollfd client[OPEN_MAX];
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
memset(&clientaddr,0,clientlen);
client[0].fd=listenfd;
client[0].events=POLLIN;
for(int i =1;i<OPEN_MAX;i++){
client[i].fd=-1;//初始化,fd=-1的位置不监听
}
int maxi=0;//client[]中有效位置的最大下标
char rec[1024]={0};
memset(rec,0,sizeof(rec));
while(1){
int cnum=poll(client,maxi+1,-1);
cout<<"cnum:"<<cnum<<endl;
if(client[0].revents&POLLIN){//有客户连接,为什么用&搞不懂
int connfd = Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
char rback[]="Connect successly!";
write(connfd,rback,sizeof(rback));
memset(rback,0,sizeof(rback));
cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
memset(&clientaddr,0,clientlen);
//将新连接的connfd加入client[]中
for(int i=1;i<OPEN_MAX;i++){
if(client[i].fd==-1){//找到一个最近的空位置,将新的连接放入
client[i].fd=connfd;
client[i].events=POLLIN;
maxi=maxi>i?maxi:i;
cout<<"maxi:"<<maxi<<endl;
break;
}
}
if(--cnum<=0)continue;//如果只有linstenfd响应,后面的就跳过
}
for(int i=1;i<=maxi;i++){
cout<<"轮询:"<<i<<endl;
if(client[i].fd==-1)continue;
if(client[i].revents&POLLIN){
int readlen = Readline(client[i].fd,rec,sizeof(rec));
if(readlen<0){
cout<<"read error!"<<endl;
if(--cnum<=0) break;//若只有这一件事响应,则执行完退出轮询
continue;//此处事件为读异常,后面的读事件不用执行
}
if(readlen==0){//接收的数据长度为0时,退出子进程
Close(client[i].fd);
client[i].fd=-1;
cout<<"close a client"<<endl;
if(--cnum<=0) break;//若只有这一件事响应,则执行完退出轮询
continue;//此次响应为客户端断开,后面的读事件不用执行
}
getpeername(client[i].fd, (struct sockaddr*)&clientaddr,&clientlen);
cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port);
cout<<" "<<rec<<endl;
memset(rec,0,sizeof(rec));
memset(&clientaddr,0,sizeof(clientaddr));
if(--cnum<=0) break;//若只有这一件事响应,则执行完退出轮询
}
}
}
Close(listenfd);
return 0;
}
4 epoll
int epoll_create(int size);
struct epoll_event;
typedef union epoll_data;
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);
原理
步骤:
#include<sys/epoll.h>
(1)创建红黑树模型
int epoll_create(int size);
参数 size:要监听的文件描述符上限,2.6版本后写1 即可,可自动扩展
返回 成功,返回树的句柄,失败: -1
(2)上树,下树,修改节点
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd; // 常用的一个成员
uint32_t u32;
uint64_t u64;
} epoll_data_t;
- events:
- EPOLLIN: 读事件, 检测文件描述符的读缓冲区, 检测有没有数据
- EPOLLOUT: 写事件, 检测文件描述符的写缓冲区, 检测是不是可写(有内存空间就可写)
- data.fd 为 epoll_ctl() 第三个参数的的fd的值
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- epfd: epoll_create() 函数的返回值, 找到epoll的实例
- op:
- EPOLL_CTL_ADD: 添加新节点
- EPOLL_CTL_MOD: 修改已经添加到树上的节点是属性,比如原来检测的是 读事件, 可以修改为写事件
- EPOLL_CTL_DEL: 将节点从树上删除
- fd: 要操作的文件描述符
- event:上述的结点,设置要检测的文件描述符的什么事件,结点与文件描述符绑定
返回值:成功: 0 失败: -1
(3)监听
// 这是一个阻塞函数
// 委托内核检测epoll树上的文件描述符状态, 如果没有状态变化, 该函数默认一直阻塞
// 有满足条件的文件描述符被检测到, 函数返回
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
- epfd epoll_create() 函数的返回值, 找到epoll的实例,理解为树根结点
- events: 传出参数, 里边记录了当前这轮检测epoll模型中有状态变化的文件描述符信息
- 这个参数是一个结构体数组的地址
- maxevents: 指定第二个参数 events 数组的容量
- timeout: 超时时长, 单位 ms, 和poll是一样的
- -1: 委托内核检测epoll树上的文件描述符状态, 如果没有状态变化, 该函数默认一直阻塞,有满足条件的文件描述符被检测到, 函数返回
- 0: epoll_wait() 调用之后, 函数马上返回
- >0: 委托内核检测epoll树上的文件描述符状态, 如果没有状态变化,但是timeout时间到达了,函数被强制解除阻塞
返回值:成功: 有多少文件描述符发生了状态变化
(4)返回监听变化,应用层处理
应用层对返回的 events[](有状态变化的文件描述符信息) 进行处理


跨平台: 不支持, 只支持linux
检测的连接数 和内存有关系
检测方式和效率 树状( 红黑树 )模型, 检测效率很高 内部处理是基于事件的, 和libevent是对应的
委托epoll检测的文件描述符集合用户和内核使用的是同一块内存, 没有数据的拷贝 使用了共享内存
传出的信息的量: 有多少文件描述符发送变化了 -> 返回值 可以精确的知道到底是哪个文件描述符发生了状态变化
server.cpp LT模式
#include<iostream>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include"wrap.h"
using namespace std;
/*
epoll 测试 服务器端
LT 水平工作模式
工作模式默认是水平模式,缓冲区有数据就调用一次epoll_wait
*/
int main(int argc, char *argv[]){
//1.创建套接字socket
int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//2.创建ipv4地址,并和socket绑定
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
cout<<"setsockopt reuseaddr error!"<<endl;
Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//3.监听
Listen(listenfd,100);
//4.epoll
//(1)创建epoll模型,创建红黑树
int epfd = epoll_create(1);
//(2)入树,将要检测的文件描述符入树
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listenfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
//(3)监测
struct epoll_event evs[1024];//用于接收epoll监听的返回值
struct sockaddr_in clientaddr;
socklen_t clientlen =sizeof(clientaddr);
char rec[1024]={0};
while(1){
cout<<"epoll_wait()..."<<endl;
int cnum=epoll_wait(epfd,evs,1024,-1);
cout<<"epoll_wait()完成 "<<"cnum:"<<cnum<<endl;
for(int i=0;i<cnum;i++){
//cout<<"i:"<<i<<endl;
if(!(evs[i].events&EPOLLIN))continue;//若不是读事件忽略
if(evs[i].data.fd==listenfd){//lfd响应,新客户连接
int connfd=Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
ev.data.fd=connfd;
ev.events=EPOLLIN;//工作模式默认是水平模式,缓冲区有数据就调用一次epoll_wait
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
char rback[]="Connect successly!";
write(connfd,rback,sizeof(rback));
cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
memset(&clientaddr,0,sizeof(clientaddr));
memset(rback,0,sizeof(rback));
continue;
}
else{
cout<<"read... ";
int readlen = Readline(evs[i].data.fd,rec,sizeof(rec));
cout<<"readlen="<<readlen<<endl;
if(readlen<0){cout<<"read error!";continue;}
if(readlen==0){//接收的数据长度为0时,退出子进程
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
Close(evs[i].data.fd);
cout<<"close a client"<<endl;
continue;
}
getpeername(evs[i].data.fd, (struct sockaddr*)&clientaddr,&clientlen);
cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port);
cout<<" "<<rec<<endl;
memset(rec,0,sizeof(rec));
memset(&clientaddr,0,sizeof(clientaddr));
}
cout<<endl;
}
}
Close(listenfd);
return 0;
}
epoll 的工作模式
两种工作模式:水平模式(LT),边沿模式(ET)
-
水平模式, 默认的工作的模式是水平模式
-
LT(level triggered)
-
阻塞和非阻塞的套接字都是支持的
-
阻塞指定的接收和发送数据的状态
- read/recv
- write/send
-
-
边沿模式
- ET(edge-triggered)
- 效率高, 只支持非阻塞的套接字
LT模式
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。
内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
epoll默认是水平模式 LT
/*
- 读事件:
在这种场景下, 只要是epoll_wait检测到读缓冲区有数据, 就会通知用户一次
- 不管数据有没有读完, 只要有数据就通知
- 通知就是 epoll_wait() 函数返回, 我们就可以处理传出参数中的文件描述符的状态
- 写事件:
检测写缓冲区是否可用(是否有容量), 只要是可写(有容量)epoll_wait()就会返回
*/
下面是客户端发送1234567890和abcdefg两次数据,服务器端接收rec[4] (一次最多接收4个字符),LT模式下可以看到多次调用epoll_wait(),文件描述符的读缓冲区有数据就会一直调用epoll_wait(),直至读完

ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll通知一次
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
/*
特点: epoll_wait()检测的次数变少了, 效率变高了(有满足条件的新状态才会通知)
- 读事件:
- 接收端每次收到一条(***新的***)数据, epoll_wait() 只会通知一次,不管你有没有将数据读完
- 写事件:
- 检测写缓冲区是否可用(是否有容量),通知一次
- 写缓冲区原来是不可用(满了), 后来缓冲区可用(不满), epoll_wait()检测到之后通知一次(唯一)
*/
下面是客户端发送1234567890和abcdefg两次数据,服务器端接收rec[4] (一次最多接收4个字符),ET模式下可以看到两次调用epoll_wait(),因为客户端只发送两两次数据,对应文件描述符发生两次变化

如何设置边沿模式?
// 在struct epoll_event 结构体的成员变量 events 事件中额外设置 EPOLLET
// 往epoll模型上添加新节点
int cfd = accept(lfd, NULL, NULL);
// cfd 添加到检测的原始集合中
ev.events = EPOLLIN | EPOLLET; // 设置文件描述符的边沿模式
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
通过测试如果epoll_wait()只通知一次, 并且接收端接收数据的缓存比较小, 导致服务器端通信的文件描述符中的数据越来越多, 数据如果不能全部读出, 就无法处理客户端请求, 如果解决这个问题?
// 解决方案, 在epoll_wait() 通知的这一次中, 将客户端发送的数据全部读出
- 循环的进行数据接收
- 需要使用这种方案, 但是有问题, 会导致服务器端程序的阻塞在read()
while(1)
{
int len = read(cfd, buf, sizeof(buf));
// 读完之后需要跳出循环
// 如果客户端和服务器的连接还保持着, 如果数据接收完毕, read函数阻塞
// 服务器端程序的单线程/进程的, read阻塞会导致整个服务器程序阻塞
}
- 解决上述的问题: 将数据的接收动作修改为非阻塞
- read()/recv(), write()/send()阻塞是函数行为, 还是操作的文件描述符导致的?
- 调用这些函数都是去检测操作的文件描述符的读写缓冲区 => 是文件描述符导致的
如何设置文件描述符的非阻塞?
// 使用fcntl函数设置文件描述符的非阻塞
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
// 因为文件描述符行为默认是阻塞的, 因此要追加非阻塞行为
// 获取文件描述符的flag属性
int flag = fcntl(cfd, F_GETFL);
// 给flag追加非阻塞
flag = flag | O_NONBLOCK; // 或 flag |= O_NONBLOCK;
// 将新的flag属性设置到文件描述符中
fcntl(cfd, F_SETFL, flag);
在非阻塞模式下读完数据时遇到的错误,此时应该跳出循环
// recv error: Resource temporarily unavailable -> 资源不可用, 因为内存中没有数据了
// 错误出现的原因:
#include<errno.h>
while(1){
int raadlen = read();
if(readlen==-1&&errno = EAGAIN)//read()本来阻塞在这里,这时应该跳出循环
}
循环的读数据, 当通信的文件描述符对应读缓冲区数据被读完, recv/read 不会阻塞, 继续读缓冲区
但是缓冲区中没有数据, 这时候read/recv 调用就失败了, 返回 -1, 这时候错误号 errno的值为:
errno = EAGAIN or EWOULDBLOCK , 一般情况下使用 EAGAIN 判断就可以了
server.cpp ET模式
#include<iostream>
#include<errno.h>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"
using namespace std;
/*
epoll 测试 服务器端
ET 边沿工作模式
发生变化时只调用一次epoll_wait
*/
int main(int argc, char *argv[]){
//1.创建套接字socket
int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//2.创建ipv4地址,并和socket绑定
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");//my(seirver) addr
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
cout<<"setsockopt reuseaddr error!"<<endl;
Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//3.监听
Listen(listenfd,100);
//4.epoll
//(1)创建epoll模型,创建红黑树
int epfd = epoll_create(1);
//(2)入树,将要检测的文件描述符入树
struct epoll_event ev;
ev.events=EPOLLIN|EPOLLET;
ev.data.fd=listenfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
//(3)监测
struct epoll_event evs[1024];//用于接收epoll监听的返回值
struct sockaddr_in clientaddr;
socklen_t clientlen =sizeof(clientaddr);
char rec[4]={0};
while(1){
cout<<"epoll_wait()..."<<endl;
int cnum=epoll_wait(epfd,evs,1024,-1);
cout<<"epoll_wait()完成 "<<"cnum:"<<cnum<<endl;
for(int i=0;i<cnum;i++){
//cout<<"i:"<<i<<endl;
while(1){
if(!(evs[i].events&EPOLLIN))break;//若不是读事件忽略
if(evs[i].data.fd==listenfd){//lfd响应,新客户连接
int connfd=Accept(listenfd,(struct sockaddr*)&clientaddr,&clientlen);
//因为文件描述符行为默认是阻塞的, 因此要追加非阻塞行为
//fcntl系统调用:对已打开的文件描述符进行控制操作,改变已打开文件的各种属性
int flag=fcntl(connfd,F_GETFL);//获取文件描述符的flag属性
flag |= O_NONBLOCK;//给flag追加非阻塞
fcntl(connfd,F_SETFL,flag);//将新的flag属性设置到文件描述符中
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
char rback[]="Connect successly!";
write(connfd,rback,sizeof(rback));
cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
memset(&clientaddr,0,sizeof(clientaddr));
memset(rback,0,sizeof(rback));
break;
}
else{//connfd响应的读事件
cout<<"read... ";
int readlen = Readline(evs[i].data.fd,rec,sizeof(rec));
cout<<"readlen="<<readlen<<endl;
if(readlen<0){
//缓冲区读干净了,若设置阻塞套接字时read会阻塞在这里
//设置非阻塞套接字,read会返回错误信息EAGAIN,此时跳出循环
if(errno==EAGAIN) break;
//若为其他错误,关闭套接字,下树,跳出循环
cout<<"read error!";
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
Close(evs[i].data.fd);
break;
}
if(readlen==0){//接收的数据长度为0时,退出子进程
epoll_ctl(epfd,EPOLL_CTL_DEL,evs[i].data.fd,NULL);
Close(evs[i].data.fd);
cout<<"close a client"<<endl;
break;
}
getpeername(evs[i].data.fd, (struct sockaddr*)&clientaddr,&clientlen);
cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port);
cout<<" "<<rec<<endl;
memset(rec,0,sizeof(rec));
memset(&clientaddr,0,sizeof(clientaddr));
}
}
cout<<endl;
}
}
Close(listenfd);
return 0;
}
5 client.cpp
客户端代码,select、poll、epoll相同
#include<iostream>
#include<string>
#include<string.h>
#include<stdio.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#include"wrap.h"
using namespace std;
int main(int argc,char* argv[]){
//1.make socket
int sockfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//2.make ip addr and connect
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
//void *memset(void *s,int c,size_t n)
//总的作用:将已开辟内存空间 s 的首 n 个字节的值设为值 c。
servaddr.sin_family= AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("192.168.177.128");
cout<<"Connect ..."<<endl;
Connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
char rback[1024]={0};
read(sockfd,rback,sizeof(rback));
cout<<rback<<endl;
//3.after connect
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
int iconnect =0;
while(1){
fgets(sendbuf,sizeof(sendbuf),stdin);
Writen(sockfd,sendbuf,strlen(sendbuf));
memset(sendbuf,0,sizeof(sendbuf));
}
Close(sockfd);
return 0;
}
6 select、poll、epoll总结
时间复杂度
| select、poll、epoll时间复杂度 |
|---|
| select==>时间复杂度O(n) 它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。 |
| poll==>时间复杂度O(n) poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的. |
| epoll==>时间复杂度O(1) epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1)) |
同步I/O
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
如何选择
1、epoll是Linux所特有,select则应该是POSIX所规定,windows、linux都有
2、多连接,少活跃,选择epoll
3、在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,因为epoll的通知机制需要很多函数回调。
4、select低效是因为每次它都需要轮询,但低效也是相对的,视情况而定,也可通过良好的设计改善
7 epoll反应堆
原理
前面的epoll对应每个事件的发生,都需要去处理(如果是IO事件,处理较慢,相当于阻塞),epoll反应堆的思想就是当某个时间发生了,自动的去处理这 个事件(先从内核区拷贝到用户区,后面可以用户再处理)
这样的思想对我们的编码来说就是设置回调,将文件描述符,对应的事件,和事件产生时的处理函数封装 到一起,这样当某个文件描述符的事件发生了,回调函数会自动被触发,这就是所谓的反应堆思想。

epoll反应堆流程

一般我们先会给epoll_event中的data进行赋值,然后再epoll_wait返回后取出来进行使用
切记不要同时给data.fd和data.ptr赋值,会覆盖,先赋值的那个将无意义,data.fd和data.ptr只能用其中一个
//**epoll反应堆流程**
epoll_create(); // 创建监听红黑树
epoll_ctl(); // 向书上添加监听fd
epoll_wait(); // 监听
有客户端连接上来--->lfd调用acceptconn()--->将cfd挂载到红黑树上监听其读事件--->
epoll_wait()返回cfd--->cfd回调recvdata()--->将cfd摘下来监听写事件--->...--->
epoll_wait()返回cfd--->cfd回调senddata()--->将cfd摘下来监听读事件--->...--->
//目前的代码是只监听读事件
epoll反应堆 server.cpp
//自定义结构体
struct myevent{
int fd;
int events;//要处理的时间
void *arg;//指向自己结构体指针
void (*call_back)(int fd,void *arg);//回调函数,函数指针
int status;//0:未上树,1:已上树
char buf[BUFLEN];
int len;
};
//创建一个myevent结构体
void set_myevent(struct myevent *myev,int fd,void (*call_back)(int fd,void *arg),int events);
//构造一个对应myevent的内核结构体epoll_event,并将epoll_event上树
void eventadd(int efd,struct myevent *myev);
//从epoll红黑树上删除一个epoll_event结构体,下树,并修改对应的myevent
void eventdel(int efd,struct myevent *myev);
//当listenfd文件描述符就绪, 调用该函数完成与客户端的连接
void acceptconn(int lfd,void *arg);
//当connfd文件描述符就绪,且为读事件,调用此函数完成读
void recvdata(int fd,void *arg);
//当connfd文件描述符就绪,且为写事件,调用此函数完成写
void senddata(int fd,void *arg);
#include<iostream>
#include<errno.h>
#include<string>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/times.h>
#include<unistd.h>
#include<stdlib.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include"wrap.h"
using namespace std;
//epoll reactor 测试epoll反应堆 服务器端
#define MAX_EVENTS 1024 //监听数上限
#define BUFLEN 1024 //buf[]数组长度,缓冲区大小
struct myevent{
int fd;
int events;//要处理的时间
void *arg;//指向自己结构体指针
void (*call_back)(int fd,void *arg);//回调函数,函数指针
int status;//0:未上树,1:已上树
char buf[BUFLEN];
int len;
long last_active;
};
//全局变量
int efd;//红黑树树根
struct myevent myevs[MAX_EVENTS+1];//与epoll_event对应的自定义结构体数组,用来调用回调函数
void recvdata(int fd,void *arg);
void senddata(int fd,void *arg);
//创建一个myevent结构体
void set_myevent(struct myevent *myev,int fd,void (*call_back)(int fd,void *arg),int events){
myev->fd=fd;
myev->events=events;
myev->arg=myev;//指向自己结构体本身的指针
myev->call_back=call_back;
myev->status=0;//0表示未上树
memset(myev->buf,0,sizeof(myev->buf));//清空buf
myev->len=0;//长度置0
myev->last_active=time(NULL);
}
//构造一个对应myevent的内核结构体epoll_event,并将epoll_event上树
void eventadd(int efd,struct myevent *myev){
struct epoll_event ev;
//data.fd和data.ptr只能用其中一个,不要两个都赋值了
ev.data.ptr = myev;//ev的ptr指针指向myev结构体
ev.events=myev->events;
if(myev->status==0){//如果myev的status为0,表示未上树
if(epoll_ctl(efd,EPOLL_CTL_ADD,myev->fd,&ev)<0)//将ev上树
cout<<"Add ev to efd error!"<<endl;
else cout<<"Add ev successly!"<<endl;
}
myev->status=1;
}
//从epoll红黑树上删除一个epoll_event结构体,下树,并修改对应的myevent
void eventdel(int efd,struct myevent *myev){
struct epoll_event ev;
if(myev->status!=1) return;//myev并没所有上树,不处理
ev.data.ptr=NULL;
myev->status=0;
if(epoll_ctl(efd,EPOLL_CTL_DEL,myev->fd,NULL)<0)
cout<<"Delete ev error!"<<endl;
else cout<<"Delete ev successly!"<<endl;
}
//当listenfd文件描述符就绪, 调用该函数完成与客户端的连接
void acceptconn(int lfd,void *arg){
struct sockaddr_in clientaddr;
socklen_t len =sizeof(clientaddr);
int connfd;
connfd=Accept(lfd,(struct sockaddr*)&clientaddr,&len);
int i;
//从下标0开始寻找一个未上树的结构体myevent
for(i=0;i<MAX_EVENTS;i++){
if(myevs[i].status==0)break;
}
if(i==MAX_EVENTS){
cout<<"over max connect limit"<<endl;
return;
}
fcntl(connfd,F_SETFL,O_NONBLOCK);
set_myevent(&myevs[i],connfd,recvdata,EPOLLIN|EPOLLET);//创建myevent结构体
eventadd(efd,&myevs[i]);//上树
char rback[]="connect successly!";
write(connfd,rback,sizeof(rback));
cout<<rback<<endl;
cout<<"New Connect"<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
memset(&clientaddr,0,sizeof(clientaddr));
memset(rback,0,sizeof(rback));
}
//当connfd文件描述符就绪,且为读事件,调用此函数完成读
void recvdata(int fd,void *arg){
struct myevent *myev = (struct myevent*)arg;
int readlen=Readline(fd,myev->buf,sizeof(myev->buf));
cout<<"readlen:"<<readlen<<endl;
struct sockaddr_in clientaddr;
socklen_t clientlen =sizeof(clientaddr);
getpeername(myev->fd, (struct sockaddr*)&clientaddr,&clientlen);
if(readlen<0){
eventdel(efd,myev);
Close(myev->fd);
return;
}
if(readlen==0){
eventdel(efd,myev);
Close(myev->fd);
cout<<"Client Close:";
cout<<" ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port)<<endl;
memset(&clientaddr,0,sizeof(clientaddr));
return;
}
cout<<"From ip:"<<inet_ntoa(clientaddr.sin_addr);
cout<<" port:"<<ntohs(clientaddr.sin_port);
cout<<" "<<myev->buf<<endl;
memset(&clientaddr,0,sizeof(clientaddr));
}
//当connfd文件描述符就绪,且为写事件,调用此函数完成写
void senddata(int fd,void *arg){
}
int main(int argc, char *argv[]){
//1.创建套接字socket
int listenfd = Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//2.创建ipv4地址,并和socket绑定
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("192.168.109.128");//my(seirver) addr
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//本机的任意地址
int on = 1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)
cout<<"setsockopt reuseaddr error!"<<endl;
fcntl(listenfd, F_SETFL, O_NONBLOCK);//设置非阻塞
Bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//3.监听
Listen(listenfd,100);
//epoll 反应堆
efd=epoll_create(MAX_EVENTS+1);
struct epoll_event evs[MAX_EVENTS + 1];//用来接收epoll_wait返回的事件
//构建listenfd对应的myevent结构体,回调函数为acceptconn
set_myevent(&myevs[MAX_EVENTS],listenfd,acceptconn,EPOLLIN|EPOLLET);
//将listenfd对应的myevent对应到epoll_event,再将其上树
eventadd(efd,&myevs[MAX_EVENTS]);
while(1){
cout<<"wait..."<<endl;
int cnum=epoll_wait(efd,evs,MAX_EVENTS+1,-1);
cout<<"wait successly, cnum:"<<cnum<<endl;
for(int i=0;i<cnum;i++){
struct myevent *myev = (struct myevent*)evs[i].data.ptr;
if(evs[i].events & EPOLLIN){
myev->call_back(myev->fd,myev);
}
if(evs[i].events & EPOLLOUT){
myev->call_back(myev->fd,myev);
}
cout<<endl;
}
}
Close(listenfd);
return 0;
}
8 epoll线程池
线程池是一个抽象概念,可以简单的认为若干线程在一起运行,线程不退出,有任务时线程执行任务,无任务时等待。
为什么要有线程池?
- 利用多线程处理高并发时,对于多个请求每次都去建立线程,这样线程的创建和销毁也会有很大的系统开销,使用上效率很低。
- 创建线程并非多多益善,所以提前创建好若干个线程,不退出,等待任务的产生,去接收任务处理后等待下一个任务。
**线程池如何实现?**需要思考 2 个问题?
- 假设线程池创建了,线程们如何去协调接收任务并且处理?
- 线程池上的线程如何能够执行不同的请求任务?
上述**问题 1 解决思路是操作系统资源分配思路,助互斥锁和条件变量来搞定。**就很像我们之前学过的生产者和消费者模型,客户端对应生产者,服务器端这边的线程池对应消费者,再用互斥锁和条件变量来搞定。 问题 2 解决思路就是利用回调机制,我们同样可以借助结构体的方式,对任务进行封装,比如任务的数据和任 务处理回调都封装在结构体上,这样线程池的工作线程拿到任务的同时,也知道该如何执行了。

任务队列中的任务包含了要执行的事件,及其回调函数等信息。
本文探讨了epoll反应堆技术在服务器中的高效应用,对比了与select/poll的差异,并引入线程池的概念,以提升高并发场景下的处理能力。通过实例展示了如何利用epoll事件驱动和线程池来管理连接,提升服务器响应效率。
726

被折叠的 条评论
为什么被折叠?



