请参考上一篇博客:
epoll为什么更高效 ? epoll涉及的数据结构?
1、事件
可读事件
,当文件描述符关联的内核读缓冲区可读,则触发可读事件。
(可读:内核缓冲区非空,有数据可以读取)
可写事件
,当文件描述符关联的内核写缓冲区可写,则触发可写事件。
(可写:内核缓冲区不满,有空闲空间可以写入)
2、通知机制
通知机制,就是当事件发生的时候,则主动通知。通知机制的反面,就是轮询机制。
3、socket 和 文件描述符之间的关系
套接字也是文件。具体数据传输流程如下:
当server端
监听到有连接时,应用程序会请求内核创建Socket;
Socket创建好后会返回一个文件描述符
给应用程序;
当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口
等在内核维护的一个ipcb双向链表
中找到对应的Socket,并将数据包赋值到该Socket的缓冲区
;
应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据
注意:
操作系统针对不同的传输方式(TCP,UDP)会在内核中各自维护一个Socket双向链表,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口从对应的链表中找到其对应的Socket,并会将数据拷贝到Socket的缓冲区,等待应用程序读取。
想了解文件描述符(fd)是什么请参考此链接:
文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?
4、服务器文件传输过程:server(服务器) ——》 client(客户)
当然:服务器接收客户的请求,红色箭头方向相反。
5、内核接收网络数据全过程
1、计算机收到了对端传送的数据(步骤①);
2、数据经由网卡传送到内存(步骤②);
3、然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能;
注:
蓝色区域
里面的等待队列
:就是用户空间进程调用recv函数(读取数据)
请求读取内核缓冲区内的数据,由于缓冲区数据没有准备好,所以处于等待状态(又称为阻塞状态)。
你有这样的疑问吗?操作系统如何知道网络数据对应于哪个socket?
因为一个socket对应着一个端口号
,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。
如何同时监视多个socket的数据?
哈哈哈,下面就该引出epoll了。epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
epoll_create的作用
文件描述符
,来唯一标识内核中的这个事件表
。 这个文件描述符使用如下epoll_create函数来创建;file结点(epoll_create创建的文件描述符)
,在内核cache里建了个 红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.(概括就是:调用epoll_create方法时,内核会跟着创建一个eventpoll对象)eventpoll
对象也是文件系统中的一员,和socket一样,它也会有等待队列。epoll
的eventpoll对象
是必须的,因为内核要维护“就绪列表”
等数据,“就绪列表”可以作为eventpoll
的成员。struct eventpoll
{
spin_lock_t lock; //对本数据结构的访问
struct mutex mtx; //防止使用时被删除
wait_queue_head_t wq; //sys_epoll_wait() 使用的等待队列
wait_queue_head_t poll_wait; //file->poll()使用的等待队列
struct list_head rdllist; //事件满足条件的链表
struct rb_root rbr; //用于管理所有fd的红黑树
struct epitem *ovflist; //将事件到达的fd进行链接起来发送至用户空间
}
总结:
epoll_create创建额外的文件描述符
,来唯一标识内核中的这个内核事件表(eventpoll对象)
epoll_create函数原型
该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
#include
int epoll_create(int size);
返回值:
epollfd
参数说明:
需要注意的是:
这个文件描述符也会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到这个fd,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽
。
epoll_ctl的作用
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket
。以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
注意:
当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。
epoll_ctl函数原型
操作epoll的内核事件表
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
返回值:
参数说明:
1、epfd
:epoll_create()的返回值;2、op
: 指定操作类型。操作类型有如下3种:
EPOLL_CTL_ADD
:注册新的fd到epfd中;EPOLL_CTL_MOD
:修改已经注册的fd的监听事件;EPOLL_CTL_DEL
:从epfd中删除一个fd;3、fd
:是需要监听的fd4、event
:告诉内核需要监听什么事,指定事件,它是epoll_event
结构指针类型。
epoll_event
的定义如下:struct epoll_event {
__uint32_t events; /* Epoll 事件 */
epoll_data_t data; /* 用户数据 */
};
其中events
成员描述事件类型,具体如下:
事件 | 描述 |
---|---|
EPOLLIN | 可读取非高优先级数据(重要,必用) |
EPOLLPRI | 可读取高优先级数据 |
EPOLLOUT | 普通数据可写 (重要,必用) |
EPOLLHUP | 本端描述符产生一个挂断事件,默认监测事件 |
EPOLLET | 采用边沿触发事件通知(重要) |
EPOLLONESHOT | 在完成事件通知后禁用检查 |
EPOLLRDHUP | 套接字对端关闭 |
EPOLLERR | 有错误发生 |
data
成员用于存储用户数据,其类型epoll_data_t
的定义如下:
typedef union epoll_data
{
void* ptr; //指定与fd相关的用户数据
int fd; //指定事件所从属的目标文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_data_t是一个共用体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。
epoll_wait的作用
当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket,而不是像select轮询所有的sock。
eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。
当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程,如下:
epoll_wait函数原型
它在一段超时时间内等待一组文件描述符上的事件,阻塞等待注册的事件发生,返回事件的数目,其原型如下:
#include
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
返回值:
就绪
的文件描述符个数
,失败时返回-1并设置errno。参数说明:
1、epfd
:epoll_create()的返回值;2、events
: 用来记录被触发的events(结构参考epoll_ctl),其大小受制于maxevents3、maxevents
: 设定最多监听多少个事件,必须大于0,一般设定为655354、timeout
:在函数调用中阻塞时间上限,单位是ms
timeout = -1
:表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;timeout = 0
:用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;timeout > 0
:表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。timeout
参数设计技巧问题?
将其设置为1
,但还没完,因为即使这样设置,处理其它任务时,在每次循环都会在这浪费1ms的阻塞时间,多次循环后性能损失就比较明显了。
epoll再添加一个fd
,我们有其它任务要执行时直接向该fd随便写入一个字节
,将epoll唤醒从而跳过阻塞时间
。没任务时epoll超过阻塞时间1ms也会自动挂起,不会占用cpu,两全其美。Level Triggered (LT)
水平触发
socket
接收缓冲区不为空
,说明有数据可读, 读事件一直触发socket
发送缓冲区不满
,说明可以继续写入数据 ,写事件一直触发LT的处理过程:
accept
一个连接,添加到epoll
中监听EPOLLIN事件
.(注意这里没有关注EPOLLOUT事件)EPOLLIN事件
到达时,read fd
中的数据并处理 .write到fd
中;如果数据较大,无法一次性写出,那么在epoll
中监听EPOLLOUT事件
.EPOLLOUT
事件到达时,继续把数据write到fd
中;如果数据写出完毕,那么在epoll
中关闭EPOLLOUT事件
//LT模式的工作流程
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
char buf[ BUFFER_SIZE ];
for ( int i = 0; i < number; i++ )
{
int sockfd = events[i].data.fd;
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd, false );
}
else if ( events[i].events & EPOLLIN )
{
//只要socket读缓存中还有未读出的数据,这段代码就被触发
printf( "event trigger once\n" );
memset( buf, '\0', BUFFER_SIZE );
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret <= 0 )
{
close( sockfd );
continue;
}
printf( "get %d bytes of content: %s\n", ret, buf );
}
else
{
printf( "something else happened \n" );
}
}
}
Edge Triggered (ET)
边沿触发
socket
的接收缓冲区状态变化
时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件(从无到有)socket
的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件(从有到无)ET的处理过程:
accept
一个一个连接,添加到epoll中监听EPOLLIN|EPOLLOUT
事件()EPOLLIN事件
到达时,read fd
中的数据并处理,read
需要一直读,直到返回EAGAIN
为止write到fd
中,直到数据全部写完,或者write
返回EAGAIN
EPOLLOUT
事件到达时,继续把数据write到fd
中,直到数据全部写完,或者write
返回EAGAIN
从ET的处理过程中可以看到,ET的要求是需要一直读写,直到返回EAGAIN,否则就会遗漏事件。而LT的处理过程中,直到返回EAGAIN不是硬性要求,但通常的处理过程都会读写直到返回EAGAIN,但LT比ET多了一个开关EPOLLOUT事件的步骤
当我们使用ET模式的epoll时,我们应该按照以下规则设计:
非阻塞的(非常重要)
。
循环(ET里面采用while循环,看清楚呦,LE是if判断)
来对文件描述符执行尽可能多的I/O,而文件描述符又被设置为可阻塞的,那么最终当没有更多的I/O可执行时,I/O系统调用就会阻塞。基于这个原因,每个被检查的文件描述符通常应该置为非阻塞模式,在得到I/O事件通知后重复执行I/O操作
,直到相应的系统调用(比如read(),write())以错误码EAGAIN或EWOULDBLOCK的形式失败。//ET模式的工作流程
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
char buf[ BUFFER_SIZE ];
for ( int i = 0; i < number; i++ )
{
int sockfd = events[i].data.fd;
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd, true );
}
else if ( events[i].events & EPOLLIN )
{
//这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出
printf( "event trigger once\n" );
while( 1 )
{
memset( buf, '\0', BUFFER_SIZE );
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret < 0 )
{
//对于非阻塞IO,下面条件成立表示数据已经全部读取完毕。
//此后,epoll就能再次触发sockfd上的EPOLLIN事件,已驱动下一次读操作
if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
{
printf( "read later\n" );
break;
}
close( sockfd );
break;
}
else if( ret == 0 )
{
close( sockfd );
}
else
{
printf( "get %d bytes of content: %s\n", ret, buf );
}
}
}
else
{
printf( "something else happened \n" );
}
}
}
Server.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SOCKET_PORT 9999
#define EPOLL_SIZE 10
int main(int argc, char *argv[])
{
int server_fd;
int client_fd;
int epfd;
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
if (argc == 2){
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(SOCKET_PORT);
}else if(argc==3){
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
}else{
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(SOCKET_PORT);
}
server_fd = socket(PF_INET,SOCK_STREAM,0);
if (server_fd < 0){
std::cout<<"Create Socket Errors, "<<server_fd<<std::endl;
exit(server_fd);
}
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0)
{
perror("bind error");
return -1;
}
if (listen(server_fd, 10)<0)
{
perror("listen error");
return -1;
}else{
printf("Stared the Socket: %s:%d\n",
inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port));
}
epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0)
{
perror("Epoll Create");
exit(-1);
}
struct epoll_event ev;
struct epoll_event events[EPOLL_SIZE];
ev.data.fd = server_fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
fcntl(server_fd, F_SETFL, fcntl(server_fd, F_GETFD, 0)| O_NONBLOCK);
while (1)
{
int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (epoll_events_count < 0)
{
perror("epoll failed");
break;
}
printf("epoll_events_count = %d\n", epoll_events_count);
for (int i=0;i < epoll_events_count;i++)
{
if (events[i].data.fd==server_fd&&(events[i].events & EPOLLIN))
{
struct sockaddr_in remote_addr;
socklen_t socklen = sizeof(struct sockaddr_in);
client_fd = accept(server_fd, (struct sockaddr *)&remote_addr, &socklen);
if (client_fd < 0)
{
perror("accept error");
}
printf("client connection from: %s : % d(IP : port), "
"clientfd = %d \n",
inet_ntoa(remote_addr.sin_addr),
ntohs(remote_addr.sin_port),
client_fd);
ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
fcntl(client_fd, F_SETFL,
fcntl(client_fd, F_GETFD, 0)| O_NONBLOCK);
send(client_fd,"Welcome to My server",21,0);
}else{
char buffer[1024];
int len;
if ((len=recv(events[i].data.fd,buffer,sizeof(buffer), 0))>0)
{
send(events[i].data.fd,"Welcome to My server\n",21,0);
printf("%s fd %d \n",buffer,events[i].data.fd);
//close(client_fd);
}else{
printf("client offline with: "
"clientfd = %d \n",
events[i].data.fd);
}
}
}
}
close(server_fd);
close(epfd);
return 0;
}
Client.cpp
#include
#include
#include
#include
#include
#include
#include
#include
//#include //menset
#include
#include
#define EPOLL_SIZE 10
int main(int argc, char *argv[])
{
int size;
int client_fd;
struct sockaddr_in addr;
memset(&addr,0,sizeof(addr));
addr.sin_family = AF_INET;
if (argc == 3){
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(atoi(argv[2]));
}else{
printf("The host ip and port is falied.\n");
exit(-1);
}
client_fd = socket(PF_INET,SOCK_STREAM,0);
if (client_fd < 0){
std::cout<<"Create Socket Errors, "<<client_fd<<std::endl;
exit(client_fd);
}
size = connect(client_fd, (struct sockaddr *)&addr,
sizeof(struct sockaddr));
if (size < 0)
{
perror("connect error");
}
printf("Connect client: %s:%d \n",inet_ntoa(addr.sin_addr),
ntohs(addr.sin_port));
int epfd;
epfd = epoll_create(EPOLL_SIZE);
struct epoll_event ev;
ev.data.fd = client_fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
fcntl(client_fd, F_SETFL, fcntl(client_fd, F_GETFD, 0)| O_NONBLOCK);
char buffer[1024];
struct epoll_event events[EPOLL_SIZE];
int len;
while (1)
{
int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (epoll_events_count<0)
{
perror("Epoll wait");
exit(-1);
}
for (int i=0;i<epoll_events_count;i++)
{
printf("client fd %d, event fd %d \n",client_fd,
events[i].data.fd);
len = recv(client_fd, buffer, sizeof(buffer), 0);
if (len > 0)
{
printf("%s",buffer);
}
sleep(3);
send(client_fd,"Welcome to My client",21,0);
}
}
return 0;
}
部分函数说明
atoi 将字符串转化为int
int atoi (const char * str)
atol 将字符串转化为long
long int atol ( const char * str )
参考上一篇博客:Epoll常见面试问题,LT和ET区别?将socket设为非阻塞?正确的读写方式?关于epoll的数据结构?epoll与select、poll的对比?
1、https://www.cnblogs.com/kefeiGame/p/7246942.html
2、https://zhuanlan.zhihu.com/p/21374980
3、https://www.cnblogs.com/lojunren/p/3856290.html
4、https://bbs.gameres.com/thread_842984_1_1.html
5、https://blog.csdn.net/ChaseRaod/article/details/79361025
6、https://www.zhihu.com/question/23614342/answer/184513538