在socket网络编程为了避免“Broke Pipe”
,所以我们第一件事就应该处理SIG_PIPE
避免程序退出
broken pipe经常发生socket关闭之后(或者其他的描述符关闭之后)的write操作中,此时进程会收到
SIGPIPE
信号,默认动作是进程终止
signal(SIGPIPE,SIG_IGN)
谈及EPOLL首先必定涉及LT
和ET
的工作模式。在实际处理过程中,ET得效率高于LT,但是选择符合自己的才是最好的。下面给出两种的工作处理模式。
LT
LT:水平触发:
对于EPOLLIN
:只要数据可读则会一直触发EPOLLIN
直至用户将所有数据读取完毕。例如socket收到:ABCDEF,此时用户一次读取两个字符,则系统会通知用户3次,用于依次读取:AB->CD->EF,直至读缓冲区为空。
对于EPOLLOUT
:只要socke可写,则会一直触发EPOLLOUT
事件
读取数据处理逻辑:
if(events[i].events & EPOLLIN)
{
int count = read(events[i].data.fd,szBuffer,MAX_BUF);
//下面进行组包拆包即可
}
ET
ET:边沿触发,比LT效率高。
对于EPOLLIN
:只有socket上的数据从无到有,EPOLLIN
才会触发,此时用户需要一次性将数据读取完毕,否则将导致数据延后甚至阻塞。例如socket收到:ABCDEF,此时用户一次只读取2个字符,那么当用户读取AB之后,系统并不会再次通知用户去读取CDEF,此时数据CDEF只能等待下次数据再次可读之后才能被取出。此时就会导致数据延迟并且导致缓冲区数据阻塞。
对于EPOLLOUT
:只有在socket写缓冲区从不可写变为可写,EPOLLOUT 才会触发(刚刚添加事件完成调用epoll_wait时或者缓冲区从满到不满)
读取数据处理逻辑:一次性读取所有数据
if(events[i].events & EPOLLIN)
{
//循环读取数据,直至所有数据读取完毕
while(true)
{
int count = read(events[i].data.fd,szBuffer,MAX_BUF);
if(count == -1)
{
if(errno == EINTER )
{
continue;
}
else if(errno == EWOULDBLOCK)
{
break;
}
//读数据出错,进行错误处理
break;
}
//套接字关闭了
if(count == 0)
{
break;
}
//数据读取完毕
if(count < MAX_BUF)
{
break;
}
}
//下面进行组包拆包即可
}
EPOLL事件类型
事件 | 描述 |
---|---|
EPOLLIN | 数据可读时触发(包括普通数据和优先数据) |
EPOLLOUT | 数据可写时触发 |
EPOLLPRI | 优先数据可读,eg:tcp的带外数据 |
EPOLLRDHUP | tcp链接被对方关闭,或者关闭了些操作。由GNU引入 |
EPOLLERR | 错误引起 |
EPOLLHUP | 挂起,比如管道的写端被关闭后 |
EPOLLNVAL | 文件描述符没打开 |
EPOLLET | 边缘触发模式 |
这些事件中我们往往只需要关注EPOLLIN
进行读取数据即可。至于EPOLLOUT
往往不会被设置。如果在LT
模式设置了EPOLLOUT
该事件,只要缓冲区可写则会一直触发,容易导致cpu浪费。但是如果用户自己维护了数据发送缓冲区,则可以在该事件中进行数据发送,当数据发送完毕之后应该去掉该标记,避免cpu浪费,例如:解决short write问题
。
EPOLLONESHOT事件
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或异常事件,且只能触发一次。当事件(读,写,error)处理完成之后应该重置该事件。否则系统会认为上一次事件(读,写,error)没完成,后面的事件(读,写,error)将永远不会触发
如果我们在多线程开发中,建议设置该事件,否则可能导致很多意想不到的错误。例如在多线程中我们处理accpet
操作。
if (events[i].data.fd == listen_sock)
{
int client_sock = accept(listen_sock,(struct sockaddr *)&client_addr,&addr_len);
if(client_sock == -1)
{
printf("[%d]accept error:%s\r\n",thread_id, strerror(errno));
continue;
}
intf("[%d]socket[%d] connected\r\n",thread_id,client_sock);
}
此时可能多个线程都会触发这段代码,但是只有一个线程能执行成功,其他的将报告Resource temporarily unavailable
。虽然我们可以忽略这段代码,但是并不是我们期望的。我们期望的是只触发一次。同样的对于读事件,虽然读可以保证read的原子性,但是多线程读取的数据顺序我们没办法保证。例如:客户端发送数据ABCDEF,服务端3个线程进行读取数据,每个线程每次读取两个字符。此时结果可能是,线程A:AB,线程B:EF,线程C:CD,此时数据顺序混乱了,导致数据错误。所以如果在多线程中操作socket的时候我们一定要设置EPOLLONESHOT
并在事件操作完之后重置。重置的时候尽可能的在处理事件的线程中处理,避免在其他线程中重置,否则会导致线程不安全,如果一定要在其他线程中重置,则需要开发者自行保证事件对应的线程安全性
。
void resetOneshotFlag(int epollfd,int fd)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLONESHOT;
if(-1 == epoll_ctl(epollfd, EPOLL_CTL_MOD,fd,&event))
{
printf("modifyfd error:%s\r\n", strerror(errno));
}
}
多线程版本Epoll demo
- 处理SIGPIPE信号,并将socket设置为非阻塞模式
- 将listen socket 设置EPOLLONESHOT并加入epoll
- 多线程
epoll_wait
,等到事件处理完成之后重置EPOLLONESHOT。
#include<stdio.h>
#include<iostream>
#include<sys/socket.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<string.h>
#include<mutex>
#include<thread>
#include <sys/syscall.h>
#include <signal.h>
void modifyfd(int epollfd,int fd,bool oneshot,bool notify_write);
const int SEND_BUF_MAX = 10240;
void setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);
}
void addfd(int epollfd,int fd,bool oneshot,bool notify_write)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if(oneshot)
{
event.events |= EPOLLONESHOT;
}
if(notify_write)
{
event.events |= EPOLLOUT;
}
if(-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD,fd,&event))
{
printf("addfd error:%s\r\n", strerror(errno));
}
}
void removefd(int epollfd,int fd)
{
if(-1 == epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL))
{
printf("removefd error:%s\r\n", strerror(errno));
}
}
void modifyfd(int epollfd,int fd,bool oneshot,bool notify_write)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if(oneshot)
{
event.events |= EPOLLONESHOT;
}
if(notify_write)
{
event.events |= EPOLLOUT;
}
if(-1 == epoll_ctl(epollfd, EPOLL_CTL_MOD,fd,&event))
{
printf("modifyfd error:%s\r\n", strerror(errno));
}
}
int handle_accepter_event(int thread_id,int epoll_id,int listen_sock)
{
const int MAX_EVENTS = 10;
struct epoll_event events[MAX_EVENTS];
while (1)
{
int ret = epoll_wait(epoll_id,events,MAX_EVENTS,1000);
if(ret == -1)
{
if(errno == EINTR)
{
continue;
}
printf("[%d]epoll_wait error:%s\r\n",thread_id, strerror(errno));
return -1;
}
for (size_t i = 0; i < ret; i++)
{
//listen_sock
if (events[i].data.fd == listen_sock)
{
struct sockaddr_in client_addr = {0};
socklen_t addr_len = sizeof(client_addr);
int client_sock = accept(listen_sock,(struct sockaddr *)&client_addr,&addr_len);
modifyfd(epoll_id,events[i].data.fd,true,false);
if(client_sock == -1)
{
printf("[%d]accept error:%s\r\n",thread_id, strerror(errno));
continue;
}
printf("[%d]socket[%d] connected\r\n",thread_id,client_sock);
addfd(epoll_id,client_sock,true,false);
}
else if(events[i].events & EPOLLIN)
{
char szBuffer[3] = "";
int count = read(events[i].data.fd,szBuffer,2);
if(count <= 0)
{
printf("[%d]socket[%d] close\r\n",thread_id, events[i].data.fd);
removefd(epoll_id,events[i].data.fd);
close(events[i].data.fd);
}
else
{
printf("[%d]socket[%d] recv data:%s\r\n",thread_id, events[i].data.fd,szBuffer);
modifyfd(epoll_id,events[i].data.fd,true,false);
write(events[i].data.fd,"hellow word",11);
}
}
else if(events[i].events & EPOLLOUT)
{
printf("EPOLLOUT\r\n");
}
}
}
}
void handle_signal(int signal)
{
if(signal == SIGPIPE)
{
printf("recv sig pipe\r\n");
}
}
int main()
{
signal(SIGPIPE,handle_signal);
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(listen_sock == -1)
{
printf("socket error:%s\r\n", strerror(errno));
return -1;
}
int reuse = 1;
setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
struct sockaddr_in ser_addr = {0};
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(6360);
ser_addr.sin_addr.s_addr = INADDR_ANY;
if(-1 == bind(listen_sock, (struct sockaddr *)&ser_addr, sizeof(ser_addr)))
{
printf("bind socket error:%s\r\n", strerror(errno));
return -1;
}
if(-1 == listen(listen_sock,5))
{
printf("listen socket error:%s\r\n", strerror(errno));
return -1;
}
setnonblocking(listen_sock);
int epoll_id = epoll_create(5);
if(epoll_id == -1)
{
printf("epoll_create error:%s\r\n", strerror(errno));
return -1;
}
addfd(epoll_id,listen_sock,true,false);
std::thread t(handle_accepter_event,1,epoll_id,listen_sock);
std::thread t2(handle_accepter_event,2,epoll_id,listen_sock);
t.join();
t2.join();
printf("hellow word\r\n");
return 0;
}
这个demo没有考虑short write
问题,因为该代码只有监听套接字设置了非阻塞模式
,accept
的套接字没有设置非阻塞模式,所以使用send或者write
的时候不存在部分发送导致的short write
模式。
缓冲区可以参考:动画图解 socket 缓冲区的那些事儿
关于epoll
的多线程demo和short write
的解决方案可以参考:tcp 完美解决short write问题