目录
epoll 和select
epoll的接口
如何来使用epoll
epoll程序框架
实例源码
相关知识
Socket的阻塞模式和非阻塞模式
如何动态的改变listen监听的个数呢?
队列已满的情况,如何处理?
SYN泛滥攻击
参考
实例代码二
多进程Epoll:
建立2000+个链接的测试代码
关于ET、LT两种工作模式
epoll中读写数据 的注意事项
多路复用
讨论
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
并且,在linux/posix_types.h头文件有这样的声明:
#define __FD_SETSIZE 1024
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。
一、IO多路复用的select
IO多路复用相对于阻塞式和非阻塞式的好处就是它可以监听多个 socket ,并且不会消耗过多资源。当用户进程调用 select 时,它会监听其中所有 socket 直到有一个或多个 socket 数据已经准备好,否则就一直处于阻塞状态。select的缺点在于单个进程能够监视的文件描述符的数量存在最大限制,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的的开销也线性增长。同时,由于网络响应时间的延迟使得大量的tcp链接处于非常活跃状态,但调用select()会对所有的socket进行一次线性扫描,所以这也浪费了一定的开销。不过它的好处还有就是它的跨平台特性。
二、 异步IO的epoll
epoll的优点就是完全的异步,你只需要对其中 poll 函数注册相应的 socket 和事件,就可以完全不管。当有事件发生时,数据已经从内核态拷贝到用户态,也就是完全没有阻塞。
epoll的接口非常简单,一共就三个函数:
(1)epoll_create系统调用
epoll_create在C库中的原型如下。
int epoll_create(int size);
epoll_create返回一个句柄,之后 epoll的使用都将依靠这个句柄来标识。参数 size是告诉 epoll所要处理的大致事件数目。不再使用 epoll时,必须调用 close关闭这个句柄。
注意:size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在 Linux最新的一些内核版本的实现中,这个 size参数没有任何意义。
(2)epoll_ctl系统调用
epoll_ctl在C库中的原型如下。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。
参数:
epfd: epoll_create返回的句柄,
op:的意义见下表:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
fd:需要监听的socket句柄fd,
event:告诉内核需要监听什么事,struct epoll_event结构如下:
epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
data成员是一个epoll_data联合,其定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
可见,这个 data成员还与具体的使用方式相关。例如,ngx_epoll_module模块只使用了联合中的 ptr成员,
作为指向 ngx_connection_t连接的指针。我们在项目中一般使用的也是 ptr成员,因为它可以指向任意的结构
体地址。
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait在C库中的原型如下:
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
收集在 epoll监控的事件中已经发生的事件,如果 epoll中没有任何一个事件发生,则最多等待timeout毫秒后返回。epoll_wait的返回值表示当前发生的事件个数,如果返回0,则表示本次调用中没有事件发生,如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。
epfd:epoll的描述符。
events:分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。
maxevents:表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。
timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。
epoll有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。
默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。
(
水平触发LT:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写
边缘触发ET:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你
此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。)
1、包含一个头文件#include
2、create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
3、之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。
最后一个timeout:是epoll_wait的超时,
为0的时候表示马上返回,
为-1的时候表示一直等下去,直到有事件返回,
为任意正整数的时候表示等这么长的时间,如果一直没有事件,则返回。
一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait范围之后应该是一个循环,遍利所有的事件。
epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接
几乎所有的epoll程序都使用下面的框架:
伪代码:
关于epoll_wait返回值的一个简单测试
void test(int epollfd)
{
struct epoll_event events[MAX_EVENT_NUMBER];
int number;
while (1)
{
number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
printf("number : %2d\n\n", number);
for (i = 0; i < number; i++)
{
sockfd = events[i].data.fd;
if (sockfd == listenfd)
{/*用户上线*/
}
else if (events[i].events & EPOLLIN)
{/*有数据可读*/
}
else if (events[i].events & EPOLLOUT)
{/*有数据可写*/
}
else
{/*出错*/
}
}
}
}
通过测试发现epoll_wait返回值number是不会大于MAX_EVENT_NUMBER的。
测试过程中,连接的客户端数远大于MAX_EVENT_NUMBER,由此可以推论:epoll_wait()每次返回的是活跃客户端的个数,每次并将这些活跃的客户端信息加入到events[MAX_EVENT_NUMBER]。
由此可见,活跃客户端的个数相同的情况下,events[MAX_EVENT_NUMBER]越大,epoll_wait()函数执行次数越少,但是events[MAX_EVENT_NUMBER]越大越消耗存储资源。
所以,MAX_EVENT_NUMBER的选择应该在效率和资源间取一个平衡点。
示例代码:
for( ; ; )
{
nfds = epoll_wait(epfd,events,20,500);
for(i=0;ifd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
}
else
{
//其他的处理
}
}
}
大致流程
struct epoll_event ev, event_list[EVENT_MAX_COUNT];//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
listenfd = socket(AF_INET, SOCK_STREAM, 0);
ev.data.fd = listenfd; //设置与要处理的事件相关的文件描述符
ev.events = EPOLLIN | EPOLLET; //设置要处理的事件类型EPOLLIN :表示对应的文件描述符可以读,EPOLLET状态变化才通知
epfd = epoll_create(256); //生成用于处理accept的epoll专用的文件描述符
//注册epoll事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll实例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(监听listenfd)
if(0 != bind(listenfd, (struct sockaddr *)
if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定义了宏//#define LISTENQ 20
nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //等待epoll事件的发生
原文有相当多的如错误:
需要增加到头文件和错误修改
//'/0'->'\0'
//bzero() 替换为memset (注意二者参数不一样,bzero将前n个字节设为0,memset将前n 个字节的值设为值 c)
//local_addr 由char* 改为 string
#include
#include //atoi
#include //memset
#include //std:cout 等
修正后的源码C++ lnux:
#include
#include
#include
#include
#include
#include
#include
#include
#include
//'/0'->'\0'
//bzero() 替换为memset (注意二者参数不一样,bzero将前n个字节设为0,memset将前n 个字节的值设为值 c)
//local_addr 由char* 改为 string
#include
#include //atoi
#include //memset
#include //std:cout 等
using namespace std;
#define MAXLINE 255 //读写缓冲
#define OPEN_MAX 100
#define LISTENQ 20 //listen的第二个参数 定义TCP链接未完成队列的大小(linux >2.6 则表示accpet之前的队列)
#define SERV_PORT 5000
#define INFTIM 1000
#define TIMEOUT_MS 500
#define EVENT_MAX_COUNT 20
void setnonblocking(int sock)
{
int opts;
opts = fcntl(sock, F_GETFL);
if(opts < 0)
{
perror("fcntl(sock,GETFL)");
exit(1);
}
opts = opts | O_NONBLOCK;
if(fcntl(sock, F_SETFL, opts) < 0)
{
perror("fcntl(sock,SETFL,opts)");
exit(1);
}
}
int main(int argc, char *argv[])
{
int i, maxi, listenfd, connfd, sockfd, epfd, nfds, portnumber;
ssize_t n;
char line_buff[MAXLINE];
if ( 2 == argc )
{
if( (portnumber = atoi(argv[1])) < 0 )
{
fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
//fprintf()函数根据指定的format(格式)(格式)发送信息(参数)到由stream(流)指定的文件
//printf 将内容发送到Default的输出设备,通常为本机的显示器,fprintf需要指定输出设备,可以为文件,设备。
//stderr
return 1;
}
}
else
{
fprintf(stderr, "Usage:%s portnumber/r/n", argv[0]);
return 1;
}
//声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
struct epoll_event ev, event_list[EVENT_MAX_COUNT];
//生成用于处理accept的epoll专用的文件描述符
epfd = epoll_create(256); //生成epoll文件描述符,既在内核申请一空间,存放关注的socket fd上是否发生以及发生事件。size既epoll fd上能关注的最大socket fd数。随你定好了。只要你有空间。
struct sockaddr_in clientaddr;
socklen_t clilenaddrLen;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);//Unix/Linux“一切皆文件”,创建(套接字)文件,id=listenfd
if (listenfd < 0)
{
printf("socket error,errno %d:%s\r\n",errno,strerror(errno));
}
//把socket设置为非阻塞方式
//setnonblocking(listenfd);
//设置与要处理的事件相关的文件描述符
ev.data.fd = listenfd;
//设置要处理的事件类型
ev.events = EPOLLIN | EPOLLET; //EPOLLIN :表示对应的文件描述符可以读,EPOLLET状态变化才通知
//ev.events=EPOLLIN;
//注册epoll事件
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //epfd epoll实例ID,EPOLL_CTL_ADD添加,listenfd:socket,ev事件(监听listenfd)
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr=htonl(INADDR_ANY); /*IP,INADDR_ANY转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP*/
serveraddr.sin_port = htons(portnumber);
if(0 != bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)))
{
printf("bind error,errno %d:%s\r\n",errno,strerror(errno));
}
if(0 != listen(listenfd, LISTENQ)) //LISTENQ 定义了宏
{
printf("listen error,errno %d:%s\r\n",errno,strerror(errno));
}
maxi = 0;
for ( ; ; )
{
//等待epoll事件的发生
nfds = epoll_wait(epfd, event_list, EVENT_MAX_COUNT, TIMEOUT_MS); //epoll_wait(int epfd, struct epoll_event * event_list, int maxevents, int timeout),返回需要处理的事件数目
//处理所发生的所有事件
for(i = 0; i < nfds; ++i)
{
if(event_list[i].data.fd == listenfd) //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
{
clilenaddrLen = sizeof(struct sockaddr_in);//在调用accept()前,要给addrLen赋值,这样才不会出错,addrLen = sizeof(clientaddr);或addrLen = sizeof(struct sockaddr_in);
connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilenaddrLen);//(accpet详解:https://blog.csdn.net/David_xtd/article/details/7087843)
if(connfd < 0)
{
//perror("connfd<0:connfd= %d",connfd);
printf("connfd<0,accept error,errno %d:%s\r\n",errno,strerror(errno));
exit(1);
}
//setnonblocking(connfd);
char *str = inet_ntoa(clientaddr.sin_addr);//将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址
cout << "accapt a connection from " << str << endl;
//设置用于读操作的文件描述符
ev.data.fd = connfd;
//设置用于注测的读操作事件
ev.events = EPOLLIN | EPOLLET;
//ev.events=EPOLLIN;
//注册ev
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //将accpet的句柄添加进入(增加监听的对象)
}
else if(event_list[i].events & EPOLLIN) //如果是已经连接的用户,并且收到数据,那么进行读入。
{
cout << "EPOLLIN" << endl;
if ( (sockfd = event_list[i].data.fd) < 0)
continue;
if ( (n = read(sockfd, line_buff, MAXLINE)) < 0) //read时fd中的数据如果小于要读取的数据,就会引起阻塞?
{
if (errno == ECONNRESET)
{
close(sockfd);
event_list[i].data.fd = -1;
}
else
std::cout << "readline error" << std::endl;
}
else if (n == 0)
{
close(sockfd);
event_list[i].data.fd = -1;
}
line_buff[n] = '\0';
cout << "read " << line_buff << endl;
//设置用于写操作的文件描述符
ev.data.fd = sockfd;
//设置用于注测的写操作事件
ev.events = EPOLLOUT | EPOLLET; //EPOLLOUT:表示对应的文件描述符可以写;
//修改sockfd上要处理的事件为EPOLLOUT
//epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
else if(event_list[i].events & EPOLLOUT) // 如果有数据发送
{
sockfd = event_list[i].data.fd;
write(sockfd, line_buff, n);
//设置用于读操作的文件描述符
ev.data.fd = sockfd;
//设置用于注测的读操作事件
ev.events = EPOLLIN | EPOLLET;
//修改sockfd上要处理的事件为EPOLIN
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
}
}
return 0;
}
编译命令
linux下编译:g++ epoll.cpp -o epoll
命令行简单测试
curl 192.168.0.250:5000 -d "phone=123456789&name=Hwei"
https://www.cnblogs.com/duhuo/articles/4982386.html
如果指定值在源代码中是一个常值,那么增长其大小需要重新编译服务器程序。那么,我们可以为它设定一个缺省值,不过允许通过命令行选项或者环境变量来覆写该值。
void Listen(int fd, int backlog)
{
char *ptr;
if((ptr = getenv("LISTENQ")) != NULL)
backlog = atoi(ptr);
if(listen(fd, backlog) < 0)
printf("listen error\n");
}
当一个客户SYN到达时,若这个队列是满的,TCP就忽略该分节,也就是不会发送RST。
这么做的原因在于,队列已满的情况是暂时的,客户TCP如果没收收到RST,就会重发SYN,在队列有空闲的时候处理该请求。如果服务器TCP立即响应一个RST,客户的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不会再次重发SYN。而且客户端也不无区别该套接口的状态,是“队列已满”还是“该端口没有在监听”。
向某一目标服务器发送大量的SYN,用以填满一个或多个TCP端口的未完成队列。每个SYN的源IP地址都置成随机数(IP欺骗),这样防止攻击服务器获悉黑客的真实IP地址。通过伪造的SYN装满未完成连接队列,使得合法的SYN不能排上队,导致针对合法用户的服务被拒绝。
防御方法:
SYN:同步序列编号(Synchronize Sequence Numbers)
它们的含义是:
SYN表示建立连接,
FIN表示关闭连接,
ACK表示响应,
PSH表示有
DATA数据传输,
RST表示连接重置。
SYN(synchronous建立联机)
ACK(acknowledgement 确认)
PSH(push传送)
FIN(finish结束)
RST(reset重置)
URG(urgent紧急)
Sequence number(顺序号码)
Acknowledge number(确认号码)
三次握手:
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。完成三次握手,客户端与服务器开始传送数据.
第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;
第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包;
第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
Unix网络编程
基于主动网的SYN攻击防御
摘自:https://www.cnblogs.com/coder2012/archive/2013/12/13/3472677.html
《listen() 函数》
https://www.cnblogs.com/love-yh/p/7518552.html
code sample
https://blog.csdn.net/shenya1314/article/details/73691088
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PROCESS_NUM 10
static int
create_and_bind (char *port)
{
int fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(atoi(port));
bind(fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
return fd;
}
static int
make_socket_non_blocking (int sfd)
{
int flags, s;
flags = fcntl (sfd, F_GETFL, 0);
if (flags == -1)
{
perror ("fcntl");
return -1;
}
flags |= O_NONBLOCK;
s = fcntl (sfd, F_SETFL, flags);
if (s == -1)
{
perror ("fcntl");
return -1;
}
return 0;
}
#define MAXEVENTS 64
int
main (int argc, char *argv[])
{
int sfd, s;
int efd;
struct epoll_event event;
struct epoll_event *events;
sfd = create_and_bind("1234");
if (sfd == -1)
abort ();
s = make_socket_non_blocking (sfd);
if (s == -1)
abort ();
s = listen(sfd, SOMAXCONN);
if (s == -1)
{
perror ("listen");
abort ();
}
efd = epoll_create(MAXEVENTS);
if (efd == -1)
{
perror("epoll_create");
abort();
}
event.data.fd = sfd;
//event.events = EPOLLIN | EPOLLET;
event.events = EPOLLIN;
s = epoll_ctl(efd, EPOLL_CTL_ADD, sfd, &event);
if (s == -1)
{
perror("epoll_ctl");
abort();
}
/* Buffer where events are returned */
events = calloc(MAXEVENTS, sizeof event);
int k;
for(k = 0; k < PROCESS_NUM; k++)
{
int pid = fork();
if(pid == 0)
{
/* The event loop */
while (1)
{
int n, i;
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("process %d return from epoll_wait!\n", getpid());
/* sleep here is very important!*/
//sleep(2);
for (i = 0; i < n; i++)
{
if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN)))
{
/* An error has occured on this fd, or the socket is not
ready for reading (why were we notified then?) */
fprintf (stderr, "epoll error\n");
close (events[i].data.fd);
continue;
}
else if (sfd == events[i].data.fd)
{
/* We have a notification on the listening socket, which
means one or more incoming connections. */
struct sockaddr in_addr;
socklen_t in_len;
int infd;
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
in_len = sizeof in_addr;
infd = accept(sfd, &in_addr, &in_len);
if (infd == -1)
{
printf("process %d accept failed!\n", getpid());
break;
}
printf("process %d accept successed!\n", getpid());
/* Make the incoming socket non-blocking and add it to the
list of fds to monitor. */
close(infd);
}
}
}
}
}
int status;
wait(&status);
free (events);
close (sfd);
return EXIT_SUCCESS;
}
https://www.jianshu.com/p/362b56b573f4
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
const int MAXLINE = 5;
int count = 1;
static int make_socket_non_blocking(int fd)
{
int flags, s;
flags = fcntl (fd, F_GETFL, 0);
if (flags == -1)
{
perror ("fcntl");
return -1;
}
flags |= O_NONBLOCK;
s = fcntl (fd, F_SETFL, flags);
if (s == -1)
{
perror ("fcntl");
return -1;
}
return 0;
}
void sockconn()
{
int sockfd;
struct sockaddr_in server_addr;
struct hostent *host;
char buf[100];
unsigned int value = 1;
host = gethostbyname("127.0.0.1");
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket error\r\n");
return;
}
//setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value));
//make_socket_non_blocking(sockfd);
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr = *((struct in_addr*) host->h_addr);
int cn = connect(sockfd, (struct sockaddr *) &server_addr,
sizeof(server_addr));
if (cn == -1) {
printf("connect error errno=%d\r\n", errno);
return;
}
// char *buf = "h";
sprintf(buf, "%d", count);
count++;
write(sockfd, buf, strlen(buf));
close(sockfd);
printf("client send %s\r\n", buf);
return;
}
int main(void) {
int i;
for (i = 0; i < 2000; i++)
{
sockconn();
}
return 0;
}
水平触发LT:
其中LT就是与select和poll类似,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写
边缘触发ET:
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你
由此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。
原文链接:https://blog.csdn.net/hahachenchen789/article/details/83276058
4、关于ET、LT两种工作模式
可以得出这样的结论:
ET模式仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用ET模式,需要一直read/write直到出错为止,很多人反映为什么采用ET模式只接收了一部分数据就再也得不到通知了,大多因为这样;而LT模式是只要有数据没有处理就会一直通知下去的.
(https://blog.csdn.net/ctthuangcheng/article/details/9716715)
在一个非阻塞的socket上调用read/write函数,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。
从字面上看,意思是:
EAGAIN: 再试一次
EWOULDBLOCK:如果这是一个阻塞socket, 操作将被block
perror输出:Resource temporarily unavailable
总结:
这个错误表示资源暂时不够,可能read时, 读缓冲区没有数据, 或者write时,写缓冲区满了。
遇到这种情况,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 时errno设置为EAGAIN。
所以对于阻塞socket、 read/write返回-1代表网络出错了。但对于非阻塞socket、read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。
本文主要讲述epoll模型(不完全是针对epoll)下读写数据接口使用的注意事项
1、read write
函数原型如下:
#include
ssize_t read(int filedes, void* buf, size_t nbytes)
ssize_t write(int filedes, const void* buf, size_t nbytes)
其中,read返回实际读取到的字节数。但实际读取的字节很有可能少于指定要读取的字节数nbytes。因此会分为:
①返回值大于0。 读取正常,返回实际读取到的字节数
②返回值等于0。 读取异常,读取到文件filedes结尾处了。这里逻辑上要理解为read已经读取完数据
③返回值小于0(-1)。 读取出错,在处理网络请求时可能是网络异常。着重注意当返回-1,此时errno的值EAGAIN、EWOULLDBLOCK,表示内核对应的读缓冲区为空
而write返回的实际写入字节数正常情况是与制定写入的字节数nbytes相同的,不相等说明写入异常了,着重注意,此时errno的值EAGAIN、EWOULLDBLOCK,表示内核对应的写缓冲区为空。注,EAGAIN等同于EWOULLDBLOCK。
总之,这个错误表示资源暂时不够,可能read时读缓冲区没有数据, 或者write时写缓冲区满了。遇到这种情况,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同时errno设置为EAGAIN。
所以对于阻塞socket、 read/write返回-1代表网络出错了。但对于非阻塞socket、read/write返回-1不一定网络真的出错了。可能支持缓冲区空或者满,这时应该再试,直到Resource available。
综上,对于非阻塞的socket,正确的读写操作为:
LT模式
读: 忽略掉errno = EAGAIN的错误,下次继续读;
写:忽略掉errno = EAGAIN的错误,下次继续写。
对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。
下面来介绍下epoll事件的两种模式LT(水平触发)和ET(边沿触发),根据可以理解为,文件描述符的读写状态发生变化才会触发epoll事件,具体说来如下:二者的差异在于 level-trigger 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger 模式下只有某个 socket 从 unreadable 变为 readable,或从unwritable 变为writable时,epoll_wait 才会返回该 socket。如下两个示意图:
从socket读数据:
往socket写数据:
所以在epoll的ET模式下,正确的读写方式为:
读: 只要可读, 就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写, 就一直写,直到数据发送完,或者 errno = EAGAIN
这里的意思是,对于ET模式,相当于我们要自己重写read和write,使其像”原子操作“一样,保证一次read 或 write能够完整的读完缓冲区的数据或者写完要写入缓冲区的数据。因此,实现为用while包住read和write即可。但是对于select或者LT模式,我们可以只使用一次read和write,因为在主程序中会一直while,而事件再下一次select时还会被获取到。但也可以实现为用while包住read和write。从逻辑上讲,一次性把数据读取完整可以保证数据的完整性。
下面来说明这种”原子操作“read和write
int n = 0;
while(1)
{
nread = read(fd, buf + n, BUFSIZ - 1); //读时,用户进程指定的接收数据缓冲区大小固定,一般要比数据大
if(nread < 0)
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
continue;
}
else
{
break; //or return;
}
}
else if(nread == 0)
{
break; //or return. because read the EOF
}
else
{
n += nread;
}
}
int data_size = strlen(buf);
int n = 0;
while(1)
{
nwrite = write(fd, buf + n, data_size);//写时,数据大小一直在变化
if(nwrite < data_size)
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
continue;
}
else
{
break;//or return;
}
}
else
{
n += nwrite;
data_size -= nwrite;
}
}
注参考文章:
http://blog.csdn.net/ctthuangcheng/article/details/9716715
正确的accept,accept 要考虑 2 个问题:参考<<UNIX网络编程——epoll的 et,lt关注点>>讲解的更加详细
(1) LT模式下或ET模式下,阻塞的监听socket, accept 存在的问题
accept每次都是从已经完成三次握手的tcp队列中取出一个连接,考虑这种情况: TCP 连接被客户端夭折,即在服务器调用 accept 之前,客户端主动发送 RST 终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在 accept 调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上,就绪队列中的其他描述符都得不到处理。
解决办法是:把监听套接口设置为非阻塞,当客户在服务器调用 accept 之前中止某个连接时,accept 调用可以立即返回 -1, 这时源自 Berkeley 的实现会在内核中处理该事件,并不会将该事件通知给 epool,而其他实现把 errno 设置为 ECONNABORTED 或者 EPROTO 错误,我们应该忽略这两个错误。
(2) ET 模式下 accept 存在的问题
考虑这种情况:多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。
解决办法是:将监听套接字设置为非阻塞模式,用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。
综合以上两种情况,服务器应该使用非阻塞地 accept, accept 在 ET 模式下 的正确使用方式为:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
(size_t *)&addrlen)) > 0) {
handle_client(conn_sock);
}
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED
&& errno != EPROTO && errno != EINTR)
perror("accept");
}
一道腾讯后台开发的面试题:
使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发 socket 可写的事件,如何处理?
需要向 socket 写数据的时候才把 socket 加入 epoll ,等待可写事件。接受到可写事件后,调用 write 或者 send 发送数据。当所有数据都写完后,把 socket 移出 epoll。
这种方式的缺点是,即使发送很少的数据,也要把 socket 加入 epoll,写完后在移出 epoll,有一定操作代价。
开始不把 socket 加入 epoll,需要向 socket 写数据的时候,直接调用 write 或者 send 发送数据。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驱动下写数据,全部数据发送完毕后,再移出 epoll。
这种方式的优点是:数据不多的时候可以避免 epoll 的事件处理,提高效率。
如文初的说明表示,这三者都是 I/O 多路复用机制,且简要介绍了多路复用的定义,那么如何更加直观地了解多路复用呢?这里有张图:
对于网页服务器 Nginx 来说,会有很多连接进来, epoll 会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
一般来说以下场合需要使用 I/O 多路复用:
#1楼 epoll 自身是基于 IO 多路复用模式的同步IO模型,可实现是基于事件驱动的异步非阻塞的进程模型。不要把进程模型的异步分阻塞看成异步IO。
#2楼 epoll只是非阻塞,不是异步,linux真正的异步是aio系列函数!
#3楼
调用epoll是会进入等待的(取决设置timeout的值),而且在数据从内核态取到用户态也是阻塞的,严格意义上说,epoll实现的多路复用IO是阻塞同步IO。相对于普通阻塞同步IO,就是能够不开启多线程来处理多个IO请求。资料里说Linux下的真正的AIO还不成熟,不清楚。
https://www.cnblogs.com/zhouyang123200/p/6613302.html