前言
即使使用ET模式,一个socket上的同一事件还是可能被触发多次(将TCP缓冲区设置的小一点,发送一个较大的数据,接受的数据会多次填满缓冲区,导致会被触发多次),,当某个线程在读取完某个socket的数据后开始处理这些数据,但在处理这些数据的时候这个socket上又有数据来了,此时另外一个线程被唤醒来读取这些新的数据,于是出现了两个线程同时操作一个socket的局面。但我们期望一个socket在任意时刻值被一个socket处理。此时可以用epoll的EPOLLONESHOT事件来解决。
引入
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或者异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上EPOLLONESHOT事件。
使用
注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能触发,进而让其他线程有机会继续处理这个socket.
代码实现
epolloneshot.cpp
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <iostream>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 200
struct fds
{
int epollfd;
int sockfd;
};
//设置非阻塞
int setnonblocking(int fd)
{
int old_flag = fcntl(fd,F_GETFL);
int new_flag = old_flag | O_NONBLOCK;
fcntl(fd,F_SETFL,new_flag);
return old_flag;
}
//向epollfd注册文件描述符,并添加EPOLLONESHOT事件
void addfd(int epollfd,int fd,bool oneshot)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN|EPOLLET;
if(oneshot)
{
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&event);
setnonblocking(fd);
}
//重置EPOLLONESHOT事件
void reset_oneshot(int epollfd,int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN|EPOLLOUT|EPOLLONESHOT;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}
//工作线程
void* worker(void *arg)
{
int sockfd = ((fds*)arg)->sockfd;
int epollfd = ((fds*)arg)->epollfd;
printf("start new thread to recive data on fd:%d\n",sockfd);
char buf[BUFFER_SIZE];
memset(buf,'\0',BUFFER_SIZE);
//循环读取sockfd上的数据,直到遇到EAGAIN错误
while(1)
{
memset(buf,'\0',sizeof(buf));
int ret = recv(sockfd,buf,BUFFER_SIZE-1,0);
if(ret == 0)
{
close(sockfd);
printf("client closed the connection\n");
break;
}
else if(ret < 0)
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
reset_oneshot(epollfd,sockfd);
printf("read later\n");
break;
}
}
else
{
printf("get content:%s\n",buf);
sleep(5);
}
}
printf("end thread reciving data on fd:%d\n",sockfd);
}
int main(int argc,char** argv)
{
if(argc <= 2)
{
printf("usage:%s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr.s_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET,SOCK_STREAM,0);
assert(listenfd >= 0);
ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
assert(ret >= 0);
ret = listen(listenfd,5);
assert(ret >= 0);
//复用地址和端口号
int on = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(char*)&on,sizeof(on));
setsockopt(listenfd,SOL_SOCKET,SO_REUSEPORT,(char*)&on,sizeof(on));
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd >= 0);
//注意,监听socket上时不能注册EPOLLONESHOT事件的,因为后续的客户连接请求将不会触发EPOLLIN
addfd(epollfd,listenfd,false);
while(1)
{
int ret = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
if(ret < 0)
{
printf("epoll failure");
break;
}
for(int i = 0;i < ret;i++)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength;
int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
//更改缓冲区大小,模拟ET模式下一个事件触发多次
int in = 200;
setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,(void*)&in,sizeof(in));
//注册EPOLLONESHOT事件;
//addfd(epollfd,connfd,false);
addfd(epollfd,sockfd,true);
}
else if(events[i].events & EPOLLIN)
{
pthread_t thread;
fds new_worker;
new_worker.epollfd = epollfd;
new_worker.sockfd = sockfd;
pthread_create(&thread,NULL,worker,(void*)&new_worker);
}
else
{
printf("something else happened\n");
}
}
}
close(listenfd);
return 0;
}
client_tess.cpp
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;
int main(int argc,char** argv)
{
if(argc <= 2)
{
printf("usage:%s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in ser_addr;
bzero(&ser_addr,sizeof(ser_addr));
ser_addr.sin_family = AF_INET;
inet_pton(AF_INET,ip,&ser_addr.sin_addr.s_addr);
ser_addr.sin_port = htons(port);
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd >= 0);
if(connect(sockfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) < 0)
{
cout<<"error"<<endl;
}
else
{
string str(2000,'a');
int ret = send(sockfd,str.c_str(),str.size(),0);
cout<<"ret = "<<ret<<endl;
}
return 0;
}
通过缩小接受缓冲区的大小,发送一个远远大于接受缓冲区的数据来模拟多个线程同时处理一个socket的现象,通过开启EPOLLONESHOT避免这种情况。
要点
- 处理fd的读事件,一直读一直读,读到没有数据 ( errno==EAGAIN||errno == EWOULDBLOCK) ,这时才重置fd上的事件 。
- 重置fd上的事件,这样尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次。如果不进行重置,下一次读事件到来时就不会触发EPOLLIN.
- 监听描述符listenfd不能注册EPOLLONESHOT,否则只能处理一个客户连接
- 更改缓冲区的大小setsockopt(sockfd,SOL_SOCKET,SO_RCVBUF,(void*)&in,sizeof(in));
- 非阻塞IO下的收发包逻辑
重置代码
void reset_oneshot(int epollfd,int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN|EPOLLOUT|EPOLLONESHOT;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&event);
}