在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

  1. 处理SIGPIPE信号,并将socket设置为非阻塞模式
  2. 将listen socket 设置EPOLLONESHOT并加入epoll
  3. 多线程​​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问题